From 49b7726ac866ccc7add1c516c8865eb3847dcb14 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Mon, 14 Feb 2022 15:09:01 +0200 Subject: [PATCH 001/517] - 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/517] 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/517] 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/517] 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/517] 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/517] 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/517] 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/517] 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/517] 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/517] 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/517] 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/517] 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/517] 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/517] 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/517] 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/517] 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/517] 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/517] 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/517] 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/517] 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/517] 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/517] 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/517] 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/517] 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/517] 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/517] 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/517] 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/517] 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/517] 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 221e9b85df82d2df34442b4c1107816a2c6b77c1 Mon Sep 17 00:00:00 2001 From: Michael Kaye <1917473+michaelkaye@users.noreply.github.com> Date: Mon, 28 Feb 2022 17:06:34 +0000 Subject: [PATCH 030/517] Ensure we have logcat for nightly runs --- .github/workflows/nightly.yml | 55 ++++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 192be1fe9e..d114a2aa11 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -104,7 +104,13 @@ jobs: force-avd-creation: false emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none emulator-build: 7425822 - script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.package='org.matrix.android.sdk.session' matrix-sdk-android:connectedDebugAndroidTest + script: | + adb root + adb logcat -c + touch emulator-session.log + chmod 777 emulator-session.log + adb logcat >> emulator-session.log & + ./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.package='org.matrix.android.sdk.session' matrix-sdk-android:connectedDebugAndroidTest - name: Read Results [org.matrix.android.sdk.session] if: always() id: get-comment-body-session @@ -119,7 +125,13 @@ jobs: force-avd-creation: false emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none emulator-build: 7425822 - script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.package='org.matrix.android.sdk.account' matrix-sdk-android:connectedDebugAndroidTest + script: | + adb root + adb logcat -c + touch emulator-account.log + chmod 777 emulator-account.log + adb logcat >> emulator-account.log & + ./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.package='org.matrix.android.sdk.account' matrix-sdk-android:connectedDebugAndroidTest - name: Read Results [org.matrix.android.sdk.account] if: always() id: get-comment-body-account @@ -135,7 +147,13 @@ jobs: force-avd-creation: false emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none emulator-build: 7425822 - script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.package='org.matrix.android.sdk.internal' matrix-sdk-android:connectedDebugAndroidTest + script: | + adb root + adb logcat -c + touch emulator-internal.log + chmod 777 emulator-internal.log + adb logcat >> emulator-internal.log & + ./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.package='org.matrix.android.sdk.internal' matrix-sdk-android:connectedDebugAndroidTest - name: Read Results [org.matrix.android.sdk.internal] if: always() id: get-comment-body-internal @@ -151,7 +169,13 @@ jobs: force-avd-creation: false emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none emulator-build: 7425822 - script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.package='org.matrix.android.sdk.ordering' matrix-sdk-android:connectedDebugAndroidTest + script: | + adb root + adb logcat -c + touch emulator-ordering.log + chmod 777 emulator-ordering.log + adb logcat >> emulator-ordering.log & + ./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.package='org.matrix.android.sdk.ordering' matrix-sdk-android:connectedDebugAndroidTest - name: Read Results [org.matrix.android.sdk.ordering] if: always() id: get-comment-body-ordering @@ -167,7 +191,13 @@ jobs: force-avd-creation: false emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none emulator-build: 7425822 - script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.class='org.matrix.android.sdk.PermalinkParserTest' matrix-sdk-android:connectedDebugAndroidTest + script: | + adb root + adb logcat -c + touch emulator-permalink.log + chmod 777 emulator-permalink.log + adb logcat >> emulator-permalink.log & + ./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.class='org.matrix.android.sdk.PermalinkParserTest' matrix-sdk-android:connectedDebugAndroidTest - name: Read Results [org.matrix.android.sdk.PermalinkParserTest] if: always() id: get-comment-body-permalink @@ -196,6 +226,17 @@ jobs: - `[org.matrix.android.sdk.ordering]`
${{ steps.get-comment-body-ordering.outputs.ordering }} - `[org.matrix.android.sdk.PermalinkParserTest]`
${{ steps.get-comment-body-permalink.outputs.permalink }} edit-mode: replace + - name: Upload Test Report Log + uses: actions/upload-artifact@v2 + if: always() + with: + name: integrationtest-error-results + path: | + emulator-permalink.log + emulator-internal.log + emulator-ordering.log + emulator-account.log + emulator-session.log ui-tests: name: UI Tests (Synapse) @@ -250,7 +291,7 @@ jobs: uses: actions/upload-artifact@v2 if: always() with: - name: sanity-error-results + name: uitest-error-results path: | emulator.log failure_screenshots/ @@ -270,4 +311,4 @@ jobs: matrix_access_token: ${{ secrets.ELEMENT_ANDROID_NOTIFICATION_ACCESS_TOKEN }} matrix_room_id: ${{ secrets.ELEMENT_ANDROID_INTERNAL_ROOM_ID }} text_template: "Nightly test run: {{#each job_statuses }}{{#with this }}{{#if completed }} {{name}} {{conclusion}} at {{completed_at}}, {{/if}}{{/with}}{{/each}}" - html_template: "Nightly test run results: {{#each job_statuses }}{{#with this }}{{#if completed }}
{{name}} {{conclusion}} at {{completed_at}} [details]{{/if}}{{/with}}{{/each}}" \ No newline at end of file + html_template: "Nightly test run results: {{#each job_statuses }}{{#with this }}{{#if completed }}
{{name}} {{conclusion}} at {{completed_at}} [details]{{/if}}{{/with}}{{/each}}" From 39d1fc939a9980e19a01f69719ef82eb7332f54d Mon Sep 17 00:00:00 2001 From: Michael Kaye <1917473+michaelkaye@users.noreply.github.com> Date: Mon, 28 Feb 2022 17:08:52 +0000 Subject: [PATCH 031/517] Use pkill (killall has a failuremode of killing PID 1 on some systems) --- .github/workflows/nightly.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index d114a2aa11..449c494600 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -115,6 +115,9 @@ jobs: if: always() id: get-comment-body-session run: python3 ./tools/ci/render_test_output.py session ./matrix-sdk-android/build/outputs/androidTest-results/connected/*.xml + - name: Remove adb logcat + if: always() + run: pkill -9 adb - name: Run integration tests for Matrix SDK [org.matrix.android.sdk.account] API[${{ matrix.api-level }}] if: always() uses: reactivecircus/android-emulator-runner@v2 @@ -136,6 +139,9 @@ jobs: if: always() id: get-comment-body-account run: python3 ./tools/ci/render_test_output.py account ./matrix-sdk-android/build/outputs/androidTest-results/connected/*.xml + - name: Remove adb logcat + if: always() + run: pkill -9 adb # package: org.matrix.android.sdk.internal - name: Run integration tests for Matrix SDK [org.matrix.android.sdk.internal] API[${{ matrix.api-level }}] if: always() @@ -158,6 +164,9 @@ jobs: if: always() id: get-comment-body-internal run: python3 ./tools/ci/render_test_output.py internal ./matrix-sdk-android/build/outputs/androidTest-results/connected/*.xml + - name: Remove adb logcat + if: always() + run: pkill -9 adb # package: org.matrix.android.sdk.ordering - name: Run integration tests for Matrix SDK [org.matrix.android.sdk.ordering] API[${{ matrix.api-level }}] if: always() @@ -180,6 +189,9 @@ jobs: if: always() id: get-comment-body-ordering run: python3 ./tools/ci/render_test_output.py ordering ./matrix-sdk-android/build/outputs/androidTest-results/connected/*.xml + - name: Remove adb logcat + if: always() + run: pkill -9 adb # package: class PermalinkParserTest - name: Run integration tests for Matrix SDK class [org.matrix.android.sdk.PermalinkParserTest] API[${{ matrix.api-level }}] if: always() @@ -202,6 +214,9 @@ jobs: if: always() id: get-comment-body-permalink run: python3 ./tools/ci/render_test_output.py permalink ./matrix-sdk-android/build/outputs/androidTest-results/connected/*.xml + - name: Remove adb logcat + if: always() + run: pkill -9 adb # package: class PermalinkParserTest - name: Find Comment if: always() && github.event_name == 'pull_request' From 12ea262ebc3d9e960a0dd8ee97468bedd3629c0b Mon Sep 17 00:00:00 2001 From: oksya8and8 Date: Mon, 28 Feb 2022 20:45:22 +0000 Subject: [PATCH 032/517] 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 033/517] 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 27f7fadb3d07d071124a5b9561213ae76eb15216 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Feb 2022 23:09:55 +0000 Subject: [PATCH 034/517] Bump oss-licenses-plugin from 0.10.4 to 0.10.5 Bumps oss-licenses-plugin from 0.10.4 to 0.10.5. --- updated-dependencies: - dependency-name: com.google.android.gms:oss-licenses-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 9cae9e7e70..013d3bfa5b 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ buildscript { classpath libs.gradle.hiltPlugin classpath 'com.google.gms:google-services:4.3.10' classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.3' - classpath 'com.google.android.gms:oss-licenses-plugin:0.10.4' + classpath 'com.google.android.gms:oss-licenses-plugin:0.10.5' classpath "com.likethesalad.android:stem-plugin:2.0.0" // NOTE: Do not place your application dependencies here; they belong From cb00a668fe48b0d5f0ea77ecaaf1a9848719341a Mon Sep 17 00:00:00 2001 From: Michael Kaye <1917473+michaelkaye@users.noreply.github.com> Date: Tue, 1 Mar 2022 11:42:54 +0000 Subject: [PATCH 035/517] Format unit test results as well --- .github/workflows/tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d6e194916b..3d24108084 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -31,6 +31,9 @@ jobs: ${{ runner.os }}-gradle- - name: Run unit tests run: ./gradlew clean test $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false --stacktrace + - name: Format unit test results + if: always() + run: python3 ./tools/ci/render_test_output.py unit ./**/build/test-results/**/*.xml - name: Publish Unit Test Results uses: EnricoMi/publish-unit-test-result-action@v1 if: always() && From 7837d1d6d65e4287152cc7f7105253ba7575369d Mon Sep 17 00:00:00 2001 From: Michael Kaye <1917473+michaelkaye@users.noreply.github.com> Date: Wed, 2 Mar 2022 10:05:07 +0000 Subject: [PATCH 036/517] Wrap up the argument list; it can be long --- tools/ci/render_test_output.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/ci/render_test_output.py b/tools/ci/render_test_output.py index 48dd3987a3..9df3170058 100755 --- a/tools/ci/render_test_output.py +++ b/tools/ci/render_test_output.py @@ -9,9 +9,9 @@ import sys import xml.etree.ElementTree as ET suitename = sys.argv[1] xmlfiles = sys.argv[2:] - -print(f"Arguments: {sys.argv}") - +print("::group::Arguments") +print(f"{sys.argv}") +print("::endgroup::") for xmlfile in xmlfiles: print(f"Handling: {xmlfile}") tree = ET.parse(xmlfile) From 0c628905dea5ea5a8463ebdc61000f5afbde9106 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 2 Mar 2022 10:08:43 +0000 Subject: [PATCH 037/517] Lifting debug overrides to their own abstraction (#5361) * separating the debug overrides to their own abstraction - rather than sharing the user facing vector data store * inlining the debug flow getters and declarations - also replaces funs with vals as the references are immutable * adding changelog entry --- changelog.d/5361.misc | 1 + .../app/features/debug/di/FeaturesModule.kt | 16 ++++++ .../debug/features/DebugVectorOverrides.kt | 54 +++++++++++++++++++ .../settings/DebugPrivateSettingsViewModel.kt | 17 +++--- .../app/features/DefaultVectorOverrides.kt | 30 +++++++++++ .../app/features/home/HomeDetailViewModel.kt | 7 +-- .../onboarding/OnboardingViewModel.kt | 4 +- .../app/features/settings/VectorDataStore.kt | 27 ---------- .../im/vector/app/core/di/FeaturesModule.kt | 7 +++ 9 files changed, 124 insertions(+), 39 deletions(-) create mode 100644 changelog.d/5361.misc create mode 100644 vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorOverrides.kt create mode 100644 vector/src/main/java/im/vector/app/features/DefaultVectorOverrides.kt diff --git a/changelog.d/5361.misc b/changelog.d/5361.misc new file mode 100644 index 0000000000..d49554c7e7 --- /dev/null +++ b/changelog.d/5361.misc @@ -0,0 +1 @@ +Creates dedicated VectorOverrides for forcing behaviour for local testing/development \ No newline at end of file diff --git a/vector/src/debug/java/im/vector/app/features/debug/di/FeaturesModule.kt b/vector/src/debug/java/im/vector/app/features/debug/di/FeaturesModule.kt index 0c4a3ef637..3a68a0b956 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/di/FeaturesModule.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/di/FeaturesModule.kt @@ -23,8 +23,11 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import im.vector.app.features.DefaultVectorFeatures +import im.vector.app.features.DefaultVectorOverrides import im.vector.app.features.VectorFeatures +import im.vector.app.features.VectorOverrides import im.vector.app.features.debug.features.DebugVectorFeatures +import im.vector.app.features.debug.features.DebugVectorOverrides @InstallIn(SingletonComponent::class) @Module @@ -33,6 +36,9 @@ interface FeaturesModule { @Binds fun bindFeatures(debugFeatures: DebugVectorFeatures): VectorFeatures + @Binds + fun bindOverrides(debugOverrides: DebugVectorOverrides): VectorOverrides + companion object { @Provides @@ -44,5 +50,15 @@ interface FeaturesModule { fun providesDebugVectorFeatures(context: Context, defaultVectorFeatures: DefaultVectorFeatures): DebugVectorFeatures { return DebugVectorFeatures(context, defaultVectorFeatures) } + + @Provides + fun providesDefaultVectorOverrides(): DefaultVectorOverrides { + return DefaultVectorOverrides() + } + + @Provides + fun providesDebugVectorOverrides(context: Context): DebugVectorOverrides { + return DebugVectorOverrides(context) + } } } diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorOverrides.kt b/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorOverrides.kt new file mode 100644 index 0000000000..4394f5436e --- /dev/null +++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorOverrides.kt @@ -0,0 +1,54 @@ +/* + * 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.debug.features + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.preferencesDataStore +import im.vector.app.features.VectorOverrides +import kotlinx.coroutines.flow.map +import org.matrix.android.sdk.api.extensions.orFalse + +private val Context.dataStore: DataStore by preferencesDataStore(name = "vector_overrides") +private val keyForceDialPadDisplay = booleanPreferencesKey("force_dial_pad_display") +private val keyForceLoginFallback = booleanPreferencesKey("force_login_fallback") + +class DebugVectorOverrides(private val context: Context) : VectorOverrides { + + override val forceDialPad = context.dataStore.data.map { preferences -> + preferences[keyForceDialPadDisplay].orFalse() + } + + override val forceLoginFallback = context.dataStore.data.map { preferences -> + preferences[keyForceLoginFallback].orFalse() + } + + suspend fun setForceDialPadDisplay(force: Boolean) { + context.dataStore.edit { settings -> + settings[keyForceDialPadDisplay] = force + } + } + + suspend fun setForceLoginFallback(force: Boolean) { + context.dataStore.edit { settings -> + settings[keyForceLoginFallback] = force + } + } +} diff --git a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt index 038b1e6cc7..8d040d4773 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt @@ -24,12 +24,12 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel -import im.vector.app.features.settings.VectorDataStore +import im.vector.app.features.debug.features.DebugVectorOverrides import kotlinx.coroutines.launch class DebugPrivateSettingsViewModel @AssistedInject constructor( @Assisted initialState: DebugPrivateSettingsViewState, - private val vectorDataStore: VectorDataStore + private val debugVectorOverrides: DebugVectorOverrides ) : VectorViewModel(initialState) { @AssistedFactory @@ -44,11 +44,12 @@ class DebugPrivateSettingsViewModel @AssistedInject constructor( } private fun observeVectorDataStore() { - vectorDataStore.forceDialPadDisplayFlow.setOnEach { - copy(dialPadVisible = it) + debugVectorOverrides.forceDialPad.setOnEach { + copy( + dialPadVisible = it + ) } - - vectorDataStore.forceLoginFallbackFlow.setOnEach { + debugVectorOverrides.forceLoginFallback.setOnEach { copy(forceLoginFallback = it) } } @@ -62,13 +63,13 @@ class DebugPrivateSettingsViewModel @AssistedInject constructor( private fun handleSetDialPadVisibility(action: DebugPrivateSettingsViewActions.SetDialPadVisibility) { viewModelScope.launch { - vectorDataStore.setForceDialPadDisplay(action.force) + debugVectorOverrides.setForceDialPadDisplay(action.force) } } private fun handleSetForceLoginFallbackEnabled(action: DebugPrivateSettingsViewActions.SetForceLoginFallbackEnabled) { viewModelScope.launch { - vectorDataStore.setForceLoginFallbackFlow(action.force) + debugVectorOverrides.setForceLoginFallback(action.force) } } } diff --git a/vector/src/main/java/im/vector/app/features/DefaultVectorOverrides.kt b/vector/src/main/java/im/vector/app/features/DefaultVectorOverrides.kt new file mode 100644 index 0000000000..4128fdbe3c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/DefaultVectorOverrides.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 im.vector.app.features + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +interface VectorOverrides { + val forceDialPad: Flow + val forceLoginFallback: Flow +} + +class DefaultVectorOverrides : VectorOverrides { + override val forceDialPad = flowOf(false) + override val forceLoginFallback = flowOf(false) +} diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt index d7239373bd..e812942996 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt @@ -28,6 +28,7 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.VectorOverrides import im.vector.app.features.call.dialpad.DialPadLookup import im.vector.app.features.call.lookup.CallProtocolsChecker import im.vector.app.features.call.webrtc.WebRtcCallManager @@ -67,7 +68,8 @@ class HomeDetailViewModel @AssistedInject constructor( private val callManager: WebRtcCallManager, private val directRoomHelper: DirectRoomHelper, private val appStateHandler: AppStateHandler, - private val autoAcceptInvites: AutoAcceptInvites + private val autoAcceptInvites: AutoAcceptInvites, + private val vectorOverrides: VectorOverrides ) : VectorViewModel(initialState), CallProtocolsChecker.Listener { @@ -106,8 +108,7 @@ class HomeDetailViewModel @AssistedInject constructor( pushCounter = nbOfPush ) } - - vectorDataStore.forceDialPadDisplayFlow.setOnEach { force -> + vectorOverrides.forceDialPad.setOnEach { force -> copy( forceDialPadTab = force ) diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt index 63f1875235..c8f7ac5b3d 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt @@ -37,6 +37,7 @@ import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.ensureTrailingSlash import im.vector.app.features.VectorFeatures +import im.vector.app.features.VectorOverrides import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.extensions.toTrackingValue import im.vector.app.features.analytics.plan.UserProperties @@ -81,6 +82,7 @@ class OnboardingViewModel @AssistedInject constructor( private val vectorFeatures: VectorFeatures, private val analyticsTracker: AnalyticsTracker, private val vectorDataStore: VectorDataStore, + private val vectorOverrides: VectorOverrides ) : VectorViewModel(initialState) { @AssistedFactory @@ -102,7 +104,7 @@ class OnboardingViewModel @AssistedInject constructor( } private fun observeDataStore() = viewModelScope.launch { - vectorDataStore.forceLoginFallbackFlow.setOnEach { isForceLoginFallbackEnabled -> + vectorOverrides.forceLoginFallback.setOnEach { isForceLoginFallbackEnabled -> copy(isForceLoginFallbackEnabled = isForceLoginFallbackEnabled) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorDataStore.kt b/vector/src/main/java/im/vector/app/features/settings/VectorDataStore.kt index a7981a8b2a..74b3794b2c 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorDataStore.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorDataStore.kt @@ -19,13 +19,11 @@ package im.vector.app.features.settings import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.preferencesDataStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import org.matrix.android.sdk.api.extensions.orFalse import javax.inject.Inject private val Context.dataStore: DataStore by preferencesDataStore(name = "vector_settings") @@ -46,29 +44,4 @@ class VectorDataStore @Inject constructor( settings[pushCounter] = currentCounterValue + 1 } } - - // For debug only - private val forceDialPadDisplay = booleanPreferencesKey("force_dial_pad_display") - - val forceDialPadDisplayFlow: Flow = context.dataStore.data.map { preferences -> - preferences[forceDialPadDisplay].orFalse() - } - - suspend fun setForceDialPadDisplay(force: Boolean) { - context.dataStore.edit { settings -> - settings[forceDialPadDisplay] = force - } - } - - private val forceLoginFallback = booleanPreferencesKey("force_login_fallback") - - val forceLoginFallbackFlow: Flow = context.dataStore.data.map { preferences -> - preferences[forceLoginFallback].orFalse() - } - - suspend fun setForceLoginFallbackFlow(force: Boolean) { - context.dataStore.edit { settings -> - settings[forceLoginFallback] = force - } - } } diff --git a/vector/src/release/java/im/vector/app/core/di/FeaturesModule.kt b/vector/src/release/java/im/vector/app/core/di/FeaturesModule.kt index ad3416f51a..b20dd885c2 100644 --- a/vector/src/release/java/im/vector/app/core/di/FeaturesModule.kt +++ b/vector/src/release/java/im/vector/app/core/di/FeaturesModule.kt @@ -21,7 +21,9 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import im.vector.app.features.DefaultVectorFeatures +import im.vector.app.features.DefaultVectorOverrides import im.vector.app.features.VectorFeatures +import im.vector.app.features.VectorOverrides @InstallIn(SingletonComponent::class) @Module @@ -31,4 +33,9 @@ object FeaturesModule { fun providesFeatures(): VectorFeatures { return DefaultVectorFeatures() } + + @Provides + fun providesOverrides(): VectorOverrides { + return DefaultVectorOverrides() + } } From 214e0efcd990a6b4c9d491819e010dff9cc13069 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Wed, 2 Mar 2022 13:47:08 +0200 Subject: [PATCH 038/517] 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 4bbb60cc65b84d863629586411b4ac546c1f6a05 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 2 Mar 2022 15:22:59 +0100 Subject: [PATCH 039/517] White list group `org.webjars` --- dependencies_groups.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle index 7de8100469..e8bc2484d2 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -175,6 +175,7 @@ ext.groups = [ 'org.sonatype.oss', 'org.testng', 'org.threeten', + 'org.webjars', 'ru.noties', 'xerces', 'xml-apis', From ab2001cd7f8f97440a57a7f85e28723c32bc20a2 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 2 Mar 2022 17:45:27 +0300 Subject: [PATCH 040/517] Create a custom audio waveform view. --- .../main/res/values/styles_voice_message.xml | 16 +- .../detail/composer/VoiceMessageHelper.kt | 4 +- .../composer/voice/VoiceMessageViews.kt | 12 +- .../timeline/factory/MessageItemFactory.kt | 4 +- .../helper/VoiceMessagePlaybackTracker.kt | 23 +- .../detail/timeline/item/MessageVoiceItem.kt | 40 ++-- .../app/features/voice/AudioWaveformView.kt | 199 ++++++++++++++++++ .../layout/item_timeline_event_voice_stub.xml | 2 +- .../layout/view_voice_message_recorder.xml | 2 +- .../main/res/values/audio_waveform_attr.xml | 22 ++ 10 files changed, 287 insertions(+), 37 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/voice/AudioWaveformView.kt create mode 100644 vector/src/main/res/values/audio_waveform_attr.xml diff --git a/library/ui-styles/src/main/res/values/styles_voice_message.xml b/library/ui-styles/src/main/res/values/styles_voice_message.xml index 2e87353303..81d2e7581d 100644 --- a/library/ui-styles/src/main/res/values/styles_voice_message.xml +++ b/library/ui-styles/src/main/res/values/styles_voice_message.xml @@ -2,14 +2,14 @@ \ No newline at end of file diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt index 735d356476..f9dfecd1f5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt @@ -221,7 +221,9 @@ class VoiceMessageHelper @Inject constructor( private fun onPlaybackTick(id: String) { if (mediaPlayer?.isPlaying.orFalse()) { val currentPosition = mediaPlayer?.currentPosition ?: 0 - playbackTracker.updateCurrentPlaybackTime(id, currentPosition) + val totalDuration = mediaPlayer?.duration ?: 0 + val percentage = currentPosition.toFloat() / totalDuration + playbackTracker.updateCurrentPlaybackTime(id, currentPosition, percentage) } else { playbackTracker.stopPlayback(id) stopPlaybackTicker() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt index 09284ea5fc..8adecaad6e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt @@ -27,7 +27,6 @@ import androidx.core.view.doOnLayout import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams -import com.visualizer.amplitude.AudioRecordView import im.vector.app.R import im.vector.app.core.extensions.setAttributeBackground import im.vector.app.core.extensions.setAttributeTintedBackground @@ -37,6 +36,8 @@ import im.vector.app.databinding.ViewVoiceMessageRecorderBinding import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.DraggingState import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.RecordingUiState import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker +import im.vector.app.features.themes.ThemeUtils +import im.vector.app.features.voice.AudioWaveformView class VoiceMessageViews( private val resources: Resources, @@ -284,7 +285,7 @@ class VoiceMessageViews( hideRecordingViews(RecordingUiState.Idle) views.voiceMessageMicButton.isVisible = true views.voiceMessageSendButton.isVisible = false - views.voicePlaybackWaveform.post { views.voicePlaybackWaveform.recreate() } + views.voicePlaybackWaveform.post { views.voicePlaybackWaveform.clear() } } fun renderPlaying(state: VoiceMessagePlaybackTracker.Listener.State.Playing) { @@ -292,11 +293,15 @@ class VoiceMessageViews( views.voicePlaybackControlButton.contentDescription = resources.getString(R.string.a11y_pause_voice_message) val formattedTimerText = DateUtils.formatElapsedTime((state.playbackTime / 1000).toLong()) views.voicePlaybackTime.text = formattedTimerText + val waveformColorIdle = ThemeUtils.getColor(views.voicePlaybackWaveform.context, R.attr.vctr_content_quaternary) + val waveformColorPlayed = ThemeUtils.getColor(views.voicePlaybackWaveform.context, R.attr.vctr_content_secondary) + views.voicePlaybackWaveform.updateColors(state.percentage, waveformColorPlayed, waveformColorIdle) } fun renderIdle() { views.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play) views.voicePlaybackControlButton.contentDescription = resources.getString(R.string.a11y_play_voice_message) + views.voicePlaybackWaveform.summarize() } fun renderToast(message: String) { @@ -327,8 +332,9 @@ class VoiceMessageViews( fun renderRecordingWaveform(amplitudeList: Array) { views.voicePlaybackWaveform.doOnLayout { waveFormView -> + val waveformColor = ThemeUtils.getColor(waveFormView.context, R.attr.vctr_content_secondary) amplitudeList.iterator().forEach { - (waveFormView as AudioRecordView).update(it) + (waveFormView as AudioWaveformView).add(AudioWaveformView.FFT(it.toFloat(), waveformColor)) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 0c836748c8..da97cf6984 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -73,6 +73,7 @@ import im.vector.app.features.location.toLocationData import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.VideoContentRenderer import im.vector.app.features.settings.VectorPreferences +import im.vector.app.features.voice.AudioWaveformView import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.span import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl @@ -688,8 +689,7 @@ class MessageItemFactory @Inject constructor( return this ?.filterNotNull() ?.map { - // Value comes from AudioRecordView.maxReportableAmp, and 1024 is the max value in the Matrix spec - it * 22760 / 1024 + it * AudioWaveformView.MAX_FFT / 1024 } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/VoiceMessagePlaybackTracker.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/VoiceMessagePlaybackTracker.kt index c6204bff1c..076c05b9c4 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/VoiceMessagePlaybackTracker.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/VoiceMessagePlaybackTracker.kt @@ -70,7 +70,8 @@ class VoiceMessagePlaybackTracker @Inject constructor() { fun startPlayback(id: String) { val currentPlaybackTime = getPlaybackTime(id) - val currentState = Listener.State.Playing(currentPlaybackTime) + val currentPercentage = getPercentage(id) + val currentState = Listener.State.Playing(currentPlaybackTime, currentPercentage) setState(id, currentState) // Pause any active playback states @@ -87,15 +88,16 @@ class VoiceMessagePlaybackTracker @Inject constructor() { fun pausePlayback(id: String) { val currentPlaybackTime = getPlaybackTime(id) - setState(id, Listener.State.Paused(currentPlaybackTime)) + val currentPercentage = getPercentage(id) + setState(id, Listener.State.Paused(currentPlaybackTime, currentPercentage)) } fun stopPlayback(id: String) { setState(id, Listener.State.Idle) } - fun updateCurrentPlaybackTime(id: String, time: Int) { - setState(id, Listener.State.Playing(time)) + fun updateCurrentPlaybackTime(id: String, time: Int, percentage: Float) { + setState(id, Listener.State.Playing(time, percentage)) } fun updateCurrentRecording(id: String, amplitudeList: List) { @@ -113,6 +115,15 @@ class VoiceMessagePlaybackTracker @Inject constructor() { } } + fun getPercentage(id: String): Float { + return when (val state = states[id]) { + is Listener.State.Playing -> state.percentage + is Listener.State.Paused -> state.percentage + /* Listener.State.Idle, */ + else -> 0f + } + } + fun clear() { listeners.forEach { it.value.onUpdate(Listener.State.Idle) @@ -131,8 +142,8 @@ class VoiceMessagePlaybackTracker @Inject constructor() { sealed class State { object Idle : State() - data class Playing(val playbackTime: Int) : State() - data class Paused(val playbackTime: Int) : State() + data class Playing(val playbackTime: Int, val percentage: Float) : State() + data class Paused(val playbackTime: Int, val percentage: Float) : State() data class Recording(val amplitudeList: List) : State() } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt index e9f728d976..82400a431d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt @@ -26,7 +26,6 @@ import android.widget.TextView import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import com.visualizer.amplitude.AudioRecordView import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder @@ -34,6 +33,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStat import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout import im.vector.app.features.themes.ThemeUtils +import im.vector.app.features.voice.AudioWaveformView @EpoxyModelClass(layout = R.layout.item_timeline_event_base) abstract class MessageVoiceItem : AbsMessageItem() { @@ -78,11 +78,15 @@ abstract class MessageVoiceItem : AbsMessageItem() { holder.voicePlaybackWaveform.setOnLongClickListener(attributes.itemLongClickListener) + val waveformColorIdle = ThemeUtils.getColor(holder.view.context, R.attr.vctr_content_quaternary) + val waveformColorPlayed = ThemeUtils.getColor(holder.view.context, R.attr.vctr_content_secondary) + holder.voicePlaybackWaveform.post { - holder.voicePlaybackWaveform.recreate() + holder.voicePlaybackWaveform.clear() waveform.forEach { amplitude -> - holder.voicePlaybackWaveform.update(amplitude) + holder.voicePlaybackWaveform.add(AudioWaveformView.FFT(amplitude.toFloat(), waveformColorIdle)) } + holder.voicePlaybackWaveform.summarize() } val backgroundTint = if (attributes.informationData.messageLayout is TimelineMessageLayout.Bubble) { @@ -93,33 +97,39 @@ abstract class MessageVoiceItem : AbsMessageItem() { holder.voicePlaybackLayout.backgroundTintList = ColorStateList.valueOf(backgroundTint) holder.voicePlaybackControlButton.setOnClickListener { playbackControlButtonClickListener?.invoke(it) } - voiceMessagePlaybackTracker.track(attributes.informationData.eventId, object : VoiceMessagePlaybackTracker.Listener { - override fun onUpdate(state: VoiceMessagePlaybackTracker.Listener.State) { - when (state) { - is VoiceMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder) - is VoiceMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state) - is VoiceMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state) + // Don't track and don't try to update UI before view is present + holder.view.post { + voiceMessagePlaybackTracker.track(attributes.informationData.eventId, object : VoiceMessagePlaybackTracker.Listener { + override fun onUpdate(state: VoiceMessagePlaybackTracker.Listener.State) { + when (state) { + is VoiceMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder, waveformColorIdle, waveformColorPlayed) + is VoiceMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state, waveformColorIdle, waveformColorPlayed) + is VoiceMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state, waveformColorIdle, waveformColorPlayed) + } } - } - }) + }) + } } - private fun renderIdleState(holder: Holder) { + private fun renderIdleState(holder: Holder, idleColor: Int, playedColor: Int) { holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play) holder.voicePlaybackControlButton.contentDescription = holder.view.context.getString(R.string.a11y_play_voice_message) holder.voicePlaybackTime.text = formatPlaybackTime(duration) + holder.voicePlaybackWaveform.updateColors(0f, playedColor, idleColor) } - private fun renderPlayingState(holder: Holder, state: VoiceMessagePlaybackTracker.Listener.State.Playing) { + private fun renderPlayingState(holder: Holder, state: VoiceMessagePlaybackTracker.Listener.State.Playing, idleColor: Int, playedColor: Int) { holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_pause) holder.voicePlaybackControlButton.contentDescription = holder.view.context.getString(R.string.a11y_pause_voice_message) holder.voicePlaybackTime.text = formatPlaybackTime(state.playbackTime) + holder.voicePlaybackWaveform.updateColors(state.percentage, playedColor, idleColor) } - private fun renderPausedState(holder: Holder, state: VoiceMessagePlaybackTracker.Listener.State.Paused) { + private fun renderPausedState(holder: Holder, state: VoiceMessagePlaybackTracker.Listener.State.Paused, idleColor: Int, playedColor: Int) { holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play) holder.voicePlaybackControlButton.contentDescription = holder.view.context.getString(R.string.a11y_play_voice_message) holder.voicePlaybackTime.text = formatPlaybackTime(state.playbackTime) + holder.voicePlaybackWaveform.updateColors(state.percentage, playedColor, idleColor) } private fun formatPlaybackTime(time: Int) = DateUtils.formatElapsedTime((time / 1000).toLong()) @@ -138,7 +148,7 @@ abstract class MessageVoiceItem : AbsMessageItem() { val voiceLayout by bind(R.id.voiceLayout) val voicePlaybackControlButton by bind(R.id.voicePlaybackControlButton) val voicePlaybackTime by bind(R.id.voicePlaybackTime) - val voicePlaybackWaveform by bind(R.id.voicePlaybackWaveform) + val voicePlaybackWaveform by bind(R.id.voicePlaybackWaveform) val progressLayout by bind(R.id.messageFileUploadProgressLayout) } diff --git a/vector/src/main/java/im/vector/app/features/voice/AudioWaveformView.kt b/vector/src/main/java/im/vector/app/features/voice/AudioWaveformView.kt new file mode 100644 index 0000000000..9ba7597e60 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/voice/AudioWaveformView.kt @@ -0,0 +1,199 @@ +/* + * 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.voice + +import android.content.Context +import android.content.res.Resources +import android.graphics.Canvas +import android.graphics.Paint +import android.util.AttributeSet +import android.view.View +import im.vector.app.R +import kotlin.math.max +import kotlin.random.Random + +class AudioWaveformView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + + private enum class Alignment(var value: Int) { + CENTER(0), + BOTTOM(1), + TOP(2) + } + + private enum class Flow(var value: Int) { + LTR(0), + RTL(1) + } + + data class FFT(val value: Float, var color: Int) + + private fun Int.dp() = this * Resources.getSystem().displayMetrics.density + + // Configuration fields + private var alignment = Alignment.CENTER + private var flow = Flow.LTR + private var verticalPadding = 4.dp() + private var horizontalPadding = 4.dp() + private var barWidth = 2.dp() + private var barSpace = 1.dp() + private var barMinHeight = 1.dp() + private var isBarRounded = true + + private val rawFftList = mutableListOf() + private var visibleBarHeights = mutableListOf() + + private val barPaint = Paint() + + init { + attrs?.let { + context + .theme + .obtainStyledAttributes( + attrs, + R.styleable.AudioWaveformView, + 0, + 0 + ) + .apply { + alignment = Alignment.values().find { it.value == getInt(R.styleable.AudioWaveformView_alignment, alignment.value) }!! + flow = Flow.values().find { it.value == getInt(R.styleable.AudioWaveformView_flow, alignment.value) }!! + verticalPadding = getDimension(R.styleable.AudioWaveformView_verticalPadding, verticalPadding) + horizontalPadding = getDimension(R.styleable.AudioWaveformView_horizontalPadding, horizontalPadding) + barWidth = getDimension(R.styleable.AudioWaveformView_barWidth, barWidth) + barSpace = getDimension(R.styleable.AudioWaveformView_barSpace, barSpace) + barMinHeight = getDimension(R.styleable.AudioWaveformView_barMinHeight, barMinHeight) + isBarRounded = getBoolean(R.styleable.AudioWaveformView_isBarRounded, isBarRounded) + setWillNotDraw(false) + barPaint.isAntiAlias = true + } + .apply { recycle() } + .also { + barPaint.strokeWidth = barWidth + barPaint.strokeCap = if (isBarRounded) Paint.Cap.ROUND else Paint.Cap.BUTT + } + } + } + + fun initialize(fftList: List) { + handleNewFftList(fftList) + invalidate() + } + + fun add(fft: FFT) { + handleNewFftList(listOf(fft)) + invalidate() + } + + fun summarize() { + if (rawFftList.isEmpty()) return + + val maxVisibleBarCount = getMaxVisibleBarCount() + val summarizedFftList = rawFftList.summarize(maxVisibleBarCount) + clear() + handleNewFftList(summarizedFftList) + invalidate() + } + + fun updateColors(limitPercentage: Float, colorBefore: Int, colorAfter: Int) { + val size = visibleBarHeights.size + val limitIndex = (size * limitPercentage).toInt() + visibleBarHeights.forEachIndexed { index, fft -> + fft.color = if (index < limitIndex) { + colorBefore + } else { + colorAfter + } + } + invalidate() + } + + fun clear() { + rawFftList.clear() + visibleBarHeights.clear() + } + + private fun List.summarize(target: Int): List { + val result = mutableListOf() + if (size <= target) { + result.addAll(this) + val missingItemCount = target - size + repeat(missingItemCount) { + val index = Random.nextInt(result.size) + result.add(index, result[index]) + } + } else { + val step = (size.toDouble() - 1) / (target - 1) + var index = 0.0 + while (index < size) { + result.add(get(index.toInt())) + index += step + } + } + return result + } + + private fun handleNewFftList(fftList: List) { + val maxVisibleBarCount = getMaxVisibleBarCount() + fftList.forEach { fft -> + rawFftList.add(fft) + val barHeight = max(fft.value / MAX_FFT * (height - verticalPadding * 2), barMinHeight) + visibleBarHeights.add(FFT(barHeight, fft.color)) + if (visibleBarHeights.size > maxVisibleBarCount) { + visibleBarHeights = visibleBarHeights.subList(visibleBarHeights.size - maxVisibleBarCount, visibleBarHeights.size) + } + } + } + + private fun getMaxVisibleBarCount() = ((width - horizontalPadding * 2) / (barWidth + barSpace)).toInt() + + private fun drawBars(canvas: Canvas) { + var currentX = horizontalPadding + visibleBarHeights.forEach { + barPaint.color = it.color + // TODO. Support flow + when (alignment) { + Alignment.BOTTOM -> { + val startY = height - verticalPadding + val stopY = startY - it.value + canvas.drawLine(currentX, startY, currentX, stopY, barPaint) + } + Alignment.CENTER -> { + val startY = (height - it.value) / 2 + val stopY = startY + it.value + canvas.drawLine(currentX, startY, currentX, stopY, barPaint) + } + Alignment.TOP -> { + val startY = verticalPadding + val stopY = startY + it.value + canvas.drawLine(currentX, startY, currentX, stopY, barPaint) + } + } + currentX += barWidth + barSpace + } + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + drawBars(canvas) + } + + companion object { + private const val MAX_FFT = 32760f + } +} diff --git a/vector/src/main/res/layout/item_timeline_event_voice_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_stub.xml index a180afbf8e..0fad714bd4 100644 --- a/vector/src/main/res/layout/item_timeline_event_voice_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_voice_stub.xml @@ -40,7 +40,7 @@ app:layout_constraintTop_toTopOf="@id/voicePlaybackControlButton" tools:text="0:23" /> - - + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 243a714586b13afeaa2432344687bddbf9c25658 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 2 Mar 2022 17:46:09 +0300 Subject: [PATCH 041/517] Remove 3rd party waveform library. --- library/ui-styles/build.gradle | 2 -- vector/build.gradle | 1 - vector/src/main/assets/open_source_licenses.html | 5 ----- .../java/im/vector/app/features/voice/AudioWaveformView.kt | 2 +- 4 files changed, 1 insertion(+), 9 deletions(-) diff --git a/library/ui-styles/build.gradle b/library/ui-styles/build.gradle index cee58414c7..0ac513b252 100644 --- a/library/ui-styles/build.gradle +++ b/library/ui-styles/build.gradle @@ -60,6 +60,4 @@ dependencies { implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12' // dialpad dimen implementation 'im.dlg:android-dialer:1.2.5' - // AudioRecordView attr - implementation 'com.github.Armen101:AudioRecordView:1.0.5' } \ No newline at end of file diff --git a/vector/build.gradle b/vector/build.gradle index c6a2636acf..d58118eb24 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -416,7 +416,6 @@ dependencies { implementation 'jp.wasabeef:glide-transformations:4.3.0' implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12' implementation 'com.github.hyuwah:DraggableView:1.0.0' - implementation 'com.github.Armen101:AudioRecordView:1.0.5' // Custom Tab implementation 'androidx.browser:browser:1.4.0' diff --git a/vector/src/main/assets/open_source_licenses.html b/vector/src/main/assets/open_source_licenses.html index 2c25606f57..0bead1f826 100755 --- a/vector/src/main/assets/open_source_licenses.html +++ b/vector/src/main/assets/open_source_licenses.html @@ -437,11 +437,6 @@ THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Copyright (c) 2017-present, dialog LLC <info@dlg.im> -
  • - Armen101 / AudioRecordView -
    - Copyright 2019 Armen Gevorgyan -
  •  Apache License
    diff --git a/vector/src/main/java/im/vector/app/features/voice/AudioWaveformView.kt b/vector/src/main/java/im/vector/app/features/voice/AudioWaveformView.kt
    index 9ba7597e60..768635b2f7 100644
    --- a/vector/src/main/java/im/vector/app/features/voice/AudioWaveformView.kt
    +++ b/vector/src/main/java/im/vector/app/features/voice/AudioWaveformView.kt
    @@ -194,6 +194,6 @@ class AudioWaveformView @JvmOverloads constructor(
         }
     
         companion object {
    -        private const val MAX_FFT = 32760f
    +        const val MAX_FFT = 32760
         }
     }
    
    From 99e5a8f2fa63169dc4913c7b0ee333734b67c46b Mon Sep 17 00:00:00 2001
    From: Adam Brown 
    Date: Wed, 2 Mar 2022 17:59:40 +0000
    Subject: [PATCH 042/517] FTUE - Choose a display name (#5211)
    
    * adding base choose name fragment with UI
    
    * add click handling for the display name actions
    
    * updating real account display name
    
    * setting the initial disabled state when the view is created
    
    * adding header padding which would have been a toolbar
    
    * exiting the flow on display name updated or skipped, the next PR will introduce the profile picture screen
    
    * updating view model state testing to take all emissions into account
    
    * adding tests around the onboarding view model
    - cases for the personalisation and display name actions
    
    * using colorSecondary instead of accent as per quality script rule
    
    * making use of viewevent delegating action for the back handling
    
    * using debounced clicks
    
    * consuming the back action when existing the display name fragment via viewmodel
    
    * making the keyboard imeDone update the display name
    ---
     .../im/vector/app/core/di/FragmentModule.kt   |   6 +
     .../features/onboarding/OnboardingAction.kt   |   3 +
     .../onboarding/OnboardingViewEvents.kt        |   2 +
     .../onboarding/OnboardingViewModel.kt         |  17 ++
     .../onboarding/OnboardingViewState.kt         |   4 +-
     .../FtueAuthChooseDisplayNameFragment.kt      |  83 ++++++++++
     .../onboarding/ftueauth/FtueAuthVariant.kt    |  16 +-
     vector/src/main/res/layout/activity_login.xml |   5 +-
     .../res/layout/fragment_ftue_display_name.xml | 152 ++++++++++++++++++
     vector/src/main/res/values/donottranslate.xml |   8 +
     .../quads/SharedSecureStorageViewModelTest.kt |  32 +++-
     .../onboarding/OnboardingViewModelTest.kt     | 116 +++++++++++++
     .../java/im/vector/app/test/Extensions.kt     |  15 +-
     .../app/test/fakes/FakeActiveSessionHolder.kt |  29 ++++
     .../app/test/fakes/FakeAnalyticsTracker.kt    |  22 +++
     .../test/fakes/FakeAuthenticationService.kt   |  22 +++
     .../FakeHomeServerConnectionConfigFactory.kt  |  25 +++
     .../fakes/FakeHomeServerHistoryService.kt     |  24 +++
     .../app/test/fakes/FakeProfileService.kt      |  33 ++++
     .../im/vector/app/test/fakes/FakeSession.kt   |   2 +
     .../app/test/fakes/FakeVectorFeatures.kt      |  27 ++++
     21 files changed, 628 insertions(+), 15 deletions(-)
     create mode 100644 vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthChooseDisplayNameFragment.kt
     create mode 100644 vector/src/main/res/layout/fragment_ftue_display_name.xml
     create mode 100644 vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt
     create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeActiveSessionHolder.kt
     create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeAnalyticsTracker.kt
     create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt
     create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeHomeServerConnectionConfigFactory.kt
     create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeHomeServerHistoryService.kt
     create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeProfileService.kt
     create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt
    
    diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt
    index 5eb735d22e..c62f837947 100644
    --- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt
    +++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt
    @@ -99,6 +99,7 @@ import im.vector.app.features.matrixto.MatrixToRoomSpaceFragment
     import im.vector.app.features.matrixto.MatrixToUserFragment
     import im.vector.app.features.onboarding.ftueauth.FtueAuthAccountCreatedFragment
     import im.vector.app.features.onboarding.ftueauth.FtueAuthCaptchaFragment
    +import im.vector.app.features.onboarding.ftueauth.FtueAuthChooseDisplayNameFragment
     import im.vector.app.features.onboarding.ftueauth.FtueAuthGenericTextInputFormFragment
     import im.vector.app.features.onboarding.ftueauth.FtueAuthLoginFragment
     import im.vector.app.features.onboarding.ftueauth.FtueAuthResetPasswordFragment
    @@ -479,6 +480,11 @@ interface FragmentModule {
         @FragmentKey(FtueAuthAccountCreatedFragment::class)
         fun bindFtueAuthAccountCreatedFragment(fragment: FtueAuthAccountCreatedFragment): Fragment
     
    +    @Binds
    +    @IntoMap
    +    @FragmentKey(FtueAuthChooseDisplayNameFragment::class)
    +    fun bindFtueAuthChooseDisplayNameFragment(fragment: FtueAuthChooseDisplayNameFragment): Fragment
    +
         @Binds
         @IntoMap
         @FragmentKey(UserListFragment::class)
    diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt
    index 3ddea5ca2e..b8fd2255f0 100644
    --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt
    +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt
    @@ -73,4 +73,7 @@ sealed class OnboardingAction : VectorViewModelAction {
         data class PostViewEvent(val viewEvent: OnboardingViewEvents) : OnboardingAction()
     
         data class UserAcceptCertificate(val fingerprint: Fingerprint) : OnboardingAction()
    +
    +    data class UpdateDisplayName(val displayName: String) : OnboardingAction()
    +    object UpdateDisplayNameSkipped : OnboardingAction()
     }
    diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt
    index 536c1d1875..159efd1b13 100644
    --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt
    +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt
    @@ -52,4 +52,6 @@ sealed class OnboardingViewEvents : VectorViewEvents {
         object OnAccountSignedIn : OnboardingViewEvents()
         object OnTakeMeHome : OnboardingViewEvents()
         object OnPersonalizeProfile : OnboardingViewEvents()
    +    object OnDisplayNameUpdated : OnboardingViewEvents()
    +    object OnDisplayNameSkipped : OnboardingViewEvents()
     }
    diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
    index c8f7ac5b3d..bc53641419 100644
    --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
    @@ -157,6 +157,8 @@ class OnboardingViewModel @AssistedInject constructor(
                 is OnboardingAction.UserAcceptCertificate      -> handleUserAcceptCertificate(action)
                 OnboardingAction.ClearHomeServerHistory        -> handleClearHomeServerHistory()
                 is OnboardingAction.PostViewEvent              -> _viewEvents.post(action.viewEvent)
    +            is OnboardingAction.UpdateDisplayName          -> updateDisplayName(action.displayName)
    +            OnboardingAction.UpdateDisplayNameSkipped      -> _viewEvents.post(OnboardingViewEvents.OnDisplayNameSkipped)
             }.exhaustive
         }
     
    @@ -892,6 +894,21 @@ class OnboardingViewModel @AssistedInject constructor(
         fun getFallbackUrl(forSignIn: Boolean, deviceId: String?): String? {
             return authenticationService.getFallbackUrl(forSignIn, deviceId)
         }
    +
    +    private fun updateDisplayName(displayName: String) {
    +        setState { copy(asyncDisplayName = Loading()) }
    +        viewModelScope.launch {
    +            val activeSession = activeSessionHolder.getActiveSession()
    +            try {
    +                activeSession.setDisplayName(activeSession.myUserId, displayName)
    +                setState { copy(asyncDisplayName = Success(Unit)) }
    +                _viewEvents.post(OnboardingViewEvents.OnDisplayNameUpdated)
    +            } catch (error: Throwable) {
    +                setState { copy(asyncDisplayName = Fail(error)) }
    +                _viewEvents.post(OnboardingViewEvents.Failure(error))
    +            }
    +        }
    +    }
     }
     
     private fun LoginMode.supportsSignModeScreen(): Boolean {
    diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt
    index 39c5094d30..61674de8ae 100644
    --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt
    +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt
    @@ -32,6 +32,7 @@ data class OnboardingViewState(
             val asyncResetPassword: Async = Uninitialized,
             val asyncResetMailConfirmed: Async = Uninitialized,
             val asyncRegistration: Async = Uninitialized,
    +        val asyncDisplayName: Async = Uninitialized,
     
             @PersistState
             val onboardingFlow: OnboardingFlow? = null,
    @@ -71,7 +72,8 @@ data class OnboardingViewState(
                     asyncHomeServerLoginFlowRequest is Loading ||
                     asyncResetPassword is Loading ||
                     asyncResetMailConfirmed is Loading ||
    -                asyncRegistration is Loading
    +                asyncRegistration is Loading ||
    +                asyncDisplayName is Loading
         }
     
         fun isAuthTaskCompleted(): Boolean {
    diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthChooseDisplayNameFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthChooseDisplayNameFragment.kt
    new file mode 100644
    index 0000000000..f1bf3d4a86
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthChooseDisplayNameFragment.kt
    @@ -0,0 +1,83 @@
    +/*
    + * Copyright (c) 2021 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.onboarding.ftueauth
    +
    +import android.os.Bundle
    +import android.text.Editable
    +import android.view.LayoutInflater
    +import android.view.View
    +import android.view.ViewGroup
    +import android.view.inputmethod.EditorInfo
    +import com.google.android.material.textfield.TextInputLayout
    +import im.vector.app.core.platform.SimpleTextWatcher
    +import im.vector.app.databinding.FragmentFtueDisplayNameBinding
    +import im.vector.app.features.onboarding.OnboardingAction
    +import im.vector.app.features.onboarding.OnboardingViewEvents
    +import javax.inject.Inject
    +
    +class FtueAuthChooseDisplayNameFragment @Inject constructor() : AbstractFtueAuthFragment() {
    +
    +    override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueDisplayNameBinding {
    +        return FragmentFtueDisplayNameBinding.inflate(inflater, container, false)
    +    }
    +
    +    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    +        super.onViewCreated(view, savedInstanceState)
    +        setupViews()
    +    }
    +
    +    private fun setupViews() {
    +        views.displayNameSubmit.isEnabled = views.displayNameInput.hasContentEmpty()
    +        views.displayNameInput.editText?.addTextChangedListener(object : SimpleTextWatcher() {
    +            override fun afterTextChanged(s: Editable) {
    +                val newContent = s.toString()
    +                views.displayNameSubmit.isEnabled = newContent.isNotEmpty()
    +            }
    +        })
    +        views.displayNameInput.editText?.setOnEditorActionListener { _, actionId, _ ->
    +            when (actionId) {
    +                EditorInfo.IME_ACTION_DONE -> {
    +                    updateDisplayName()
    +                    true
    +                }
    +                else                       -> false
    +            }
    +        }
    +
    +        views.displayNameSubmit.debouncedClicks {
    +            updateDisplayName()
    +        }
    +
    +        views.displayNameSkip.debouncedClicks { viewModel.handle(OnboardingAction.UpdateDisplayNameSkipped) }
    +    }
    +
    +    private fun updateDisplayName() {
    +        val newDisplayName = views.displayNameInput.editText?.text.toString()
    +        viewModel.handle(OnboardingAction.UpdateDisplayName(newDisplayName))
    +    }
    +
    +    override fun resetViewModel() {
    +        // Nothing to do
    +    }
    +
    +    override fun onBackPressed(toolbarButton: Boolean): Boolean {
    +        viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnTakeMeHome))
    +        return true
    +    }
    +}
    +
    +private fun TextInputLayout.hasContentEmpty() = !editText?.text.isNullOrEmpty()
    diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt
    index 0093cb20ea..4259e90cbe 100644
    --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt
    +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt
    @@ -231,8 +231,10 @@ class FtueAuthVariant(
                 }
                 OnboardingViewEvents.OnAccountCreated                              -> onAccountCreated()
                 OnboardingViewEvents.OnAccountSignedIn                             -> onAccountSignedIn()
    -            OnboardingViewEvents.OnPersonalizeProfile                          -> TODO()
    +            OnboardingViewEvents.OnPersonalizeProfile                          -> onPersonalizeProfile()
                 OnboardingViewEvents.OnTakeMeHome                                  -> navigateToHome(createdAccount = true)
    +            OnboardingViewEvents.OnDisplayNameUpdated                          -> onDisplayNameUpdated()
    +            OnboardingViewEvents.OnDisplayNameSkipped                          -> onDisplayNameUpdated()
             }.exhaustive
         }
     
    @@ -410,4 +412,16 @@ class FtueAuthVariant(
             activity.startActivity(intent)
             activity.finish()
         }
    +
    +    private fun onPersonalizeProfile() {
    +        activity.addFragmentToBackstack(views.loginFragmentContainer,
    +                FtueAuthChooseDisplayNameFragment::class.java,
    +                option = commonOption
    +        )
    +    }
    +
    +    private fun onDisplayNameUpdated() {
    +        // TODO go to the real profile picture fragment
    +        navigateToHome(createdAccount = true)
    +    }
     }
    diff --git a/vector/src/main/res/layout/activity_login.xml b/vector/src/main/res/layout/activity_login.xml
    index 5e0d237c57..7f5f4a96a1 100644
    --- a/vector/src/main/res/layout/activity_login.xml
    +++ b/vector/src/main/res/layout/activity_login.xml
    @@ -7,13 +7,16 @@
         android:layout_height="match_parent">
     
         
     
             
    +            android:layout_height="0dp"
    +            app:layout_constraintBottom_toBottomOf="parent"
    +            app:layout_constraintTop_toTopOf="parent" />
     
             
    +
    +
    +    
    +
    +        
    +
    +        
    +
    +        
    +
    +        
    +
    +        
    +
    +        
    +
    +        
    +
    +        
    +
    +            
    +
    +        
    +
    +        
    +
    +        
    +
    +