From d8f449388ca3e70bf3ba25e8486d7e04b12fb41e Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 20 Aug 2019 18:30:24 +0200 Subject: [PATCH 001/197] Read marker: start working on it (no UI) --- gradle.properties | 2 +- .../api/session/room/read/FullyReadContent.kt | 25 ++++++ .../api/session/room/read/ReadService.kt | 5 ++ .../session/room/timeline/TimelineEvent.kt | 3 +- .../database/helper/ChunkEntityHelper.kt | 3 +- .../database/mapper/RoomSummaryMapper.kt | 4 +- .../database/mapper/TimelineEventMapper.kt | 3 +- .../database/model/ReadMarkerEntity.kt | 35 +++++++++ .../database/model/SessionRealmModule.kt | 3 +- .../database/model/TimelineEventEntity.kt | 3 +- .../database/query/ReadMarkerEntityQueries.kt | 37 +++++++++ .../session/room/read/DefaultReadService.kt | 11 +++ .../session/room/read/SetReadMarkersTask.kt | 37 +++++---- .../session/room/timeline/DefaultTimeline.kt | 78 +++++++++++++------ .../session/sync/RoomFullyReadHandler.kt | 45 +++++++++++ .../internal/session/sync/RoomSyncHandler.kt | 29 +++++-- .../home/room/detail/RoomDetailViewModel.kt | 4 + .../detail/timeline/item/AbsMessageItem.kt | 2 + .../timeline/item/MessageInformationData.kt | 3 +- .../room/detail/timeline/item/NoticeItem.kt | 3 + .../util/MessageInformationDataFactory.kt | 5 +- .../res/layout/item_timeline_event_base.xml | 10 ++- .../item_timeline_event_base_noinfo.xml | 10 +++ 23 files changed, 304 insertions(+), 56 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/FullyReadContent.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadMarkerEntity.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadMarkerEntityQueries.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt diff --git a/gradle.properties b/gradle.properties index 2e2b110f15..35ca815df8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,7 +16,7 @@ org.gradle.jvmargs=-Xmx1536m vector.debugPrivateData=false -vector.httpLogLevel=NONE +vector.httpLogLevel=HEADERS # Note: to debug, you can put and uncomment the following lines in the file ~/.gradle/gradle.properties to override the value above #vector.debugPrivateData=true diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/FullyReadContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/FullyReadContent.kt new file mode 100644 index 0000000000..a73b9ef5b7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/FullyReadContent.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 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.matrix.android.api.session.room.read + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class FullyReadContent( + @Json(name = "event_id") val eventId: String +) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt index d97fc497f0..0ff0298b44 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt @@ -42,5 +42,10 @@ interface ReadService { fun isEventRead(eventId: String): Boolean + /** + * Returns a nullable read marker for the room. + */ + fun getReadMarkerLive(): LiveData + fun getEventReadReceiptsLive(eventId: String): LiveData> } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt index 36ca360e08..f250824b1f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt @@ -39,7 +39,8 @@ data class TimelineEvent( val isUniqueDisplayName: Boolean, val senderAvatar: String?, val annotations: EventAnnotationsSummary? = null, - val readReceipts: List = emptyList() + val readReceipts: List = emptyList(), + val hasReadMarker: Boolean = false ) { val metadata = HashMap() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt index 69065f5171..3824fed779 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt @@ -23,6 +23,7 @@ import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.mapper.toEntity import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity +import im.vector.matrix.android.internal.database.model.ReadMarkerEntity import im.vector.matrix.android.internal.database.model.ReadReceiptEntity import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity @@ -157,7 +158,6 @@ internal fun ChunkEntity.add(roomId: String, } } - val eventEntity = TimelineEventEntity(localId).also { it.root = event.toEntity(roomId).apply { this.stateIndex = currentStateIndex @@ -169,6 +169,7 @@ internal fun ChunkEntity.add(roomId: String, it.roomId = roomId it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() it.readReceipts = readReceiptsSummaryEntity + it.readMarker = ReadMarkerEntity.where(realm, roomId = roomId, eventId = eventId).findFirst() } val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size timelineEvents.add(position, eventEntity) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt index 95d4d8bc62..03061c6edd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt @@ -26,8 +26,8 @@ import java.util.* import javax.inject.Inject internal class RoomSummaryMapper @Inject constructor( - val cryptoService: CryptoService, - val timelineEventMapper: TimelineEventMapper + private val cryptoService: CryptoService, + private val timelineEventMapper: TimelineEventMapper ) { fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt index fe98ebfb5b..0e9f13155e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt @@ -45,7 +45,8 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS senderAvatar = timelineEventEntity.senderAvatar, readReceipts = readReceipts?.sortedByDescending { it.originServerTs - } ?: emptyList() + } ?: emptyList(), + hasReadMarker = timelineEventEntity.readMarker?.eventId?.isEmpty() == false ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadMarkerEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadMarkerEntity.kt new file mode 100644 index 0000000000..d67308b283 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadMarkerEntity.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019 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.matrix.android.internal.database.model + +import io.realm.RealmObject +import io.realm.RealmResults +import io.realm.annotations.LinkingObjects +import io.realm.annotations.PrimaryKey + +internal open class ReadMarkerEntity( + @PrimaryKey + var roomId: String = "", + var eventId: String = "" +) : RealmObject() { + + @LinkingObjects("readMarker") + val timelineEvent: RealmResults? = null + + companion object + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt index 1d27bf07ee..0aa6ac1dd6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt @@ -43,6 +43,7 @@ import io.realm.annotations.RealmModule PushConditionEntity::class, PusherEntity::class, PusherDataEntity::class, - ReadReceiptsSummaryEntity::class + ReadReceiptsSummaryEntity::class, + ReadMarkerEntity::class ]) internal class SessionRealmModule diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt index 429b2291f6..e727ce40c7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt @@ -31,7 +31,8 @@ internal open class TimelineEventEntity(var localId: Long = 0, var isUniqueDisplayName: Boolean = false, var senderAvatar: String? = null, var senderMembershipEvent: EventEntity? = null, - var readReceipts: ReadReceiptsSummaryEntity? = null + var readReceipts: ReadReceiptsSummaryEntity? = null, + var readMarker: ReadMarkerEntity? = null ) : RealmObject() { @LinkingObjects("timelineEvents") diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadMarkerEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadMarkerEntityQueries.kt new file mode 100644 index 0000000000..061634a9da --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadMarkerEntityQueries.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 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.matrix.android.internal.database.query + +import im.vector.matrix.android.internal.database.model.ReadMarkerEntity +import im.vector.matrix.android.internal.database.model.ReadMarkerEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun ReadMarkerEntity.Companion.where(realm: Realm, roomId: String, eventId: String? = null): RealmQuery { + val query = realm.where() + .equalTo(ReadMarkerEntityFields.ROOM_ID, roomId) + if (eventId != null) { + query.equalTo(ReadMarkerEntityFields.EVENT_ID, eventId) + } + return query +} + +internal fun ReadMarkerEntity.Companion.getOrCreate(realm: Realm, roomId: String): ReadMarkerEntity { + return where(realm, roomId).findFirst() + ?: realm.createObject(ReadMarkerEntity::class.java, roomId) +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt index 505b958911..3709521cc3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt @@ -28,8 +28,10 @@ import im.vector.matrix.android.api.session.room.read.ReadService import im.vector.matrix.android.internal.database.RealmLiveData import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper import im.vector.matrix.android.internal.database.model.ChunkEntity +import im.vector.matrix.android.internal.database.model.ReadMarkerEntity import im.vector.matrix.android.internal.database.model.ReadReceiptEntity import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity +import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.query.find import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom import im.vector.matrix.android.internal.database.query.where @@ -93,6 +95,15 @@ internal class DefaultReadService @AssistedInject constructor(@Assisted private return isEventRead } + override fun getReadMarkerLive(): LiveData { + val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm -> + ReadMarkerEntity.where(realm, roomId) + } + return Transformations.map(liveRealmData) { results -> + results.firstOrNull()?.eventId + } + } + override fun getEventReadReceiptsLive(eventId: String): LiveData> { val liveEntity = RealmLiveData(monarchy.realmConfiguration) { realm -> ReadReceiptsSummaryEntity.where(realm, eventId) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt index 41c9cca507..af05510c8a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.session.room.read import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.database.model.ChunkEntity +import im.vector.matrix.android.internal.database.model.ReadMarkerEntity import im.vector.matrix.android.internal.database.model.ReadReceiptEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity @@ -57,6 +58,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI val fullyReadEventId: String? val readReceiptEventId: String? + Timber.v("Execute set read marker with params: $params") if (params.markAllAsRead) { val latestSyncedEventId = Realm.getInstance(monarchy.realmConfiguration).use { realm -> TimelineEventEntity.latestEvent(realm, roomId = params.roomId, includesSending = false)?.eventId @@ -68,7 +70,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI readReceiptEventId = params.readReceiptEventId } - if (fullyReadEventId != null) { + if (fullyReadEventId != null && isReadMarkerMoreRecent(params.roomId, fullyReadEventId)) { if (LocalEchoEventFactory.isLocalEchoId(fullyReadEventId)) { Timber.w("Can't set read marker for local event ${params.fullyReadEventId}") } else { @@ -76,7 +78,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI } } if (readReceiptEventId != null - && !isEventRead(params.roomId, readReceiptEventId)) { + && !isEventRead(params.roomId, readReceiptEventId)) { if (LocalEchoEventFactory.isLocalEchoId(readReceiptEventId)) { Timber.w("Can't set read receipt for local event ${params.fullyReadEventId}") @@ -93,12 +95,23 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI } } + private fun isReadMarkerMoreRecent(roomId: String, fullyReadEventId: String): Boolean { + return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + val readMarkerEntity = ReadMarkerEntity.where(realm, roomId = roomId).findFirst() + val readMarkerEvent = readMarkerEntity?.timelineEvent?.firstOrNull() + val eventToCheck = TimelineEventEntity.where(realm, eventId = fullyReadEventId).findFirst() + val readReceiptIndex = readMarkerEvent?.root?.displayIndex ?: Int.MAX_VALUE + val eventToCheckIndex = eventToCheck?.root?.displayIndex ?: Int.MIN_VALUE + eventToCheckIndex > readReceiptIndex + } + } + private fun updateNotificationCountIfNecessary(roomId: String, eventId: String) { monarchy.writeAsync { realm -> val isLatestReceived = TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId == eventId if (isLatestReceived) { val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() - ?: return@writeAsync + ?: return@writeAsync roomSummary.notificationCount = 0 roomSummary.highlightCount = 0 } @@ -106,19 +119,17 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI } private fun isEventRead(roomId: String, eventId: String): Boolean { - var isEventRead = false - monarchy.doWithRealm { - val readReceipt = ReadReceiptEntity.where(it, roomId, credentials.userId).findFirst() - ?: return@doWithRealm - val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(it, roomId) - ?: return@doWithRealm + return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + val readReceipt = ReadReceiptEntity.where(realm, roomId, credentials.userId).findFirst() + ?: return false + val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) + ?: return false val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex - ?: Int.MIN_VALUE + ?: Int.MIN_VALUE val eventToCheckIndex = liveChunk.timelineEvents.find(eventId)?.root?.displayIndex - ?: Int.MAX_VALUE - isEventRead = eventToCheckIndex <= readReceiptIndex + ?: Int.MAX_VALUE + eventToCheckIndex <= readReceiptIndex } - return isEventRead } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index 03f5da6e6f..26983a82c6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -32,6 +32,7 @@ import im.vector.matrix.android.internal.database.model.ChunkEntityFields import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntityFields +import im.vector.matrix.android.internal.database.model.ReadMarkerEntity import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields @@ -47,10 +48,12 @@ import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.Debouncer import im.vector.matrix.android.internal.util.createBackgroundHandler import im.vector.matrix.android.internal.util.createUIHandler +import io.realm.ObjectChangeSet import io.realm.OrderedCollectionChangeSet import io.realm.OrderedRealmCollectionChangeListener import io.realm.Realm import io.realm.RealmConfiguration +import io.realm.RealmObjectChangeListener import io.realm.RealmQuery import io.realm.RealmResults import io.realm.Sort @@ -101,6 +104,7 @@ internal class DefaultTimeline( private lateinit var eventRelations: RealmResults private var roomEntity: RoomEntity? = null + private var readMarkerEntity: ReadMarkerEntity? = null private var prevDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN private var nextDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN @@ -149,13 +153,9 @@ internal class DefaultTimeline( changeSet.changes.forEach { index -> val eventEntity = results[index] eventEntity?.eventId?.let { eventId -> - builtEventsIdMap[eventId]?.let { builtIndex -> - //Update an existing event - builtEvents[builtIndex]?.let { te -> - builtEvents[builtIndex] = buildTimelineEvent(eventEntity) - hasChanged = true - } - } + hasChanged = rebuildEvent(eventId) { + buildTimelineEvent(eventEntity) + } || hasChanged } } if (hasChanged) postSnapshot() @@ -163,27 +163,44 @@ internal class DefaultTimeline( } private val relationsListener = OrderedRealmCollectionChangeListener> { collection, changeSet -> - var hasChange = false (changeSet.insertions + changeSet.changes).forEach { val eventRelations = collection[it] if (eventRelations != null) { - builtEventsIdMap[eventRelations.eventId]?.let { builtIndex -> - //Update the relation of existing event - builtEvents[builtIndex]?.let { te -> - builtEvents[builtIndex] = te.copy(annotations = eventRelations.asDomain()) - hasChange = true + hasChange = rebuildEvent(eventRelations.eventId) { te -> + te.copy(annotations = eventRelations.asDomain()) + } || hasChange + } + } + if (hasChange) postSnapshot() + } + + private val readMarkerListener = RealmObjectChangeListener { readMarkerEntity: ReadMarkerEntity, _: ObjectChangeSet? -> + val isEventHidden = liveEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, readMarkerEntity.eventId).findFirst() == null + var hasChange = false + if (isEventHidden) { + val hiddenEvent = readMarkerEntity.timelineEvent?.firstOrNull() ?: return@RealmObjectChangeListener + val displayIndex = hiddenEvent.root?.displayIndex + if (displayIndex != null) { + // Then we are looking for the first displayable event after the hidden one + val firstDisplayedEvent = liveEvents.where() + .lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex) + .findFirst() + + // If we find one, we should rebuild this one with marker + if (firstDisplayedEvent != null) { + hasChange = rebuildEvent(firstDisplayedEvent.eventId) { + it.copy(hasReadMarker = true) } } } } - if (hasChange) - postSnapshot() + if (hasChange) postSnapshot() } - // Public methods ****************************************************************************** +// Public methods ****************************************************************************** override fun paginate(direction: Timeline.Direction, count: Int) { BACKGROUND_HANDLER.post { @@ -237,6 +254,10 @@ internal class DefaultTimeline( .findAllAsync() .also { it.addChangeListener(relationsListener) } + readMarkerEntity = ReadMarkerEntity.where(realm, roomId = roomId) + .findFirstAsync() + .also { it.addChangeListener(readMarkerListener) } + if (settings.buildReadReceipts) { hiddenReadReceipts.start(realm, liveEvents, this) } @@ -255,6 +276,7 @@ internal class DefaultTimeline( roomEntity?.sendingTimelineEvents?.removeAllChangeListeners() eventRelations.removeAllChangeListeners() liveEvents.removeAllChangeListeners() + readMarkerEntity?.removeAllChangeListeners() if (settings.buildReadReceipts) { hiddenReadReceipts.dispose() } @@ -272,20 +294,26 @@ internal class DefaultTimeline( // TimelineHiddenReadReceipts.Delegate override fun rebuildEvent(eventId: String, readReceipts: List): Boolean { - return builtEventsIdMap[eventId]?.let { builtIndex -> - //Update the relation of existing event - builtEvents[builtIndex]?.let { te -> - builtEvents[builtIndex] = te.copy(readReceipts = readReceipts) - true - } - } ?: false + return rebuildEvent(eventId) { te -> + te.copy(readReceipts = readReceipts) + } } override fun onReadReceiptsUpdated() { postSnapshot() } -// Private methods ***************************************************************************** + // Private methods ***************************************************************************** + + private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean { + return builtEventsIdMap[eventId]?.let { builtIndex -> + //Update the relation of existing event + builtEvents[builtIndex]?.let { te -> + builtEvents[builtIndex] = builder(te) + true + } + } ?: false + } private fun hasMoreInCache(direction: Timeline.Direction): Boolean { return Realm.getInstance(realmConfiguration).use { localRealm -> @@ -571,7 +599,7 @@ internal class DefaultTimeline( debouncer.debounce("post_snapshot", runnable, 50) } - // Extension methods *************************************************************************** +// Extension methods *************************************************************************** private fun Timeline.Direction.toPaginationDirection(): PaginationDirection { return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt new file mode 100644 index 0000000000..f142ca069e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019 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.matrix.android.internal.session.sync + +import im.vector.matrix.android.api.session.room.read.FullyReadContent +import im.vector.matrix.android.internal.database.model.ReadMarkerEntity +import im.vector.matrix.android.internal.database.model.TimelineEventEntity +import im.vector.matrix.android.internal.database.query.getOrCreate +import im.vector.matrix.android.internal.database.query.where +import io.realm.Realm +import timber.log.Timber +import javax.inject.Inject + +internal class RoomFullyReadHandler @Inject constructor() { + + fun handle(realm: Realm, roomId: String, content: FullyReadContent?) { + if (content == null) { + return + } + Timber.v("Handle for roomId: $roomId eventId: ${content.eventId}") + val readMarkerEntity = ReadMarkerEntity.getOrCreate(realm, roomId).apply { + eventId = content.eventId + } + // Remove the old marker if any + readMarkerEntity.timelineEvent?.firstOrNull()?.readMarker = null + // Attach to timelineEvent if known + val timelineEventEntity = TimelineEventEntity.where(realm, eventId = content.eventId).findFirst() + timelineEventEntity?.readMarker = readMarkerEntity + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt index 74b56e774c..0b4897e089 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt @@ -23,8 +23,13 @@ import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.tag.RoomTagContent +import im.vector.matrix.android.api.session.room.read.FullyReadContent import im.vector.matrix.android.internal.crypto.CryptoManager -import im.vector.matrix.android.internal.database.helper.* +import im.vector.matrix.android.internal.database.helper.add +import im.vector.matrix.android.internal.database.helper.addOrUpdate +import im.vector.matrix.android.internal.database.helper.addStateEvent +import im.vector.matrix.android.internal.database.helper.lastStateIndex +import im.vector.matrix.android.internal.database.helper.updateSenderDataFor import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.model.RoomEntity @@ -37,7 +42,11 @@ import im.vector.matrix.android.internal.session.notification.DefaultPushRuleSer import im.vector.matrix.android.internal.session.notification.ProcessEventForPushTask import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection -import im.vector.matrix.android.internal.session.sync.model.* +import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync +import im.vector.matrix.android.internal.session.sync.model.RoomSync +import im.vector.matrix.android.internal.session.sync.model.RoomSyncAccountData +import im.vector.matrix.android.internal.session.sync.model.RoomSyncEphemeral +import im.vector.matrix.android.internal.session.sync.model.RoomsSyncResponse import im.vector.matrix.android.internal.session.user.UserEntityFactory import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith @@ -50,6 +59,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch private val readReceiptHandler: ReadReceiptHandler, private val roomSummaryUpdater: RoomSummaryUpdater, private val roomTagHandler: RoomTagHandler, + private val roomFullyReadHandler: RoomFullyReadHandler, private val cryptoManager: CryptoManager, private val tokenStore: SyncTokenStore, private val pushRuleService: DefaultPushRuleService, @@ -247,11 +257,16 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch } private fun handleRoomAccountDataEvents(realm: Realm, roomId: String, accountData: RoomSyncAccountData) { - accountData.events - .asSequence() - .filter { it.getClearType() == EventType.TAG } - .map { it.content.toModel() } - .forEach { roomTagHandler.handle(realm, roomId, it) } + for (event in accountData.events) { + val eventType = event.getClearType() + if (eventType == EventType.TAG) { + val content = event.getClearContent().toModel() + roomTagHandler.handle(realm, roomId, content) + } else if (eventType == EventType.FULLY_READ) { + val content = event.getClearContent().toModel() + roomFullyReadHandler.handle(realm, roomId, content) + } + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 1cd8cc4a41..607f999e30 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -626,9 +626,13 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro .buffer(1, TimeUnit.SECONDS) .filter { it.isNotEmpty() } .subscribeBy(onNext = { actions -> + val readMarkerVisible = actions.find { it.event.hasReadMarker } != null val mostRecentEvent = actions.maxBy { it.event.displayIndex } mostRecentEvent?.event?.root?.eventId?.let { eventId -> room.setReadReceipt(eventId, callback = object : MatrixCallback {}) + if (readMarkerVisible) { + room.setReadMarker(eventId, callback = object : MatrixCallback {}) + } } }) .disposeOnClear() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt index a394f47124..570daf669c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -129,6 +129,7 @@ abstract class AbsMessageItem : BaseEventItem() { holder.memberNameView.setOnLongClickListener(null) } + holder.readMarkerView.isVisible = informationData.displayReadMarker holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener) if (!shouldShowReactionAtBottom() || informationData.orderedReactionList.isNullOrEmpty()) { @@ -182,6 +183,7 @@ abstract class AbsMessageItem : BaseEventItem() { val memberNameView by bind(R.id.messageMemberNameView) val timeView by bind(R.id.messageTimeView) val readReceiptsView by bind(R.id.readReceiptsView) + val readMarkerView by bind(R.id.readMarkerView) var reactionWrapper: ViewGroup? = null var reactionFlowHelper: Flow? = null } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt index d46b2a8db3..041b6dbddd 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt @@ -33,7 +33,8 @@ data class MessageInformationData( val orderedReactionList: List? = null, val hasBeenEdited: Boolean = false, val hasPendingEdits: Boolean = false, - val readReceipts: List = emptyList() + val readReceipts: List = emptyList(), + val displayReadMarker: Boolean = false ) : Parcelable diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt index 51a7b0ce38..dd42dc7b66 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt @@ -19,6 +19,7 @@ package im.vector.riotx.features.home.room.detail.timeline.item import android.view.View import android.widget.ImageView import android.widget.TextView +import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R @@ -65,6 +66,7 @@ abstract class NoticeItem : BaseEventItem() { ) holder.view.setOnLongClickListener(longClickListener) holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener) + holder.readMarkerView.isVisible = informationData.displayReadMarker } override fun getViewType() = STUB_ID @@ -73,6 +75,7 @@ abstract class NoticeItem : BaseEventItem() { val avatarImageView by bind(R.id.itemNoticeAvatarView) val noticeTextView by bind(R.id.itemNoticeTextView) val readReceiptsView by bind(R.id.readReceiptsView) + val readMarkerView by bind(R.id.readMarkerView) } companion object { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/util/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/util/MessageInformationDataFactory.kt index a00dd3fa9f..71a7549b46 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/util/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/util/MessageInformationDataFactory.kt @@ -62,6 +62,8 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId ?: "")) } + val displayReadMarker = event.hasReadMarker && event.readReceipts.find { it.user.userId == session.myUserId } == null + return MessageInformationData( eventId = eventId, senderId = event.root.senderId ?: "", @@ -85,7 +87,8 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses .map { ReadReceiptData(it.user.userId, it.user.avatarUrl, it.user.displayName, it.originServerTs) } - .toList() + .toList(), + displayReadMarker = displayReadMarker ) } } \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml index 2f0be78f38..ea4cfd5d4a 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -122,7 +122,6 @@ - + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml index 7726839902..ad6999c5ee 100644 --- a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml +++ b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml @@ -61,5 +61,15 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" /> + + \ No newline at end of file From 51a4c936761b5f5194832859bd3e4f9e74a4c72c Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 20 Aug 2019 19:12:22 +0200 Subject: [PATCH 002/197] Read markers: continue working on ui --- .../api/session/room/model/RoomSummary.kt | 9 +- .../api/session/room/timeline/Timeline.kt | 18 +- .../database/mapper/RoomSummaryMapper.kt | 15 +- .../database/model/RoomSummaryEntity.kt | 3 +- .../query/ReadReceiptEntityQueries.kt | 7 + .../query/RoomSummaryEntityQueries.kt | 6 + .../matrix/android/internal/di/MatrixScope.kt | 2 +- .../android/internal/session/SessionModule.kt | 2 - .../android/internal/session/SessionScope.kt | 2 +- .../session/room/read/SetReadMarkersTask.kt | 23 +- .../session/room/timeline/DefaultTimeline.kt | 137 ++++++----- .../room/timeline/DefaultTimelineService.kt | 3 +- .../room/timeline/TimelineHiddenReadMarker.kt | 96 ++++++++ .../session/sync/RoomFullyReadHandler.kt | 6 + .../core/ui/views/JumpToReadMarkerView.kt | 75 ++++++ .../riotx/core/ui/views/ReadMarkerView.kt | 86 +++++++ .../home/room/detail/DownloadFileState.kt | 25 ++ .../home/room/detail/RoomDetailActions.kt | 11 +- .../home/room/detail/RoomDetailFragment.kt | 112 +++++++-- .../home/room/detail/RoomDetailViewModel.kt | 178 +++++++------- .../home/room/detail/RoomDetailViewState.kt | 4 +- .../ScrollOnHighlightedEventCallback.kt | 2 +- .../timeline/TimelineEventController.kt | 69 ++++-- .../timeline/factory/EncryptedItemFactory.kt | 22 +- .../timeline/factory/EncryptionItemFactory.kt | 71 ------ .../timeline/factory/MessageItemFactory.kt | 223 +++++------------- .../timeline/factory/NoticeItemFactory.kt | 5 +- .../timeline/factory/TimelineItemFactory.kt | 11 +- .../timeline/format/NoticeEventFormatter.kt | 14 +- .../MessageInformationDataFactory.kt | 33 +-- .../helper/MessageItemAttributesFactory.kt | 58 +++++ .../helper/TimelineDisplayableEvents.kt | 27 +-- ...lineEventVisibilityStateChangedListener.kt | 9 +- .../detail/timeline/item/AbsMessageItem.kt | 108 ++++----- .../detail/timeline/item/BaseEventItem.kt | 3 + .../timeline/item/MessageImageVideoItem.kt | 12 +- .../detail/timeline/item/MessageTextItem.kt | 4 +- .../room/detail/timeline/item/NoticeItem.kt | 19 +- .../src/main/res/anim/unread_marker_anim.xml | 2 - .../main/res/layout/fragment_room_detail.xml | 82 ++++--- .../res/layout/item_timeline_event_base.xml | 17 +- .../item_timeline_event_base_noinfo.xml | 17 +- ...item_timeline_event_merged_header_stub.xml | 2 +- .../res/layout/view_jump_to_read_marker.xml | 41 ++++ .../src/main/res/layout/view_read_marker.xml | 58 +++++ 45 files changed, 1073 insertions(+), 656 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt create mode 100644 vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt create mode 100644 vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/DownloadFileState.kt delete mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt rename vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/{util => helper}/MessageInformationDataFactory.kt (82%) create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt create mode 100644 vector/src/main/res/layout/view_jump_to_read_marker.xml create mode 100644 vector/src/main/res/layout/view_read_marker.xml diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt index aae72dd41f..36aab8db29 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt @@ -35,9 +35,14 @@ data class RoomSummary( val highlightCount: Int = 0, val tags: List = emptyList(), val membership: Membership = Membership.NONE, - val versioningState: VersioningState = VersioningState.NONE + val versioningState: VersioningState = VersioningState.NONE, + val readMarkerId: String? = null ) { val isVersioned: Boolean get() = versioningState != VersioningState.NONE -} \ No newline at end of file + + val hasNewMessages: Boolean + get() = notificationCount != 0 +} + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt index 314c9f61b8..3f90d3cd13 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt @@ -32,6 +32,8 @@ interface Timeline { var listener: Listener? + val isLive: Boolean + /** * This should be called before any other method after creating the timeline. It ensures the underlying database is open */ @@ -42,6 +44,10 @@ interface Timeline { */ fun dispose() + + fun restartWithEventId(eventId: String) + + /** * Check if the timeline can be enriched by paginating. * @param the direction to check in @@ -49,6 +55,7 @@ interface Timeline { */ fun hasMoreToLoad(direction: Direction): Boolean + /** * This is the main method to enrich the timeline with new data. * It will call the onUpdated method from [Listener] when the data will be processed. @@ -56,9 +63,16 @@ interface Timeline { */ fun paginate(direction: Direction, count: Int) - fun pendingEventCount() : Int + fun pendingEventCount(): Int + + fun failedToDeliverEventCount(): Int + + fun getIndexOfEvent(eventId: String?): Int? + + fun getTimelineEventAtIndex(index: Int): TimelineEvent? + + fun getTimelineEventWithId(eventId: String?): TimelineEvent? - fun failedToDeliverEventCount() : Int interface Listener { /** diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt index 03061c6edd..cf829b44bc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt @@ -43,12 +43,12 @@ internal class RoomSummaryMapper @Inject constructor( //for now decrypt sync try { val result = cryptoService.decryptEvent(latestEvent.root, latestEvent.root.roomId + UUID.randomUUID().toString()) - latestEvent.root.mxDecryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain - ) + latestEvent.root.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) } catch (e: MXCryptoError) { } @@ -65,7 +65,8 @@ internal class RoomSummaryMapper @Inject constructor( notificationCount = roomSummaryEntity.notificationCount, tags = tags, membership = roomSummaryEntity.membership, - versioningState = roomSummaryEntity.versioningState + versioningState = roomSummaryEntity.versioningState, + readMarkerId = roomSummaryEntity.readMarkerId ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt index 6fe81f4cdd..dde01c3740 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt @@ -35,7 +35,8 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "", var otherMemberIds: RealmList = RealmList(), var notificationCount: Int = 0, var highlightCount: Int = 0, - var tags: RealmList = RealmList() + var tags: RealmList = RealmList(), + var readMarkerId: String? = null ) : RealmObject() { private var membershipStr: String = Membership.NONE.name diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadReceiptEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadReceiptEntityQueries.kt index acac419946..330d76fd15 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadReceiptEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadReceiptEntityQueries.kt @@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.database.model.ReadReceiptEntity import im.vector.matrix.android.internal.database.model.ReadReceiptEntityFields import io.realm.Realm import io.realm.RealmQuery +import io.realm.RealmResults import io.realm.kotlin.where internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, userId: String): RealmQuery { @@ -28,6 +29,12 @@ internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, use .equalTo(ReadReceiptEntityFields.USER_ID, userId) } +internal fun ReadReceiptEntity.Companion.whereUserId(realm: Realm, userId: String): RealmQuery { + return realm.where() + .equalTo(ReadReceiptEntityFields.USER_ID, userId) +} + + internal fun ReadReceiptEntity.Companion.createUnmanaged(roomId: String, eventId: String, userId: String, originServerTs: Double): ReadReceiptEntity { return ReadReceiptEntity().apply { this.primaryKey = "${roomId}_$userId" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomSummaryEntityQueries.kt index f2c260421f..bfa3f2c51c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomSummaryEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomSummaryEntityQueries.kt @@ -31,6 +31,12 @@ internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = n return query } +internal fun RoomSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: String): RoomSummaryEntity { + return where(realm, roomId).findFirst() + ?: realm.createObject(RoomSummaryEntity::class.java, roomId) +} + + internal fun RoomSummaryEntity.Companion.getDirectRooms(realm: Realm): RealmResults { return RoomSummaryEntity.where(realm) .equalTo(RoomSummaryEntityFields.IS_DIRECT, true) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixScope.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixScope.kt index 9c9327df55..032b645f59 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixScope.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixScope.kt @@ -21,4 +21,4 @@ import javax.inject.Scope @Scope @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) -annotation class MatrixScope \ No newline at end of file +internal annotation class MatrixScope \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index ab44a4aa93..106a80ce9f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -36,9 +36,7 @@ import im.vector.matrix.android.internal.di.Unauthenticated import im.vector.matrix.android.internal.network.AccessTokenInterceptor import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater -import im.vector.matrix.android.internal.session.room.DefaultRoomFactory import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater -import im.vector.matrix.android.internal.session.room.RoomFactory import im.vector.matrix.android.internal.session.room.create.RoomCreateEventLiveObserver import im.vector.matrix.android.internal.session.room.prune.EventsPruner import im.vector.matrix.android.internal.session.room.tombstone.RoomTombstoneEventLiveObserver diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionScope.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionScope.kt index 37753fdfcc..964165e00d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionScope.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionScope.kt @@ -21,4 +21,4 @@ import javax.inject.Scope @Scope @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) -annotation class SessionScope \ No newline at end of file +internal annotation class SessionScope \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt index af05510c8a..9652faae81 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.session.room.read import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.session.room.read.FullyReadContent import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ReadMarkerEntity import im.vector.matrix.android.internal.database.model.ReadReceiptEntity @@ -25,13 +26,17 @@ import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.query.find import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom +import im.vector.matrix.android.internal.database.query.getOrCreate import im.vector.matrix.android.internal.database.query.latestEvent import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory +import im.vector.matrix.android.internal.session.sync.RoomFullyReadHandler import im.vector.matrix.android.internal.task.Task +import im.vector.matrix.android.internal.util.awaitTransaction import io.realm.Realm +import io.realm.RealmConfiguration import timber.log.Timber import javax.inject.Inject @@ -50,7 +55,8 @@ private const val READ_RECEIPT = "m.read" internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI: RoomAPI, private val credentials: Credentials, - private val monarchy: Monarchy + private val monarchy: Monarchy, + private val roomFullyReadHandler: RoomFullyReadHandler ) : SetReadMarkersTask { override suspend fun execute(params: SetReadMarkersTask.Params) { @@ -74,12 +80,12 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI if (LocalEchoEventFactory.isLocalEchoId(fullyReadEventId)) { Timber.w("Can't set read marker for local event ${params.fullyReadEventId}") } else { + updateReadMarker(params.roomId, fullyReadEventId) markers[READ_MARKER] = fullyReadEventId } } if (readReceiptEventId != null && !isEventRead(params.roomId, readReceiptEventId)) { - if (LocalEchoEventFactory.isLocalEchoId(readReceiptEventId)) { Timber.w("Can't set read receipt for local event ${params.fullyReadEventId}") } else { @@ -95,6 +101,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI } } + private fun isReadMarkerMoreRecent(roomId: String, fullyReadEventId: String): Boolean { return Realm.getInstance(monarchy.realmConfiguration).use { realm -> val readMarkerEntity = ReadMarkerEntity.where(realm, roomId = roomId).findFirst() @@ -106,12 +113,18 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI } } - private fun updateNotificationCountIfNecessary(roomId: String, eventId: String) { - monarchy.writeAsync { realm -> + private suspend fun updateReadMarker(roomId: String, eventId: String) { + monarchy.awaitTransaction { realm -> + roomFullyReadHandler.handle(realm, roomId, FullyReadContent(eventId)) + } + } + + private suspend fun updateNotificationCountIfNecessary(roomId: String, eventId: String) { + monarchy.awaitTransaction { realm -> val isLatestReceived = TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId == eventId if (isLatestReceived) { val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() - ?: return@writeAsync + ?: return@awaitTransaction roomSummary.notificationCount = 0 roomSummary.highlightCount = 0 } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index 26983a82c6..f14df5ada2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -27,36 +27,15 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.api.util.CancelableBag import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper import im.vector.matrix.android.internal.database.mapper.asDomain -import im.vector.matrix.android.internal.database.model.ChunkEntity -import im.vector.matrix.android.internal.database.model.ChunkEntityFields -import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity -import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.model.EventEntityFields -import im.vector.matrix.android.internal.database.model.ReadMarkerEntity -import im.vector.matrix.android.internal.database.model.RoomEntity -import im.vector.matrix.android.internal.database.model.TimelineEventEntity -import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields -import im.vector.matrix.android.internal.database.query.FilterContent -import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates -import im.vector.matrix.android.internal.database.query.findIncludingEvent -import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom -import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.database.query.whereInRoom +import im.vector.matrix.android.internal.database.model.* +import im.vector.matrix.android.internal.database.query.* import im.vector.matrix.android.internal.task.TaskConstraints import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.Debouncer import im.vector.matrix.android.internal.util.createBackgroundHandler import im.vector.matrix.android.internal.util.createUIHandler -import io.realm.ObjectChangeSet -import io.realm.OrderedCollectionChangeSet -import io.realm.OrderedRealmCollectionChangeListener -import io.realm.Realm -import io.realm.RealmConfiguration -import io.realm.RealmObjectChangeListener -import io.realm.RealmQuery -import io.realm.RealmResults -import io.realm.Sort +import io.realm.* import timber.log.Timber import java.util.* import java.util.concurrent.atomic.AtomicBoolean @@ -70,7 +49,7 @@ private const val DISPLAY_INDEX_UNKNOWN = Int.MIN_VALUE internal class DefaultTimeline( private val roomId: String, - private val initialEventId: String? = null, + private var initialEventId: String? = null, private val realmConfiguration: RealmConfiguration, private val taskExecutor: TaskExecutor, private val contextOfEventTask: GetContextOfEventTask, @@ -78,8 +57,9 @@ internal class DefaultTimeline( private val cryptoService: CryptoService, private val timelineEventMapper: TimelineEventMapper, private val settings: TimelineSettings, - private val hiddenReadReceipts: TimelineHiddenReadReceipts -) : Timeline, TimelineHiddenReadReceipts.Delegate { + private val hiddenReadReceipts: TimelineHiddenReadReceipts, + private val hiddenReadMarker: TimelineHiddenReadMarker +) : Timeline, TimelineHiddenReadReceipts.Delegate, TimelineHiddenReadMarker.Delegate { private companion object { val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD") @@ -104,11 +84,9 @@ internal class DefaultTimeline( private lateinit var eventRelations: RealmResults private var roomEntity: RoomEntity? = null - private var readMarkerEntity: ReadMarkerEntity? = null private var prevDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN private var nextDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN - private val isLive = initialEventId == null private val builtEvents = Collections.synchronizedList(ArrayList()) private val builtEventsIdMap = Collections.synchronizedMap(HashMap()) private val backwardsPaginationState = AtomicReference(PaginationState()) @@ -116,6 +94,9 @@ internal class DefaultTimeline( private val timelineID = UUID.randomUUID().toString() + override val isLive + get() = initialEventId == null + private val eventDecryptor = TimelineEventDecryptor(realmConfiguration, timelineID, cryptoService) private val eventsChangeListener = OrderedRealmCollectionChangeListener> { results, changeSet -> @@ -124,10 +105,7 @@ internal class DefaultTimeline( } else { // If changeSet has deletion we are having a gap, so we clear everything if (changeSet.deletionRanges.isNotEmpty()) { - prevDisplayIndex = DISPLAY_INDEX_UNKNOWN - nextDisplayIndex = DISPLAY_INDEX_UNKNOWN - builtEvents.clear() - builtEventsIdMap.clear() + clearAllValues() } changeSet.insertionRanges.forEach { range -> val (startDisplayIndex, direction) = if (range.startIndex == 0) { @@ -176,29 +154,6 @@ internal class DefaultTimeline( if (hasChange) postSnapshot() } - private val readMarkerListener = RealmObjectChangeListener { readMarkerEntity: ReadMarkerEntity, _: ObjectChangeSet? -> - val isEventHidden = liveEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, readMarkerEntity.eventId).findFirst() == null - var hasChange = false - if (isEventHidden) { - val hiddenEvent = readMarkerEntity.timelineEvent?.firstOrNull() ?: return@RealmObjectChangeListener - val displayIndex = hiddenEvent.root?.displayIndex - if (displayIndex != null) { - // Then we are looking for the first displayable event after the hidden one - val firstDisplayedEvent = liveEvents.where() - .lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex) - .findFirst() - - // If we find one, we should rebuild this one with marker - if (firstDisplayedEvent != null) { - hasChange = rebuildEvent(firstDisplayedEvent.eventId) { - it.copy(hasReadMarker = true) - } - } - } - } - if (hasChange) postSnapshot() - } - // Public methods ****************************************************************************** @@ -254,14 +209,10 @@ internal class DefaultTimeline( .findAllAsync() .also { it.addChangeListener(relationsListener) } - readMarkerEntity = ReadMarkerEntity.where(realm, roomId = roomId) - .findFirstAsync() - .also { it.addChangeListener(readMarkerListener) } - if (settings.buildReadReceipts) { hiddenReadReceipts.start(realm, liveEvents, this) } - + hiddenReadMarker.start(realm, liveEvents, this) isReady.set(true) } } @@ -276,10 +227,11 @@ internal class DefaultTimeline( roomEntity?.sendingTimelineEvents?.removeAllChangeListeners() eventRelations.removeAllChangeListeners() liveEvents.removeAllChangeListeners() - readMarkerEntity?.removeAllChangeListeners() + hiddenReadMarker.dispose() if (settings.buildReadReceipts) { hiddenReadReceipts.dispose() } + clearAllValues() backgroundRealm.getAndSet(null).also { it.close() } @@ -287,6 +239,27 @@ internal class DefaultTimeline( } } + override fun restartWithEventId(eventId: String) { + dispose() + initialEventId = eventId + start() + postSnapshot() + } + + override fun getIndexOfEvent(eventId: String?): Int? { + return builtEventsIdMap[eventId] + } + + override fun getTimelineEventAtIndex(index: Int): TimelineEvent? { + return builtEvents.getOrNull(index) + } + + override fun getTimelineEventWithId(eventId: String?): TimelineEvent? { + return builtEventsIdMap[eventId]?.let { + getTimelineEventAtIndex(it) + } + } + override fun hasMoreToLoad(direction: Timeline.Direction): Boolean { return hasMoreInCache(direction) || !hasReachedEnd(direction) } @@ -303,6 +276,18 @@ internal class DefaultTimeline( postSnapshot() } + // TimelineHiddenReadMarker.Delegate + + override fun rebuildEvent(eventId: String, hasReadMarker: Boolean): Boolean { + return rebuildEvent(eventId) { te -> + te.copy(hasReadMarker = hasReadMarker) + } + } + + override fun onReadMarkerUpdated() { + postSnapshot() + } + // Private methods ***************************************************************************** private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean { @@ -423,8 +408,9 @@ internal class DefaultTimeline( prevDisplayIndex = initialDisplayIndex nextDisplayIndex = initialDisplayIndex - if (initialEventId != null && shouldFetchInitialEvent) { - fetchEvent(initialEventId) + val currentInitialEventId = initialEventId + if (currentInitialEventId != null && shouldFetchInitialEvent) { + fetchEvent(currentInitialEventId) } else { val count = Math.min(settings.initialSize, liveEvents.size) if (isLive) { @@ -571,10 +557,11 @@ internal class DefaultTimeline( } private fun findCurrentChunk(realm: Realm): ChunkEntity? { - return if (initialEventId == null) { + val currentInitialEventId = initialEventId + return if (currentInitialEventId == null) { ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) } else { - ChunkEntity.findIncludingEvent(realm, initialEventId) + ChunkEntity.findIncludingEvent(realm, currentInitialEventId) } } @@ -594,11 +581,23 @@ internal class DefaultTimeline( } private fun postSnapshot() { - val snapshot = createSnapshot() - val runnable = Runnable { listener?.onUpdated(snapshot) } - debouncer.debounce("post_snapshot", runnable, 50) + BACKGROUND_HANDLER.post { + val snapshot = createSnapshot() + val runnable = Runnable { listener?.onUpdated(snapshot) } + debouncer.debounce("post_snapshot", runnable, 50) + } } + private fun clearAllValues() { + prevDisplayIndex = DISPLAY_INDEX_UNKNOWN + nextDisplayIndex = DISPLAY_INDEX_UNKNOWN + builtEvents.clear() + builtEventsIdMap.clear() + backwardsPaginationState.set(PaginationState()) + forwardsPaginationState.set(PaginationState()) + } + + // Extension methods *************************************************************************** private fun Timeline.Direction.toPaginationDirection(): PaginationDirection { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt index b6cc80ca78..59d37a8062 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt @@ -59,7 +59,8 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv cryptoService, timelineEventMapper, settings, - TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings) + TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings), + TimelineHiddenReadMarker(roomId) ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt new file mode 100644 index 0000000000..532a66140e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt @@ -0,0 +1,96 @@ +/* + + * Copyright 2019 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.matrix.android.internal.session.room.timeline + +import im.vector.matrix.android.internal.database.model.ReadMarkerEntity +import im.vector.matrix.android.internal.database.model.TimelineEventEntity +import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields +import im.vector.matrix.android.internal.database.query.where +import io.realm.Realm +import io.realm.RealmObjectChangeListener +import io.realm.RealmResults + +/** + * This class is responsible for handling the read marker for hidden events. + * When an hidden event has read marker, we want to transfer it on the first older displayed event. + * It has to be used in [DefaultTimeline] and we should call the [start] and [dispose] methods to properly handle realm subscription. + */ +internal class TimelineHiddenReadMarker constructor(private val roomId: String) { + + interface Delegate { + fun rebuildEvent(eventId: String, hasReadMarker: Boolean): Boolean + fun onReadMarkerUpdated() + } + + private var previousDisplayedEventId: String? = null + private var readMarkerEntity: ReadMarkerEntity? = null + + private lateinit var liveEvents: RealmResults + private lateinit var delegate: Delegate + + private val readMarkerListener = RealmObjectChangeListener { readMarker, _ -> + var hasChange = false + previousDisplayedEventId?.also { + hasChange = delegate.rebuildEvent(it, false) + previousDisplayedEventId = null + } + val isEventHidden = liveEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, readMarker.eventId).findFirst() == null + if (isEventHidden) { + val hiddenEvent = readMarker.timelineEvent?.firstOrNull() + ?: return@RealmObjectChangeListener + val displayIndex = hiddenEvent.root?.displayIndex + if (displayIndex != null) { + // Then we are looking for the first displayable event after the hidden one + val firstDisplayedEvent = liveEvents.where() + .lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex) + .findFirst() + + // If we find one, we should rebuild this one with marker + if (firstDisplayedEvent != null) { + previousDisplayedEventId = firstDisplayedEvent.eventId + hasChange = delegate.rebuildEvent(firstDisplayedEvent.eventId, true) + } + } + } + if (hasChange) delegate.onReadMarkerUpdated() + } + + + /** + * Start the realm query subscription. Has to be called on an HandlerThread + */ + fun start(realm: Realm, liveEvents: RealmResults, delegate: Delegate) { + this.liveEvents = liveEvents + this.delegate = delegate + // We are looking for read receipts set on hidden events. + // We only accept those with a timelineEvent (so coming from pagination/sync). + readMarkerEntity = ReadMarkerEntity.where(realm, roomId = roomId) + .findFirstAsync() + .also { it.addChangeListener(readMarkerListener) } + + } + + /** + * Dispose the realm query subscription. Has to be called on an HandlerThread + */ + fun dispose() { + this.readMarkerEntity?.removeAllChangeListeners() + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt index f142ca069e..9757d0f421 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.session.sync import im.vector.matrix.android.api.session.room.read.FullyReadContent import im.vector.matrix.android.internal.database.model.ReadMarkerEntity +import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.query.getOrCreate import im.vector.matrix.android.internal.database.query.where @@ -32,9 +33,14 @@ internal class RoomFullyReadHandler @Inject constructor() { return } Timber.v("Handle for roomId: $roomId eventId: ${content.eventId}") + + RoomSummaryEntity.getOrCreate(realm, roomId).apply { + readMarkerId = content.eventId + } val readMarkerEntity = ReadMarkerEntity.getOrCreate(realm, roomId).apply { eventId = content.eventId } + // Remove the old marker if any readMarkerEntity.timelineEvent?.firstOrNull()?.readMarker = null // Attach to timelineEvent if known diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt new file mode 100644 index 0000000000..398d525217 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt @@ -0,0 +1,75 @@ +/* + + * Copyright 2019 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.riotx.core.ui.views + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.LinearLayout +import android.widget.RelativeLayout +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import butterknife.ButterKnife +import im.vector.riotx.R +import im.vector.riotx.features.themes.ThemeUtils +import kotlinx.android.synthetic.main.view_jump_to_read_marker.view.* +import me.gujun.android.span.span +import me.saket.bettermovementmethod.BetterLinkMovementMethod + +class JumpToReadMarkerView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : RelativeLayout(context, attrs, defStyleAttr) { + + interface Callback { + fun onJumpToReadMarkerClicked(readMarkerId: String) + fun onClearReadMarkerClicked() + } + + var callback: Callback? = null + + init { + setupView() + } + + private fun setupView() { + LinearLayout.inflate(context, R.layout.view_jump_to_read_marker, this) + setBackgroundColor(ContextCompat.getColor(context, R.color.notification_accent_color)) + jumpToReadMarkerLabelView.movementMethod = BetterLinkMovementMethod.getInstance() + isClickable = true + closeJumpToReadMarkerView.setOnClickListener { + visibility = View.GONE + callback?.onClearReadMarkerClicked() + } + } + + fun render(show: Boolean, readMarkerId: String?) { + isVisible = show + if (readMarkerId != null) { + jumpToReadMarkerLabelView.text = span(resources.getString(R.string.room_jump_to_first_unread)) { + textDecorationLine = "underline" + onClick = { callback?.onJumpToReadMarkerClicked(readMarkerId) } + } + } + + } + + +} diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt new file mode 100644 index 0000000000..becab54da3 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt @@ -0,0 +1,86 @@ +/* + + * Copyright 2019 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.riotx.core.ui.views + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.view.animation.Animation +import android.view.animation.AnimationUtils +import im.vector.riotx.R +import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData +import kotlinx.coroutines.* + +private const val DELAY_IN_MS = 1_500L + +class ReadMarkerView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + + interface Callback { + fun onReadMarkerDisplayed() + } + + private var callback: Callback? = null + private var callbackDispatcherJob: Job? = null + + fun bindView(informationData: MessageInformationData, readMarkerCallback: Callback) { + this.callback = readMarkerCallback + if (informationData.displayReadMarker) { + visibility = VISIBLE + callbackDispatcherJob = GlobalScope.launch(Dispatchers.Main) { + delay(DELAY_IN_MS) + callback?.onReadMarkerDisplayed() + } + startAnimation() + } else { + visibility = INVISIBLE + } + + } + + fun unbind() { + this.callbackDispatcherJob?.cancel() + this.callback = null + this.animation?.cancel() + this.visibility = INVISIBLE + } + + private fun startAnimation() { + if (animation == null) { + animation = AnimationUtils.loadAnimation(context, R.anim.unread_marker_anim) + animation.startOffset = DELAY_IN_MS / 2 + animation.duration = DELAY_IN_MS / 2 + animation.setAnimationListener(object : Animation.AnimationListener { + override fun onAnimationStart(animation: Animation) { + } + + override fun onAnimationEnd(animation: Animation) { + visibility = INVISIBLE + } + + override fun onAnimationRepeat(animation: Animation) {} + }) + } + animation.start() + } + +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/DownloadFileState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/DownloadFileState.kt new file mode 100644 index 0000000000..2426a41e75 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/DownloadFileState.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 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.riotx.features.home.room.detail + +import java.io.File + +data class DownloadFileState( + val mimeType: String, + val file: File?, + val throwable: Throwable? + ) \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt index e60bc422a8..70d0d59c06 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt @@ -18,7 +18,6 @@ package im.vector.riotx.features.home.room.detail import com.jaiselrahman.filepicker.model.MediaFile import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary import im.vector.matrix.android.api.session.room.model.message.MessageFileContent import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent @@ -27,15 +26,18 @@ sealed class RoomDetailActions { data class SendMessage(val text: String, val autoMarkdown: Boolean) : RoomDetailActions() data class SendMedia(val mediaFiles: List) : RoomDetailActions() - data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions() + data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailActions() + data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailActions() data class LoadMoreTimelineEvents(val direction: Timeline.Direction) : RoomDetailActions() data class SendReaction(val reaction: String, val targetEventId: String) : RoomDetailActions() data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailActions() data class UndoReaction(val targetEventId: String, val key: String, val reason: String? = "") : RoomDetailActions() data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailActions() - data class NavigateToEvent(val eventId: String, val position: Int?) : RoomDetailActions() + data class NavigateToEvent(val eventId: String, val highlight: Boolean) : RoomDetailActions() + data class SetReadMarkerAction(val eventId: String) : RoomDetailActions() + object MarkAllAsRead : RoomDetailActions() data class DownloadFile(val eventId: String, val messageFileContent: MessageFileContent) : RoomDetailActions() - data class HandleTombstoneEvent(val event: Event): RoomDetailActions() + data class HandleTombstoneEvent(val event: Event) : RoomDetailActions() object AcceptInvite : RoomDetailActions() object RejectInvite : RoomDetailActions() @@ -47,5 +49,4 @@ sealed class RoomDetailActions { object ClearSendQueue : RoomDetailActions() object ResendAll : RoomDetailActions() - } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 19262fad49..fd83a6f69e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -28,7 +28,12 @@ import android.os.Parcelable import android.text.Editable import android.text.Spannable import android.text.TextUtils -import android.view.* +import android.view.HapticFeedbackConstants +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.Window import android.view.inputmethod.InputMethodManager import android.widget.TextView import android.widget.Toast @@ -46,7 +51,12 @@ import androidx.recyclerview.widget.RecyclerView import butterknife.BindView import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyVisibilityTracker -import com.airbnb.mvrx.* +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.args +import com.airbnb.mvrx.fragmentViewModel import com.github.piasy.biv.BigImageViewer import com.github.piasy.biv.loader.ImageLoader import com.google.android.material.snackbar.Snackbar @@ -60,7 +70,13 @@ import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.model.Membership -import im.vector.matrix.android.api.session.room.model.message.* +import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent +import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageFileContent +import im.vector.matrix.android.api.session.room.model.message.MessageImageContent +import im.vector.matrix.android.api.session.room.model.message.MessageTextContent +import im.vector.matrix.android.api.session.room.model.message.MessageType +import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent @@ -77,9 +93,21 @@ import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.core.files.addEntryToDownloadManager import im.vector.riotx.core.glide.GlideApp -import im.vector.riotx.core.ui.views.NotificationAreaView import im.vector.riotx.core.platform.VectorBaseFragment -import im.vector.riotx.core.utils.* +import im.vector.riotx.core.ui.views.JumpToReadMarkerView +import im.vector.riotx.core.ui.views.NotificationAreaView +import im.vector.riotx.core.utils.PERMISSIONS_FOR_TAKING_PHOTO +import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES +import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_DOWNLOAD_FILE +import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA +import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_CAMERA +import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CAMERA +import im.vector.riotx.core.utils.allGranted +import im.vector.riotx.core.utils.checkPermissions +import im.vector.riotx.core.utils.copyToClipboard +import im.vector.riotx.core.utils.openCamera +import im.vector.riotx.core.utils.shareMedia +import im.vector.riotx.core.utils.toast import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter @@ -94,9 +122,18 @@ import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController -import im.vector.riotx.features.home.room.detail.timeline.action.* +import im.vector.riotx.features.home.room.detail.timeline.action.ActionsHandler +import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet +import im.vector.riotx.features.home.room.detail.timeline.action.SimpleAction +import im.vector.riotx.features.home.room.detail.timeline.action.ViewEditHistoryBottomSheet +import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionBottomSheet import im.vector.riotx.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener -import im.vector.riotx.features.home.room.detail.timeline.item.* +import im.vector.riotx.features.home.room.detail.timeline.item.AbsMessageItem +import im.vector.riotx.features.home.room.detail.timeline.item.MessageFileItem +import im.vector.riotx.features.home.room.detail.timeline.item.MessageImageVideoItem +import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData +import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem +import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.html.PillImageSpan import im.vector.riotx.features.invite.VectorInviteView @@ -134,7 +171,8 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callback, AutocompleteUserPresenter.Callback, - VectorInviteView.Callback { + VectorInviteView.Callback, + JumpToReadMarkerView.Callback { companion object { @@ -194,6 +232,7 @@ class RoomDetailFragment : override fun getMenuRes() = R.menu.menu_timeline private lateinit var actionViewModel: ActionsHandler + private lateinit var layoutManager: LinearLayoutManager @BindView(R.id.composerLayout) lateinit var composerLayout: TextComposerView @@ -211,6 +250,7 @@ class RoomDetailFragment : setupAttachmentButton() setupInviteView() setupNotificationView() + setupJumpToReadMarkerView() roomDetailViewModel.subscribe { renderState(it) } textComposerViewModel.subscribe { renderTextComposerState(it) } roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) } @@ -224,8 +264,12 @@ class RoomDetailFragment : } roomDetailViewModel.navigateToEvent.observeEvent(this) { - // - scrollOnHighlightedEventCallback.scheduleScrollTo(it) + val scrollPosition = timelineEventController.searchPositionOfEvent(it) + if (scrollPosition == null) { + scrollOnHighlightedEventCallback.scheduleScrollTo(it) + } else { + layoutManager.scrollToPosition(scrollPosition) + } } roomDetailViewModel.selectSubscribe(this, RoomDetailViewState::tombstoneEventHandling, uniqueOnly("tombstoneEventHandling")) { @@ -259,6 +303,10 @@ class RoomDetailFragment : } } + private fun setupJumpToReadMarkerView() { + jumpToReadMarkerView.callback = this + } + private fun setupNotificationView() { notificationAreaView.delegate = object : NotificationAreaView.Delegate { @@ -380,7 +428,7 @@ class RoomDetailFragment : private fun setupRecyclerView() { val epoxyVisibilityTracker = EpoxyVisibilityTracker() epoxyVisibilityTracker.attach(recyclerView) - val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true) + layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true) val stateRestorer = LayoutManagerStateRestorer(layoutManager).register() scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager) scrollOnHighlightedEventCallback = ScrollOnHighlightedEventCallback(layoutManager, timelineEventController) @@ -405,7 +453,7 @@ class RoomDetailFragment : R.drawable.ic_reply, object : RoomMessageTouchHelperCallback.QuickReplayHandler { override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { - (model as? AbsMessageItem)?.informationData?.let { + (model as? AbsMessageItem)?.attributes?.informationData?.let { val eventId = it.eventId roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId)) } @@ -416,7 +464,7 @@ class RoomDetailFragment : is MessageFileItem, is MessageImageVideoItem, is MessageTextItem -> { - return (model as AbsMessageItem).informationData.sendState == SendState.SYNCED + return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED } else -> false } @@ -585,7 +633,7 @@ class RoomDetailFragment : val summary = state.asyncRoomSummary() val inviter = state.asyncInviter() if (summary?.membership == Membership.JOIN) { - timelineEventController.setTimeline(state.timeline, state.eventId) + timelineEventController.setTimeline(state.timeline, state.highlightedEventId) inviteView.visibility = View.GONE val uid = session.myUserId val meMember = session.getRoom(state.roomId)?.getRoomMember(uid) @@ -608,10 +656,12 @@ class RoomDetailFragment : composerLayout.visibility = View.GONE notificationAreaView.render(NotificationAreaView.State.Tombstone(state.tombstoneEvent)) } + jumpToReadMarkerView.render(state.showJumpToReadMarker, summary?.readMarkerId) } private fun renderRoomSummary(state: RoomDetailViewState) { state.asyncRoomSummary()?.let { + if (it.membership.isLeft()) { Timber.w("The room has been left") activity?.finish() @@ -684,7 +734,7 @@ class RoomDetailFragment : .show() } -// TimelineEventController.Callback ************************************************************ + // TimelineEventController.Callback ************************************************************ override fun onUrlClicked(url: String): Boolean { return permalinkHandler.launch(requireActivity(), url, object : NavigateToRoomInterceptor { @@ -696,7 +746,7 @@ class RoomDetailFragment : showSnackWithMessage(getString(R.string.navigate_to_room_when_already_in_the_room)) } else { // Highlight and scroll to this event - roomDetailViewModel.process(RoomDetailActions.NavigateToEvent(eventId, timelineEventController.searchPositionOfEvent(eventId))) + roomDetailViewModel.process(RoomDetailActions.NavigateToEvent(eventId, true)) } return true } @@ -716,7 +766,11 @@ class RoomDetailFragment : } override fun onEventVisible(event: TimelineEvent) { - roomDetailViewModel.process(RoomDetailActions.EventDisplayed(event)) + roomDetailViewModel.process(RoomDetailActions.TimelineEventTurnsVisible(event)) + } + + override fun onEventInvisible(event: TimelineEvent) { + roomDetailViewModel.process(RoomDetailActions.TimelineEventTurnsInvisible(event)) } override fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View) { @@ -836,7 +890,15 @@ class RoomDetailFragment : .show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS") } -// AutocompleteUserPresenter.Callback + override fun onReadMarkerLongDisplayed(informationData: MessageInformationData) { + val firstVisibleItem = layoutManager.findFirstVisibleItemPosition() + val eventId = timelineEventController.searchEventIdAtPosition(firstVisibleItem) + if (eventId != null) { + roomDetailViewModel.process(RoomDetailActions.SetReadMarkerAction(eventId)) + } + } + + // AutocompleteUserPresenter.Callback override fun onQueryUsers(query: CharSequence?) { textComposerViewModel.process(TextComposerActions.QueryUsers(query)) @@ -1001,7 +1063,7 @@ class RoomDetailFragment : snack.show() } -// VectorInviteView.Callback + // VectorInviteView.Callback override fun onAcceptInvite() { notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId) @@ -1012,4 +1074,16 @@ class RoomDetailFragment : notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId) roomDetailViewModel.process(RoomDetailActions.RejectInvite) } + + // JumpToReadMarkerView.Callback + + override fun onJumpToReadMarkerClicked(readMarkerId: String) { + roomDetailViewModel.process(RoomDetailActions.NavigateToEvent(readMarkerId, false)) + } + + override fun onClearReadMarkerClicked() { + roomDetailViewModel.process(RoomDetailActions.MarkAllAsRead) + } + + } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 607f999e30..bbdb7ab619 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -38,6 +38,7 @@ import im.vector.matrix.android.api.session.events.model.isTextMessage import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.room.model.Membership +import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.getFileUrl @@ -58,6 +59,8 @@ import im.vector.riotx.features.command.CommandParser import im.vector.riotx.features.command.ParsedCommand import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents import im.vector.riotx.features.settings.VectorPreferences +import io.reactivex.Observable +import io.reactivex.functions.Function3 import io.reactivex.rxkotlin.subscribeBy import org.commonmark.parser.Parser import org.commonmark.renderer.html.HtmlRenderer @@ -75,7 +78,8 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private val room = session.getRoom(initialState.roomId)!! private val roomId = initialState.roomId private val eventId = initialState.eventId - private val displayedEventsObservable = BehaviorRelay.create() + private val invisibleEventsObservable = BehaviorRelay.create() + private val visibleEventsObservable = BehaviorRelay.create() private val timelineSettings = if (userPreferencesProvider.shouldShowHiddenEvents()) { TimelineSettings(30, false, true, TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES, userPreferencesProvider.shouldShowReadReceipts()) } else { @@ -109,6 +113,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro observeRoomSummary() observeEventDisplayedActions() observeSummaryState() + observeJumpToReadMarkerViewVisibility() room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear() timeline.start() setState { copy(timeline = this@RoomDetailViewModel.timeline) } @@ -116,30 +121,37 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro fun process(action: RoomDetailActions) { when (action) { - is RoomDetailActions.SendMessage -> handleSendMessage(action) - is RoomDetailActions.SendMedia -> handleSendMedia(action) - is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action) - is RoomDetailActions.LoadMoreTimelineEvents -> handleLoadMore(action) - is RoomDetailActions.SendReaction -> handleSendReaction(action) - is RoomDetailActions.AcceptInvite -> handleAcceptInvite() - is RoomDetailActions.RejectInvite -> handleRejectInvite() - is RoomDetailActions.RedactAction -> handleRedactEvent(action) - is RoomDetailActions.UndoReaction -> handleUndoReact(action) - is RoomDetailActions.UpdateQuickReactAction -> handleUpdateQuickReaction(action) - is RoomDetailActions.EnterEditMode -> handleEditAction(action) - is RoomDetailActions.EnterQuoteMode -> handleQuoteAction(action) - is RoomDetailActions.EnterReplyMode -> handleReplyAction(action) - is RoomDetailActions.DownloadFile -> handleDownloadFile(action) - is RoomDetailActions.NavigateToEvent -> handleNavigateToEvent(action) - is RoomDetailActions.HandleTombstoneEvent -> handleTombstoneEvent(action) - is RoomDetailActions.ResendMessage -> handleResendEvent(action) - is RoomDetailActions.RemoveFailedEcho -> handleRemove(action) - is RoomDetailActions.ClearSendQueue -> handleClearSendQueue() - is RoomDetailActions.ResendAll -> handleResendAll() - else -> Timber.e("Unhandled Action: $action") + is RoomDetailActions.SendMessage -> handleSendMessage(action) + is RoomDetailActions.SendMedia -> handleSendMedia(action) + is RoomDetailActions.TimelineEventTurnsVisible -> handleEventVisible(action) + is RoomDetailActions.TimelineEventTurnsInvisible -> handleEventInvisible(action) + is RoomDetailActions.LoadMoreTimelineEvents -> handleLoadMore(action) + is RoomDetailActions.SendReaction -> handleSendReaction(action) + is RoomDetailActions.AcceptInvite -> handleAcceptInvite() + is RoomDetailActions.RejectInvite -> handleRejectInvite() + is RoomDetailActions.RedactAction -> handleRedactEvent(action) + is RoomDetailActions.UndoReaction -> handleUndoReact(action) + is RoomDetailActions.UpdateQuickReactAction -> handleUpdateQuickReaction(action) + is RoomDetailActions.EnterEditMode -> handleEditAction(action) + is RoomDetailActions.EnterQuoteMode -> handleQuoteAction(action) + is RoomDetailActions.EnterReplyMode -> handleReplyAction(action) + is RoomDetailActions.DownloadFile -> handleDownloadFile(action) + is RoomDetailActions.NavigateToEvent -> handleNavigateToEvent(action) + is RoomDetailActions.HandleTombstoneEvent -> handleTombstoneEvent(action) + is RoomDetailActions.ResendMessage -> handleResendEvent(action) + is RoomDetailActions.RemoveFailedEcho -> handleRemove(action) + is RoomDetailActions.ClearSendQueue -> handleClearSendQueue() + is RoomDetailActions.ResendAll -> handleResendAll() + is RoomDetailActions.SetReadMarkerAction -> handleSetReadMarkerAction(action) + is RoomDetailActions.MarkAllAsRead -> handleMarkAllAsRead() + else -> Timber.e("Unhandled Action: $action") } } + private fun handleEventInvisible(action: RoomDetailActions.TimelineEventTurnsInvisible) { + invisibleEventsObservable.accept(action) + } + private fun handleTombstoneEvent(action: RoomDetailActions.HandleTombstoneEvent) { val tombstoneContent = action.event.getClearContent().toModel() ?: return @@ -444,14 +456,14 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro room.sendMedias(attachments) } - private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) { + private fun handleEventVisible(action: RoomDetailActions.TimelineEventTurnsVisible) { if (action.event.root.sendState.isSent()) { //ignore pending/local events - displayedEventsObservable.accept(action) + visibleEventsObservable.accept(action) } //We need to update this with the related m.replace also (to move read receipt) action.event.annotations?.editSummary?.sourceEvents?.forEach { room.getTimeLineEvent(it)?.let { event -> - displayedEventsObservable.accept(RoomDetailActions.EventDisplayed(event)) + visibleEventsObservable.accept(RoomDetailActions.TimelineEventTurnsVisible(event)) } } } @@ -494,11 +506,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } - data class DownloadFileState( - val mimeType: String, - val file: File?, - val throwable: Throwable? - ) private fun handleDownloadFile(action: RoomDetailActions.DownloadFile) { session.downloadFile( @@ -530,53 +537,15 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private fun handleNavigateToEvent(action: RoomDetailActions.NavigateToEvent) { val targetEventId = action.eventId - - if (action.position != null) { - // Event is already in RAM - withState { - if (it.eventId == targetEventId) { - // ensure another click on the same permalink will also do a scroll - setState { - copy( - eventId = null - ) - } - } - - setState { - copy( - eventId = targetEventId - ) - } - } - - _navigateToEvent.postLiveEvent(targetEventId) - } else { - // change timeline - timeline.dispose() - timeline = room.createTimeline(targetEventId, timelineSettings) - timeline.start() - - withState { - if (it.eventId == targetEventId) { - // ensure another click on the same permalink will also do a scroll - setState { - copy( - eventId = null - ) - } - } - - setState { - copy( - eventId = targetEventId, - timeline = this@RoomDetailViewModel.timeline - ) - } - } - - _navigateToEvent.postLiveEvent(targetEventId) + val indexOfEvent = timeline.getIndexOfEvent(targetEventId) + if (indexOfEvent == null) { + // Event is not already in RAM + timeline.restartWithEventId(targetEventId) } + if (action.highlight) { + setState { copy(highlightedEventId = targetEventId) } + } + _navigateToEvent.postLiveEvent(targetEventId) } private fun handleResendEvent(action: RoomDetailActions.ResendMessage) { @@ -622,22 +591,36 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private fun observeEventDisplayedActions() { // We are buffering scroll events for one second // and keep the most recent one to set the read receipt on. - displayedEventsObservable + visibleEventsObservable .buffer(1, TimeUnit.SECONDS) .filter { it.isNotEmpty() } .subscribeBy(onNext = { actions -> - val readMarkerVisible = actions.find { it.event.hasReadMarker } != null val mostRecentEvent = actions.maxBy { it.event.displayIndex } mostRecentEvent?.event?.root?.eventId?.let { eventId -> room.setReadReceipt(eventId, callback = object : MatrixCallback {}) - if (readMarkerVisible) { - room.setReadMarker(eventId, callback = object : MatrixCallback {}) - } } }) .disposeOnClear() } + private fun handleSetReadMarkerAction(action: RoomDetailActions.SetReadMarkerAction) = withState { state -> + var readMarkerId = action.eventId + if (readMarkerId == state.asyncRoomSummary()?.readMarkerId) { + val indexOfEvent = timeline.getIndexOfEvent(action.eventId) + // force to set the read marker on the next event + if (indexOfEvent != null) { + timeline.getTimelineEventAtIndex(indexOfEvent - 1)?.root?.eventId?.also { eventIdOfNext -> + readMarkerId = eventIdOfNext + } + } + } + room.setReadMarker(readMarkerId, callback = object : MatrixCallback {}) + } + + private fun handleMarkAllAsRead() { + room.markAllAsRead(object : MatrixCallback {}) + } + private fun observeSyncState() { session.rx() .liveSyncState() @@ -649,6 +632,39 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro .disposeOnClear() } + private fun observeJumpToReadMarkerViewVisibility() { + Observable + .combineLatest( + room.rx().liveRoomSummary(), + visibleEventsObservable.distinctUntilChanged(), + isEventVisibleObservable { it.hasReadMarker }.startWith(false), + Function3 { roomSummary, currentVisibleEvent, isReadMarkerViewVisible -> + val readMarkerId = roomSummary.readMarkerId + if (readMarkerId == null || isReadMarkerViewVisible || !timeline.isLive) { + false + } else { + val readMarkerPosition = timeline.getIndexOfEvent(readMarkerId) + ?: Int.MAX_VALUE + val currentVisibleEventPosition = timeline.getIndexOfEvent(currentVisibleEvent.event.root.eventId) + ?: Int.MIN_VALUE + readMarkerPosition > currentVisibleEventPosition + } + } + ) + .distinctUntilChanged() + .subscribe { + setState { copy(showJumpToReadMarker = it) } + } + .disposeOnClear() + } + + private fun isEventVisibleObservable(filterEvent: (TimelineEvent) -> Boolean): Observable { + return Observable.merge( + visibleEventsObservable.filter { filterEvent(it.event) }.map { true }, + invisibleEventsObservable.filter { filterEvent(it.event) }.map { false } + ) + } + private fun observeRoomSummary() { room.rx().liveRoomSummary() .execute { async -> diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt index d8358efe16..5e36cf42dc 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt @@ -51,7 +51,9 @@ data class RoomDetailViewState( val isEncrypted: Boolean = false, val tombstoneEvent: Event? = null, val tombstoneEventHandling: Async = Uninitialized, - val syncState: SyncState = SyncState.IDLE + val syncState: SyncState = SyncState.IDLE, + val showJumpToReadMarker: Boolean = false, + val highlightedEventId: String? = null ) : MvRxState { constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt index 43828b0ee2..cf483090f1 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt @@ -38,7 +38,7 @@ class ScrollOnHighlightedEventCallback(private val layoutManager: LinearLayoutMa // Do not scroll it item is already visible if (positionToScroll !in firstVisibleItem..lastVisibleItem) { // Note: Offset will be from the bottom, since the layoutManager is reversed - layoutManager.scrollToPositionWithOffset(positionToScroll, 120) + layoutManager.scrollToPosition(position) } scheduledEventId.set(null) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index 3c212d6129..ffc573a634 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -49,11 +49,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private val timelineMediaSizeProvider: TimelineMediaSizeProvider, private val avatarRenderer: AvatarRenderer, @TimelineEventControllerHandler - private val backgroundHandler: Handler, - userPreferencesProvider: UserPreferencesProvider + private val backgroundHandler: Handler ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener { interface Callback : BaseCallback, ReactionPillCallback, AvatarCallback, UrlClickCallback, ReadReceiptsCallback { + fun onEventInvisible(event: TimelineEvent) fun onEventVisible(event: TimelineEvent) fun onRoomCreateLinkClicked(url: String) fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View) @@ -81,6 +81,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec interface ReadReceiptsCallback { fun onReadReceiptsClicked(readReceipts: List) + fun onReadMarkerLongDisplayed(informationData: MessageInformationData) } interface UrlClickCallback { @@ -140,8 +141,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } } - private val showHiddenEvents = userPreferencesProvider.shouldShowHiddenEvents() - init { requestModelBuild() } @@ -247,7 +246,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private fun buildItemModels(currentPosition: Int, items: List): CacheItemData { val event = items[currentPosition] - val nextEvent = items.nextDisplayableEvent(currentPosition, showHiddenEvents) + val nextEvent = items.nextOrNull(currentPosition) val date = event.root.localDateTime() val nextDate = nextEvent?.root?.localDateTime() val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate() @@ -327,24 +326,50 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec return shouldAdd } - fun searchPositionOfEvent(eventId: String): Int? { - synchronized(modelCache) { - // Search in the cache - modelCache.forEachIndexed { idx, cacheItemData -> - if (cacheItemData?.eventId == eventId) { - return idx - } + fun searchPositionOfEvent(eventId: String): Int? = synchronized(modelCache) { + // Search in the cache + var realPosition = 0 + for (i in 0 until modelCache.size) { + val itemCache = modelCache[i] + if (itemCache?.eventId == eventId) { + return realPosition + } + if (itemCache?.eventModel != null) { + realPosition++ + } + if (itemCache?.mergedHeaderModel != null) { + realPosition++ + } + if (itemCache?.formattedDayModel != null) { + realPosition++ } - - return null } + return null } -} -private data class CacheItemData( - val localId: Long, - val eventId: String?, - val eventModel: EpoxyModel<*>? = null, - val mergedHeaderModel: MergedHeaderItem? = null, - val formattedDayModel: DaySeparatorItem? = null -) + fun searchEventIdAtPosition(position: Int): String? = synchronized(modelCache) { + var offsetValue = 0 + for (i in 0 until position) { + val itemCache = modelCache[i] + if (itemCache?.eventModel == null) { + offsetValue-- + } + if (itemCache?.mergedHeaderModel != null) { + offsetValue++ + } + if (itemCache?.formattedDayModel != null) { + offsetValue++ + } + } + return modelCache.getOrNull(position - offsetValue)?.eventId + } + + private data class CacheItemData( + val localId: Long, + val eventId: String?, + val eventModel: EpoxyModel<*>? = null, + val mergedHeaderModel: MergedHeaderItem? = null, + val formattedDayModel: DaySeparatorItem? = null + ) + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt index 080565cd16..938ac4673e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt @@ -16,7 +16,6 @@ package im.vector.riotx.features.home.room.detail.timeline.factory -import android.view.View import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.timeline.TimelineEvent @@ -24,11 +23,11 @@ import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.StringProvider -import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem_ -import im.vector.riotx.features.home.room.detail.timeline.util.MessageInformationDataFactory +import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory +import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory import me.gujun.android.span.span import javax.inject.Inject @@ -36,7 +35,7 @@ import javax.inject.Inject class EncryptedItemFactory @Inject constructor(private val messageInformationDataFactory: MessageInformationDataFactory, private val colorProvider: ColorProvider, private val stringProvider: StringProvider, - private val avatarRenderer: AvatarRenderer) { + private val attributesFactory: MessageItemAttributesFactory) { fun create(event: TimelineEvent, nextEvent: TimelineEvent?, @@ -65,22 +64,13 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat // TODO This is not correct format for error, change it val informationData = messageInformationDataFactory.create(event, nextEvent) + val attributes = attributesFactory.create(null, informationData, callback) return MessageTextItem_() + .attributes(attributes) .message(spannableStr) - .avatarRenderer(avatarRenderer) - .colorProvider(colorProvider) - .informationData(informationData) .highlighted(highlight) - .avatarCallback(callback) .urlClickCallback(callback) - .cellClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onEncryptedMessageClicked(informationData, view) - })) - .longClickListener { view -> - return@longClickListener callback?.onEventLongClicked(informationData, null, view) - ?: false - } + } else -> null } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt deleted file mode 100644 index 4a3f50c45e..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2019 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.riotx.features.home.room.detail.timeline.factory - -import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.events.model.EventType -import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.room.timeline.TimelineEvent -import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent -import im.vector.riotx.R -import im.vector.riotx.core.resources.StringProvider -import im.vector.riotx.features.home.AvatarRenderer -import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController -import im.vector.riotx.features.home.room.detail.timeline.helper.senderAvatar -import im.vector.riotx.features.home.room.detail.timeline.helper.senderName -import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData -import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem -import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem_ -import javax.inject.Inject - -class EncryptionItemFactory @Inject constructor(private val stringProvider: StringProvider, - private val avatarRenderer: AvatarRenderer) { - - fun create(event: TimelineEvent, - highlight: Boolean, - callback: TimelineEventController.BaseCallback?): NoticeItem? { - - val text = buildNoticeText(event.root, event.senderName) ?: return null - val informationData = MessageInformationData( - eventId = event.root.eventId ?: "?", - senderId = event.root.senderId ?: "", - sendState = event.root.sendState, - avatarUrl = event.senderAvatar(), - memberName = event.senderName(), - showInformation = false - ) - return NoticeItem_() - .avatarRenderer(avatarRenderer) - .noticeText(text) - .informationData(informationData) - .highlighted(highlight) - .baseCallback(callback) - } - - private fun buildNoticeText(event: Event, senderName: String?): CharSequence? { - return when { - EventType.ENCRYPTION == event.getClearType() -> { - val content = event.content.toModel() ?: return null - stringProvider.getString(R.string.notice_end_to_end, senderName, content.algorithm) - } - else -> null - } - - } - - -} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index f3a93a8d6d..57baf4fee8 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -47,27 +47,14 @@ import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.linkify.VectorLinkify import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.StringProvider -import im.vector.riotx.core.resources.UserPreferencesProvider import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider -import im.vector.riotx.features.home.room.detail.timeline.helper.senderAvatar -import im.vector.riotx.features.home.room.detail.timeline.item.BlankItem_ -import im.vector.riotx.features.home.room.detail.timeline.item.DefaultItem -import im.vector.riotx.features.home.room.detail.timeline.item.DefaultItem_ -import im.vector.riotx.features.home.room.detail.timeline.item.MessageFileItem -import im.vector.riotx.features.home.room.detail.timeline.item.MessageFileItem_ -import im.vector.riotx.features.home.room.detail.timeline.item.MessageImageVideoItem -import im.vector.riotx.features.home.room.detail.timeline.item.MessageImageVideoItem_ -import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData -import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem -import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem_ -import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem_ -import im.vector.riotx.features.home.room.detail.timeline.item.RedactedMessageItem -import im.vector.riotx.features.home.room.detail.timeline.item.RedactedMessageItem_ -import im.vector.riotx.features.home.room.detail.timeline.util.MessageInformationDataFactory +import im.vector.riotx.features.home.room.detail.timeline.item.* +import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory +import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.media.ImageContentRenderer import im.vector.riotx.features.media.VideoContentRenderer @@ -75,14 +62,13 @@ import me.gujun.android.span.span import javax.inject.Inject class MessageItemFactory @Inject constructor( - private val avatarRenderer: AvatarRenderer, private val colorProvider: ColorProvider, private val timelineMediaSizeProvider: TimelineMediaSizeProvider, private val htmlRenderer: Lazy, private val stringProvider: StringProvider, - private val emojiCompatFontProvider: EmojiCompatFontProvider, private val imageContentRenderer: ImageContentRenderer, private val messageInformationDataFactory: MessageInformationDataFactory, + private val messageItemAttributesFactory: MessageItemAttributesFactory, private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder, private val noticeItemFactory: NoticeItemFactory) { @@ -98,36 +84,41 @@ class MessageItemFactory @Inject constructor( if (event.root.isRedacted()) { //message is redacted - return buildRedactedItem(informationData, highlight, callback) + val attributes = messageItemAttributesFactory.create(null, informationData, callback) + return buildRedactedItem(attributes, highlight) } val messageContent: MessageContent = event.getLastMessageContent() - ?: //Malformed content, we should echo something on screen - return DefaultItem_().text(stringProvider.getString(R.string.malformed_message)) + ?: //Malformed content, we should echo something on screen + return DefaultItem_().text(stringProvider.getString(R.string.malformed_message)) if (messageContent.relatesTo?.type == RelationType.REPLACE - || event.isEncrypted() && event.root.content.toModel()?.relatesTo?.type == RelationType.REPLACE + || event.isEncrypted() && event.root.content.toModel()?.relatesTo?.type == RelationType.REPLACE ) { // This is an edit event, we should it when debugging as a notice event return noticeItemFactory.create(event, highlight, callback) } + val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback) + // val all = event.root.toContent() // val ev = all.toModel() return when (messageContent) { is MessageEmoteContent -> buildEmoteMessageItem(messageContent, - informationData, - highlight, - callback) + informationData, + highlight, + callback, + attributes) is MessageTextContent -> buildTextMessageItem(messageContent, - informationData, - highlight, - callback) - is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback) - is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback) - is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback) - is MessageFileContent -> buildFileMessageItem(messageContent, informationData, highlight, callback) - is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, callback) + informationData, + highlight, + callback, + attributes) + is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageFileContent -> buildFileMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, callback, attributes) else -> buildNotHandledMessageItem(messageContent, highlight) } } @@ -135,55 +126,29 @@ class MessageItemFactory @Inject constructor( private fun buildAudioMessageItem(messageContent: MessageAudioContent, informationData: MessageInformationData, highlight: Boolean, - callback: TimelineEventController.Callback?): MessageFileItem? { + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes): MessageFileItem? { return MessageFileItem_() - .avatarRenderer(avatarRenderer) - .colorProvider(colorProvider) - .informationData(informationData) + .attributes(attributes) .highlighted(highlight) - .avatarCallback(callback) - .readReceiptsCallback(callback) .filename(messageContent.body) .iconRes(R.drawable.filetype_audio) - .reactionPillCallback(callback) - .emojiTypeFace(emojiCompatFontProvider.typeface) - .cellClickListener( - DebouncedClickListener(View.OnClickListener { view: View -> - callback?.onEventCellClicked(informationData, messageContent, view) - })) .clickListener( DebouncedClickListener(View.OnClickListener { callback?.onAudioMessageClicked(messageContent) })) - .longClickListener { view -> - return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false - } } private fun buildFileMessageItem(messageContent: MessageFileContent, informationData: MessageInformationData, highlight: Boolean, - callback: TimelineEventController.Callback?): MessageFileItem? { + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes): MessageFileItem? { return MessageFileItem_() - .avatarRenderer(avatarRenderer) - .colorProvider(colorProvider) - .informationData(informationData) + .attributes(attributes) .highlighted(highlight) - .avatarCallback(callback) .filename(messageContent.body) - .reactionPillCallback(callback) - .readReceiptsCallback(callback) - .emojiTypeFace(emojiCompatFontProvider.typeface) .iconRes(R.drawable.filetype_attachment) - .cellClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onEventCellClicked(informationData, messageContent, view) - })) - .longClickListener { view -> - return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false - } .clickListener( DebouncedClickListener(View.OnClickListener { _ -> callback?.onFileMessageClicked(informationData.eventId, messageContent) @@ -200,7 +165,8 @@ class MessageItemFactory @Inject constructor( private fun buildImageMessageItem(messageContent: MessageImageContent, informationData: MessageInformationData, highlight: Boolean, - callback: TimelineEventController.Callback?): MessageImageVideoItem? { + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes): MessageImageVideoItem? { val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() val data = ImageContentRenderer.Data( @@ -215,42 +181,29 @@ class MessageItemFactory @Inject constructor( rotation = messageContent.info?.rotation ) return MessageImageVideoItem_() - .avatarRenderer(avatarRenderer) - .colorProvider(colorProvider) + .attributes(attributes) .imageContentRenderer(imageContentRenderer) .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) .playable(messageContent.info?.mimeType == "image/gif") - .informationData(informationData) .highlighted(highlight) - .avatarCallback(callback) .mediaData(data) - .reactionPillCallback(callback) - .readReceiptsCallback(callback) - .emojiTypeFace(emojiCompatFontProvider.typeface) .clickListener( DebouncedClickListener(View.OnClickListener { view -> callback?.onImageMessageClicked(messageContent, data, view) })) - .cellClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onEventCellClicked(informationData, messageContent, view) - })) - .longClickListener { view -> - return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false - } } private fun buildVideoMessageItem(messageContent: MessageVideoContent, informationData: MessageInformationData, highlight: Boolean, - callback: TimelineEventController.Callback?): MessageImageVideoItem? { + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes): MessageImageVideoItem? { val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() val thumbnailData = ImageContentRenderer.Data( filename = messageContent.body, url = messageContent.videoInfo?.thumbnailFile?.url - ?: messageContent.videoInfo?.thumbnailUrl, + ?: messageContent.videoInfo?.thumbnailUrl, elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(), height = messageContent.videoInfo?.height, maxHeight = maxHeight, @@ -267,33 +220,20 @@ class MessageItemFactory @Inject constructor( ) return MessageImageVideoItem_() + .attributes(attributes) .imageContentRenderer(imageContentRenderer) .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) - .avatarRenderer(avatarRenderer) - .colorProvider(colorProvider) .playable(true) - .informationData(informationData) .highlighted(highlight) - .avatarCallback(callback) .mediaData(thumbnailData) - .reactionPillCallback(callback) - .readReceiptsCallback(callback) - .emojiTypeFace(emojiCompatFontProvider.typeface) - .cellClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onEventCellClicked(informationData, messageContent, view) - })) .clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) } - .longClickListener { view -> - return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false - } } private fun buildTextMessageItem(messageContent: MessageTextContent, informationData: MessageInformationData, highlight: Boolean, - callback: TimelineEventController.Callback?): MessageTextItem? { + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes): MessageTextItem? { val bodyToUse = messageContent.formattedBody?.let { htmlRenderer.get().render(it.trim()) @@ -310,24 +250,10 @@ class MessageItemFactory @Inject constructor( message(linkifiedBody) } } - .avatarRenderer(avatarRenderer) - .informationData(informationData) - .colorProvider(colorProvider) + .attributes(attributes) .highlighted(highlight) - .avatarCallback(callback) .urlClickCallback(callback) - .reactionPillCallback(callback) - .readReceiptsCallback(callback) - .emojiTypeFace(emojiCompatFontProvider.typeface) - //click on the text - .cellClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onEventCellClicked(informationData, messageContent, view) - })) - .longClickListener { view -> - return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false - } + //click on the text } private fun annotateWithEdited(linkifiedBody: CharSequence, @@ -356,16 +282,17 @@ class MessageItemFactory @Inject constructor( //nop } }, - editStart, - editEnd, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + editStart, + editEnd, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE) return spannable } private fun buildNoticeMessageItem(messageContent: MessageNoticeContent, informationData: MessageInformationData, highlight: Boolean, - callback: TimelineEventController.Callback?): MessageTextItem? { + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes): MessageTextItem? { val message = messageContent.body.let { val formattedBody = span { @@ -376,34 +303,17 @@ class MessageItemFactory @Inject constructor( linkifyBody(formattedBody, callback) } return MessageTextItem_() - .avatarRenderer(avatarRenderer) + .attributes(attributes) .message(message) - .colorProvider(colorProvider) - .informationData(informationData) .highlighted(highlight) - .avatarCallback(callback) - .reactionPillCallback(callback) .urlClickCallback(callback) - .readReceiptsCallback(callback) - .emojiTypeFace(emojiCompatFontProvider.typeface) - .memberClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onMemberNameClicked(informationData) - })) - .cellClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onEventCellClicked(informationData, messageContent, view) - })) - .longClickListener { view -> - return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false - } } private fun buildEmoteMessageItem(messageContent: MessageEmoteContent, informationData: MessageInformationData, highlight: Boolean, - callback: TimelineEventController.Callback?): MessageTextItem? { + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes): MessageTextItem? { val message = messageContent.body.let { val formattedBody = "* ${informationData.memberName} $it" @@ -418,43 +328,16 @@ class MessageItemFactory @Inject constructor( message(message) } } - .avatarRenderer(avatarRenderer) - .colorProvider(colorProvider) - .informationData(informationData) + .attributes(attributes) .highlighted(highlight) - .avatarCallback(callback) - .reactionPillCallback(callback) - .readReceiptsCallback(callback) .urlClickCallback(callback) - .emojiTypeFace(emojiCompatFontProvider.typeface) - .cellClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onEventCellClicked(informationData, messageContent, view) - })) - .longClickListener { view -> - return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false - } } - private fun buildRedactedItem(informationData: MessageInformationData, - highlight: Boolean, - callback: TimelineEventController.Callback?): RedactedMessageItem? { + private fun buildRedactedItem(attributes: AbsMessageItem.Attributes, + highlight: Boolean): RedactedMessageItem? { return RedactedMessageItem_() - .avatarRenderer(avatarRenderer) - .colorProvider(colorProvider) - .informationData(informationData) + .attributes(attributes) .highlighted(highlight) - .avatarCallback(callback) - .readReceiptsCallback(callback) - .cellClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onEventCellClicked(informationData, null, view) - })) - .longClickListener { view -> - return@longClickListener callback?.onEventLongClicked(informationData, null, view) - ?: false - } } private fun linkifyBody(body: CharSequence, callback: TimelineEventController.Callback?): CharSequence { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt index f73a200133..6955cf3593 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt @@ -20,12 +20,9 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter -import im.vector.riotx.features.home.room.detail.timeline.helper.senderAvatar -import im.vector.riotx.features.home.room.detail.timeline.helper.senderName -import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData +import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem_ -import im.vector.riotx.features.home.room.detail.timeline.util.MessageInformationDataFactory import javax.inject.Inject class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEventFormatter, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index b1ae595ea0..9913f219f1 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -20,17 +20,11 @@ import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotx.core.epoxy.EmptyItem_ import im.vector.riotx.core.epoxy.VectorEpoxyModel -import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController -import im.vector.riotx.features.home.room.detail.timeline.helper.senderAvatar -import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData -import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem_ -import im.vector.riotx.features.home.room.detail.timeline.util.MessageInformationDataFactory import timber.log.Timber import javax.inject.Inject class TimelineItemFactory @Inject constructor(private val messageItemFactory: MessageItemFactory, - private val encryptionItemFactory: EncryptionItemFactory, private val encryptedItemFactory: EncryptedItemFactory, private val noticeItemFactory: NoticeItemFactory, private val defaultItemFactory: DefaultItemFactory, @@ -40,6 +34,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me nextEvent: TimelineEvent?, eventIdToHighlight: String?, callback: TimelineEventController.Callback?): VectorEpoxyModel<*> { + val highlight = event.root.eventId == eventIdToHighlight val computedModel = try { @@ -55,11 +50,11 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.CALL_HANGUP, EventType.CALL_ANSWER, EventType.REACTION, - EventType.REDACTION -> noticeItemFactory.create(event, highlight, callback) + EventType.REDACTION, + EventType.ENCRYPTION -> noticeItemFactory.create(event, highlight, callback) // State room create EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback) // Crypto - EventType.ENCRYPTION -> encryptionItemFactory.create(event, highlight, callback) EventType.ENCRYPTED -> { if (event.root.isRedacted()) { // Redacted event, let the MessageItemFactory handle it diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index 05ce7a9c19..2fcc1744da 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.* import im.vector.matrix.android.api.session.room.model.call.CallInviteContent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent import im.vector.riotx.R import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.home.room.detail.timeline.helper.senderName @@ -41,6 +42,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin EventType.CALL_INVITE, EventType.CALL_HANGUP, EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) + EventType.ENCRYPTION -> formatEncryptionEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.MESSAGE, EventType.REACTION, EventType.REDACTION -> formatDebug(timelineEvent.root) @@ -60,6 +62,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin EventType.CALL_INVITE, EventType.CALL_HANGUP, EventType.CALL_ANSWER -> formatCallEvent(event, senderName) + EventType.ENCRYPTION -> formatEncryptionEvent(event, senderName) EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(event, senderName) else -> { Timber.v("Type $type not handled by this formatter") @@ -96,7 +99,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?): CharSequence? { val historyVisibility = event.getClearContent().toModel()?.historyVisibility - ?: return null + ?: return null val formattedVisibility = when (historyVisibility) { RoomHistoryVisibility.SHARED -> stringProvider.getString(R.string.notice_room_visibility_shared) @@ -146,7 +149,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin stringProvider.getString(R.string.notice_display_name_removed, event.senderId, prevEventContent?.displayName) else -> stringProvider.getString(R.string.notice_display_name_changed_from, - event.senderId, prevEventContent?.displayName, eventContent?.displayName) + event.senderId, prevEventContent?.displayName, eventContent?.displayName) } displayText.append(displayNameText) } @@ -173,7 +176,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin when { eventContent.thirdPartyInvite != null -> stringProvider.getString(R.string.notice_room_third_party_registered_invite, - targetDisplayName, eventContent.thirdPartyInvite?.displayName) + targetDisplayName, eventContent.thirdPartyInvite?.displayName) TextUtils.equals(event.stateKey, selfUserId) -> stringProvider.getString(R.string.notice_room_invite_you, senderDisplayName) event.stateKey.isNullOrEmpty() -> @@ -209,4 +212,9 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin } } + private fun formatEncryptionEvent(event: Event, senderName: String?): CharSequence? { + val eventContent: EncryptionEventContent = event.getClearContent().toModel() ?: return null + return stringProvider.getString(R.string.notice_end_to_end, senderName, eventContent.algorithm) + } + } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/util/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt similarity index 82% rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/util/MessageInformationDataFactory.kt rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index 71a7549b46..1e978bcfc6 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/util/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -1,20 +1,22 @@ /* - * Copyright 2019 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. + + * Copyright 2019 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.riotx.features.home.room.detail.timeline.util +package im.vector.riotx.features.home.room.detail.timeline.helper import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.EventType @@ -62,7 +64,8 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId ?: "")) } - val displayReadMarker = event.hasReadMarker && event.readReceipts.find { it.user.userId == session.myUserId } == null + val displayReadMarker = event.hasReadMarker + && event.readReceipts.find { it.user.userId == session.myUserId } == null return MessageInformationData( eventId = eventId, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt new file mode 100644 index 0000000000..47b5094c95 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt @@ -0,0 +1,58 @@ +/* + + * Copyright 2019 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.riotx.features.home.room.detail.timeline.helper + +import android.view.View +import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.riotx.EmojiCompatFontProvider +import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.core.utils.DebouncedClickListener +import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController +import im.vector.riotx.features.home.room.detail.timeline.item.AbsMessageItem +import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData +import javax.inject.Inject + +class MessageItemAttributesFactory @Inject constructor( + private val avatarRenderer: AvatarRenderer, + private val colorProvider: ColorProvider, + private val emojiCompatFontProvider: EmojiCompatFontProvider) { + + fun create(messageContent: MessageContent?, informationData: MessageInformationData, callback: TimelineEventController.Callback?): AbsMessageItem.Attributes { + return AbsMessageItem.Attributes( + informationData = informationData, + avatarRenderer = avatarRenderer, + colorProvider = colorProvider, + itemLongClickListener = View.OnLongClickListener { view -> + callback?.onEventLongClicked(informationData, messageContent, view) ?: false + }, + itemClickListener = DebouncedClickListener(View.OnClickListener { view -> + callback?.onEventCellClicked(informationData, messageContent, view) + }), + memberClickListener = DebouncedClickListener(View.OnClickListener { view -> + callback?.onMemberNameClicked(informationData) + }), + reactionPillCallback = callback, + avatarCallback = callback, + readReceiptsCallback = callback, + emojiTypeFace = emojiCompatFontProvider.typeface + ) + + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt index fa0a71bde2..b9c9d992f8 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -49,29 +49,6 @@ object TimelineDisplayableEvents { ) } -fun TimelineEvent.isDisplayable(showHiddenEvent: Boolean): Boolean { - val allowed = TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES.takeIf { showHiddenEvent } - ?: TimelineDisplayableEvents.DISPLAYABLE_TYPES - if (!allowed.contains(root.type)) { - return false - } - if (root.content.isNullOrEmpty()) { - return false - } - //Edits should be filtered out! - if (EventType.MESSAGE == root.type - && root.content.toModel()?.relatesTo?.type == RelationType.REPLACE) { - return false - } - return true -} -// -//fun List.filterDisplayableEvents(): List { -// return this.filter { -// it.isDisplayable() -// } -//} - fun TimelineEvent.senderAvatar(): String? { // We might have no avatar when user leave, so we try to get it from prevContent return senderAvatar @@ -131,10 +108,10 @@ fun List.prevSameTypeEvents(index: Int, minSize: Int): List.nextDisplayableEvent(index: Int, showHiddenEvent: Boolean): TimelineEvent? { +fun List.nextOrNull(index: Int): TimelineEvent? { return if (index >= size - 1) { null } else { - subList(index + 1, this.size).firstOrNull { it.isDisplayable(showHiddenEvent) } + subList(index + 1, this.size).firstOrNull() } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt index 95d9b6f43b..eb3dc44e56 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt @@ -28,9 +28,10 @@ class TimelineEventVisibilityStateChangedListener(private val callback: Timeline override fun onVisibilityStateChanged(visibilityState: Int) { if (visibilityState == VisibilityState.VISIBLE) { callback?.onEventVisible(event) + } else if (visibilityState == VisibilityState.INVISIBLE) { + callback?.onEventInvisible(event) } } - } @@ -40,9 +41,9 @@ class MergedTimelineEventVisibilityStateChangedListener(private val callback: Ti override fun onVisibilityStateChanged(visibilityState: Int) { if (visibilityState == VisibilityState.VISIBLE) { - events.forEach { - callback?.onEventVisible(it) - } + events.forEach { callback?.onEventVisible(it) } + } else if (visibilityState == VisibilityState.INVISIBLE) { + events.forEach { callback?.onEventInvisible(it) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt index 570daf669c..5431b4ca85 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -32,6 +32,7 @@ import com.airbnb.epoxy.EpoxyAttribute import im.vector.matrix.android.api.session.room.send.SendState import im.vector.riotx.R import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.core.ui.views.ReadMarkerView import im.vector.riotx.core.ui.views.ReadReceiptsView import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.core.utils.DimensionUtils.dpToPx @@ -43,63 +44,42 @@ import im.vector.riotx.features.ui.getMessageTextColor abstract class AbsMessageItem : BaseEventItem() { @EpoxyAttribute - lateinit var informationData: MessageInformationData - - @EpoxyAttribute - lateinit var avatarRenderer: AvatarRenderer - - @EpoxyAttribute - lateinit var colorProvider: ColorProvider - - @EpoxyAttribute - var longClickListener: View.OnLongClickListener? = null - - @EpoxyAttribute - var cellClickListener: View.OnClickListener? = null - - @EpoxyAttribute - var memberClickListener: View.OnClickListener? = null - - @EpoxyAttribute - var emojiTypeFace: Typeface? = null - - @EpoxyAttribute - var reactionPillCallback: TimelineEventController.ReactionPillCallback? = null - - @EpoxyAttribute - var avatarCallback: TimelineEventController.AvatarCallback? = null - - @EpoxyAttribute - var readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null + lateinit var attributes: Attributes private val _avatarClickListener = DebouncedClickListener(View.OnClickListener { - avatarCallback?.onAvatarClicked(informationData) + attributes.avatarCallback?.onAvatarClicked(attributes.informationData) }) private val _memberNameClickListener = DebouncedClickListener(View.OnClickListener { - avatarCallback?.onMemberNameClicked(informationData) + attributes.avatarCallback?.onMemberNameClicked(attributes.informationData) }) private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener { - readReceiptsCallback?.onReadReceiptsClicked(informationData.readReceipts) + attributes.readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts) }) + private val _readMarkerCallback = object : ReadMarkerView.Callback { + override fun onReadMarkerDisplayed() { + attributes.readReceiptsCallback?.onReadMarkerLongDisplayed(attributes.informationData) + } + } + var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener { override fun onReacted(reactionButton: ReactionButton) { - reactionPillCallback?.onClickOnReactionPill(informationData, reactionButton.reactionString, true) + attributes.reactionPillCallback?.onClickOnReactionPill(attributes.informationData, reactionButton.reactionString, true) } override fun onUnReacted(reactionButton: ReactionButton) { - reactionPillCallback?.onClickOnReactionPill(informationData, reactionButton.reactionString, false) + attributes.reactionPillCallback?.onClickOnReactionPill(attributes.informationData, reactionButton.reactionString, false) } override fun onLongClick(reactionButton: ReactionButton) { - reactionPillCallback?.onLongClickOnReactionPill(informationData, reactionButton.reactionString) + attributes.reactionPillCallback?.onLongClickOnReactionPill(attributes.informationData, reactionButton.reactionString) } } override fun bind(holder: H) { super.bind(holder) - if (informationData.showInformation) { + if (attributes.informationData.showInformation) { holder.avatarImageView.layoutParams = holder.avatarImageView.layoutParams?.apply { val size = dpToPx(avatarStyle.avatarSizeDP, holder.view.context) height = size @@ -110,13 +90,13 @@ abstract class AbsMessageItem : BaseEventItem() { holder.memberNameView.visibility = View.VISIBLE holder.memberNameView.setOnClickListener(_memberNameClickListener) holder.timeView.visibility = View.VISIBLE - holder.timeView.text = informationData.time - holder.memberNameView.text = informationData.memberName - avatarRenderer.render(informationData.avatarUrl, informationData.senderId, informationData.memberName?.toString(), holder.avatarImageView) - holder.view.setOnClickListener(cellClickListener) - holder.view.setOnLongClickListener(longClickListener) - holder.avatarImageView.setOnLongClickListener(longClickListener) - holder.memberNameView.setOnLongClickListener(longClickListener) + holder.timeView.text = attributes.informationData.time + holder.memberNameView.text = attributes.informationData.memberName + attributes.avatarRenderer.render(attributes.informationData.avatarUrl, attributes.informationData.senderId, attributes.informationData.memberName?.toString(), holder.avatarImageView) + holder.view.setOnClickListener(attributes.itemClickListener) + holder.view.setOnLongClickListener(attributes.itemLongClickListener) + holder.avatarImageView.setOnLongClickListener(attributes.itemLongClickListener) + holder.memberNameView.setOnLongClickListener(attributes.itemLongClickListener) } else { holder.avatarImageView.setOnClickListener(null) holder.memberNameView.setOnClickListener(null) @@ -128,11 +108,10 @@ abstract class AbsMessageItem : BaseEventItem() { holder.avatarImageView.setOnLongClickListener(null) holder.memberNameView.setOnLongClickListener(null) } + holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener) + holder.readMarkerView.bindView(attributes.informationData, _readMarkerCallback) - holder.readMarkerView.isVisible = informationData.displayReadMarker - holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener) - - if (!shouldShowReactionAtBottom() || informationData.orderedReactionList.isNullOrEmpty()) { + if (!shouldShowReactionAtBottom() || attributes.informationData.orderedReactionList.isNullOrEmpty()) { holder.reactionWrapper?.isVisible = false } else { //inflate if needed @@ -144,7 +123,7 @@ abstract class AbsMessageItem : BaseEventItem() { //clear all reaction buttons (but not the Flow helper!) holder.reactionWrapper?.children?.forEach { (it as? ReactionButton)?.isGone = true } val idToRefInFlow = ArrayList() - informationData.orderedReactionList?.chunked(8)?.firstOrNull()?.forEachIndexed { index, reaction -> + attributes.informationData.orderedReactionList?.chunked(8)?.firstOrNull()?.forEachIndexed { index, reaction -> (holder.reactionWrapper?.children?.elementAtOrNull(index) as? ReactionButton)?.let { reactionButton -> reactionButton.isVisible = true reactionButton.reactedListener = reactionClickListener @@ -152,7 +131,7 @@ abstract class AbsMessageItem : BaseEventItem() { idToRefInFlow.add(reactionButton.id) reactionButton.reactionString = reaction.key reactionButton.reactionCount = reaction.count - reactionButton.emojiTypeFace = emojiTypeFace + reactionButton.emojiTypeFace = attributes.emojiTypeFace reactionButton.setChecked(reaction.addedByMe) reactionButton.isEnabled = reaction.synced } @@ -163,27 +142,48 @@ abstract class AbsMessageItem : BaseEventItem() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && !holder.view.isInLayout) { holder.reactionFlowHelper?.requestLayout() } - holder.reactionWrapper?.setOnLongClickListener(longClickListener) + holder.reactionWrapper?.setOnLongClickListener(attributes.itemLongClickListener) } } + override fun unbind(holder: H) { + holder.readMarkerView.unbind() + super.unbind(holder) + } + open fun shouldShowReactionAtBottom(): Boolean { return true } protected open fun renderSendState(root: View, textView: TextView?, failureIndicator: ImageView? = null) { - root.isClickable = informationData.sendState.isSent() - val state = if (informationData.hasPendingEdits) SendState.UNSENT else informationData.sendState - textView?.setTextColor(colorProvider.getMessageTextColor(state)) - failureIndicator?.isVisible = informationData.sendState.hasFailed() + root.isClickable = attributes.informationData.sendState.isSent() + val state = if (attributes.informationData.hasPendingEdits) SendState.UNSENT else attributes.informationData.sendState + textView?.setTextColor(attributes.colorProvider.getMessageTextColor(state)) + failureIndicator?.isVisible = attributes.informationData.sendState.hasFailed() } + /** + * This class holds all the common attributes for message items. + */ + data class Attributes( + val informationData: MessageInformationData, + val avatarRenderer: AvatarRenderer, + val colorProvider: ColorProvider, + val itemLongClickListener: View.OnLongClickListener? = null, + val itemClickListener: View.OnClickListener? = null, + val memberClickListener: View.OnClickListener? = null, + val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null, + val avatarCallback: TimelineEventController.AvatarCallback? = null, + val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null, + val emojiTypeFace: Typeface? = null + ) + abstract class Holder(@IdRes stubId: Int) : BaseHolder(stubId) { val avatarImageView by bind(R.id.messageAvatarImageView) val memberNameView by bind(R.id.messageMemberNameView) val timeView by bind(R.id.messageTimeView) val readReceiptsView by bind(R.id.readReceiptsView) - val readMarkerView by bind(R.id.readMarkerView) + val readMarkerView by bind(R.id.readMarkerView) var reactionWrapper: ViewGroup? = null var reactionFlowHelper: Flow? = null } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt index 843f52b34c..5621f6047a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt @@ -24,7 +24,10 @@ import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.platform.CheckableView +import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.utils.DimensionUtils.dpToPx +import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController /** * Children must override getViewType() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt index 6f713b17fe..94e4835862 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt @@ -43,21 +43,21 @@ abstract class MessageImageVideoItem : AbsMessageItem() { holder.messageView.setTextFuture(textFuture) renderSendState(holder.messageView, holder.messageView) - holder.messageView.setOnClickListener(cellClickListener) - holder.messageView.setOnLongClickListener(longClickListener) + holder.messageView.setOnClickListener(attributes.itemClickListener) + holder.messageView.setOnLongClickListener(attributes.itemLongClickListener) findPillsAndProcess { it.bind(holder.messageView) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt index dd42dc7b66..aad090db05 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt @@ -19,10 +19,10 @@ package im.vector.riotx.features.home.room.detail.timeline.item import android.view.View import android.widget.ImageView import android.widget.TextView -import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R +import im.vector.riotx.core.ui.views.ReadMarkerView import im.vector.riotx.core.ui.views.ReadReceiptsView import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.features.home.AvatarRenderer @@ -54,6 +54,12 @@ abstract class NoticeItem : BaseEventItem() { readReceiptsCallback?.onReadReceiptsClicked(informationData.readReceipts) }) + private val _readMarkerCallback = object : ReadMarkerView.Callback { + override fun onReadMarkerDisplayed() { + readReceiptsCallback?.onReadMarkerLongDisplayed(informationData) + } + } + override fun bind(holder: Holder) { super.bind(holder) holder.noticeTextView.text = noticeText @@ -61,12 +67,17 @@ abstract class NoticeItem : BaseEventItem() { informationData.avatarUrl, informationData.senderId, informationData.memberName?.toString() - ?: informationData.senderId, + ?: informationData.senderId, holder.avatarImageView ) holder.view.setOnLongClickListener(longClickListener) holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener) - holder.readMarkerView.isVisible = informationData.displayReadMarker + holder.readMarkerView.bindView(informationData, _readMarkerCallback) + } + + override fun unbind(holder: Holder) { + holder.readMarkerView.unbind() + super.unbind(holder) } override fun getViewType() = STUB_ID @@ -75,7 +86,7 @@ abstract class NoticeItem : BaseEventItem() { val avatarImageView by bind(R.id.itemNoticeAvatarView) val noticeTextView by bind(R.id.itemNoticeTextView) val readReceiptsView by bind(R.id.readReceiptsView) - val readMarkerView by bind(R.id.readMarkerView) + val readMarkerView by bind(R.id.readMarkerView) } companion object { diff --git a/vector/src/main/res/anim/unread_marker_anim.xml b/vector/src/main/res/anim/unread_marker_anim.xml index 0c7ddab398..9e61c80c9d 100644 --- a/vector/src/main/res/anim/unread_marker_anim.xml +++ b/vector/src/main/res/anim/unread_marker_anim.xml @@ -1,7 +1,6 @@ \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_room_detail.xml b/vector/src/main/res/layout/fragment_room_detail.xml index 1d37628210..d7ce9963a0 100644 --- a/vector/src/main/res/layout/fragment_room_detail.xml +++ b/vector/src/main/res/layout/fragment_room_detail.xml @@ -6,6 +6,29 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + + + + + + - - - - - + - - - + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/syncProgressBarWrap" /> + + - \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml index ea4cfd5d4a..4eb9be0b9f 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -128,17 +128,20 @@ android:layout_height="wrap_content" android:layout_marginEnd="8dp" android:layout_marginBottom="4dp" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@+id/readMarkerView" app:layout_constraintEnd_toEndOf="parent" /> - + app:layout_constraintStart_toStartOf="parent" /> \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml index ad6999c5ee..fc4a527d03 100644 --- a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml +++ b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml @@ -58,18 +58,21 @@ android:layout_height="wrap_content" android:layout_marginEnd="8dp" android:layout_marginBottom="4dp" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@+id/readMarkerView" app:layout_constraintEnd_toEndOf="parent" /> - + app:layout_constraintStart_toStartOf="parent" /> \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_merged_header_stub.xml b/vector/src/main/res/layout/item_timeline_event_merged_header_stub.xml index 1dd5a61104..46c84aa4e7 100644 --- a/vector/src/main/res/layout/item_timeline_event_merged_header_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_merged_header_stub.xml @@ -40,7 +40,7 @@ android:layout_width="0dp" android:layout_height="1dp" android:layout_marginTop="4dp" - android:background="?attr/colorAccent" + android:background="?attr/riotx_header_panel_background" app:layout_constraintEnd_toEndOf="@id/itemMergedExpandTextView" app:layout_constraintStart_toStartOf="@id/itemMergedAvatarListView" app:layout_constraintTop_toBottomOf="@id/itemMergedExpandTextView" /> diff --git a/vector/src/main/res/layout/view_jump_to_read_marker.xml b/vector/src/main/res/layout/view_jump_to_read_marker.xml new file mode 100644 index 0000000000..35e14a649d --- /dev/null +++ b/vector/src/main/res/layout/view_jump_to_read_marker.xml @@ -0,0 +1,41 @@ + + + + + + + + diff --git a/vector/src/main/res/layout/view_read_marker.xml b/vector/src/main/res/layout/view_read_marker.xml new file mode 100644 index 0000000000..e3cbc6ba06 --- /dev/null +++ b/vector/src/main/res/layout/view_read_marker.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + From b8ebe3570bb55733c8eedfbc8b399f1af835e029 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 11 Sep 2019 18:04:17 +0200 Subject: [PATCH 003/197] Timeline: refact epoxy attributes --- .../timeline/TimelineEventController.kt | 59 ++-------- .../timeline/factory/DefaultItemFactory.kt | 7 +- .../timeline/factory/EncryptedItemFactory.kt | 6 +- .../factory/MergedHeaderItemFactory.kt | 103 ++++++++++++++++++ .../timeline/factory/MessageItemFactory.kt | 45 +++++--- .../timeline/factory/NoticeItemFactory.kt | 24 ++-- .../timeline/helper/AvatarSizeProvider.kt | 46 ++++++++ .../helper/MessageItemAttributesFactory.kt | 7 +- .../detail/timeline/item/AbsMessageItem.kt | 28 ++--- .../detail/timeline/item/BaseEventItem.kt | 22 +--- .../timeline/item/EventItemAttributes.kt | 18 +++ .../detail/timeline/item/MergedHeaderItem.kt | 42 ++++--- .../room/detail/timeline/item/NoticeItem.kt | 40 ++++--- 13 files changed, 286 insertions(+), 161 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/AvatarSizeProvider.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/EventItemAttributes.kt diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index ffc573a634..9b9172a6f9 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -32,11 +32,13 @@ import im.vector.riotx.core.epoxy.LoadingItem_ import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.core.resources.UserPreferencesProvider import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.home.room.detail.timeline.factory.MergedHeaderItemFactory import im.vector.riotx.features.home.room.detail.timeline.factory.TimelineItemFactory import im.vector.riotx.features.home.room.detail.timeline.helper.* import im.vector.riotx.features.home.room.detail.timeline.item.DaySeparatorItem import im.vector.riotx.features.home.room.detail.timeline.item.DaySeparatorItem_ import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem +import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem_ import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.riotx.features.media.ImageContentRenderer @@ -47,7 +49,7 @@ import javax.inject.Inject class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter, private val timelineItemFactory: TimelineItemFactory, private val timelineMediaSizeProvider: TimelineMediaSizeProvider, - private val avatarRenderer: AvatarRenderer, + private val mergedHeaderItemFactory: MergedHeaderItemFactory, @TimelineEventControllerHandler private val backgroundHandler: Handler ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener { @@ -89,8 +91,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec fun onUrlLongClicked(url: String): Boolean } - private val collapsedEventIds = linkedSetOf() - private val mergeItemCollapseStates = HashMap() private val modelCache = arrayListOf() private var currentSnapshot: List = emptyList() @@ -231,7 +231,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } return modelCache .map { - val eventModel = if (it == null || collapsedEventIds.contains(it.localId)) { + val eventModel = if (it == null || mergedHeaderItemFactory.isCollapsed(it.localId)) { null } else { it.eventModel @@ -255,7 +255,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec it.id(event.localId) it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) } - val mergedHeaderModel = buildMergedHeaderItem(event, nextEvent, items, addDaySeparator, currentPosition) + val mergedHeaderModel = mergedHeaderItemFactory.create(event, nextEvent, items, addDaySeparator, currentPosition, callback){ + requestModelBuild() + } val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date) return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem) @@ -270,53 +272,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } } - // TODO Phase 3 Handle the case where the eventId we have to highlight is merged - private fun buildMergedHeaderItem(event: TimelineEvent, - nextEvent: TimelineEvent?, - items: List, - addDaySeparator: Boolean, - currentPosition: Int): MergedHeaderItem? { - return if (!event.canBeMerged() || (nextEvent?.root?.getClearType() == event.root.getClearType() && !addDaySeparator)) { - null - } else { - val prevSameTypeEvents = items.prevSameTypeEvents(currentPosition, 2) - if (prevSameTypeEvents.isEmpty()) { - null - } else { - val mergedEvents = (prevSameTypeEvents + listOf(event)).asReversed() - val mergedData = mergedEvents.map { mergedEvent -> - val senderAvatar = mergedEvent.senderAvatar() - val senderName = mergedEvent.senderName() - MergedHeaderItem.Data( - userId = mergedEvent.root.senderId ?: "", - avatarUrl = senderAvatar, - memberName = senderName ?: "", - eventId = mergedEvent.localId - ) - } - val mergedEventIds = mergedEvents.map { it.localId } - // We try to find if one of the item id were used as mergeItemCollapseStates key - // => handle case where paginating from mergeable events and we get more - val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull() - val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey) - ?: true - val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState } - if (isCollapsed) { - collapsedEventIds.addAll(mergedEventIds) - } else { - collapsedEventIds.removeAll(mergedEventIds) - } - val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() } - MergedHeaderItem(isCollapsed, mergeId, mergedData, avatarRenderer) { - mergeItemCollapseStates[event.localId] = it - requestModelBuild() - }.also { - it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents)) - } - } - } - } - /** * Return true if added */ diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt index 05e4007e04..fbe77d6ac2 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt @@ -17,11 +17,12 @@ package im.vector.riotx.features.home.room.detail.timeline.factory import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.riotx.features.home.room.detail.timeline.item.DefaultItem import im.vector.riotx.features.home.room.detail.timeline.item.DefaultItem_ import javax.inject.Inject -class DefaultItemFactory @Inject constructor(){ +class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: AvatarSizeProvider) { fun create(event: TimelineEvent, highlight: Boolean, exception: Exception? = null): DefaultItem? { val text = if (exception == null) { @@ -30,8 +31,10 @@ class DefaultItemFactory @Inject constructor(){ "an exception occurred when rendering the event ${event.root.eventId}" } return DefaultItem_() - .text(text) + .leftGuideline(avatarSizeProvider.leftGuideline) .highlighted(highlight) + .text(text) + } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt index 938ac4673e..82ac4dc4d2 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt @@ -23,7 +23,6 @@ import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.StringProvider -import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem_ import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory @@ -55,7 +54,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat } val message = stringProvider.getString(R.string.encrypted_message).takeIf { cryptoError == null } - ?: stringProvider.getString(R.string.notice_crypto_unable_to_decrypt, errorDescription) + ?: stringProvider.getString(R.string.notice_crypto_unable_to_decrypt, errorDescription) val spannableStr = span(message) { textStyle = "italic" textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) @@ -66,11 +65,10 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat val informationData = messageInformationDataFactory.create(event, nextEvent) val attributes = attributesFactory.create(null, informationData, callback) return MessageTextItem_() + .highlighted(highlight) .attributes(attributes) .message(spannableStr) - .highlighted(highlight) .urlClickCallback(callback) - } else -> null } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt new file mode 100644 index 0000000000..06514e5973 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -0,0 +1,103 @@ +/* + * Copyright 2019 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.riotx.features.home.room.detail.timeline.factory + +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController +import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider +import im.vector.riotx.features.home.room.detail.timeline.helper.MergedTimelineEventVisibilityStateChangedListener +import im.vector.riotx.features.home.room.detail.timeline.helper.canBeMerged +import im.vector.riotx.features.home.room.detail.timeline.helper.prevSameTypeEvents +import im.vector.riotx.features.home.room.detail.timeline.helper.senderAvatar +import im.vector.riotx.features.home.room.detail.timeline.helper.senderName +import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem +import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem_ +import javax.inject.Inject + +class MergedHeaderItemFactory @Inject constructor(private val avatarRenderer: AvatarRenderer, + private val avatarSizeProvider: AvatarSizeProvider) { + + private val collapsedEventIds = linkedSetOf() + private val mergeItemCollapseStates = HashMap() + + fun create(event: TimelineEvent, + nextEvent: TimelineEvent?, + items: List, + addDaySeparator: Boolean, + currentPosition: Int, + callback: TimelineEventController.Callback?, + requestModelBuild: () -> Unit) + : MergedHeaderItem? { + + return if (!event.canBeMerged() || (nextEvent?.root?.getClearType() == event.root.getClearType() && !addDaySeparator)) { + null + } else { + val prevSameTypeEvents = items.prevSameTypeEvents(currentPosition, 2) + if (prevSameTypeEvents.isEmpty()) { + null + } else { + val mergedEvents = (prevSameTypeEvents + listOf(event)).asReversed() + val mergedData = mergedEvents.map { mergedEvent -> + val senderAvatar = mergedEvent.senderAvatar() + val senderName = mergedEvent.senderName() + MergedHeaderItem.Data( + userId = mergedEvent.root.senderId ?: "", + avatarUrl = senderAvatar, + memberName = senderName ?: "", + eventId = mergedEvent.localId + ) + } + val mergedEventIds = mergedEvents.map { it.localId } + // We try to find if one of the item id were used as mergeItemCollapseStates key + // => handle case where paginating from mergeable events and we get more + val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull() + val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey) + ?: true + val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState } + if (isCollapsed) { + collapsedEventIds.addAll(mergedEventIds) + } else { + collapsedEventIds.removeAll(mergedEventIds) + } + val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() } + val attributes = MergedHeaderItem.Attributes( + isCollapsed = isCollapsed, + mergeData = mergedData, + avatarRenderer = avatarRenderer, + onCollapsedStateChanged = { + mergeItemCollapseStates[event.localId] = it + requestModelBuild() + } + ) + MergedHeaderItem_() + .id(mergeId) + .leftGuideline(avatarSizeProvider.leftGuideline) + .attributes(attributes) + .also { + it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents)) + } + } + } + } + + fun isCollapsed(localId: Long): Boolean { + return collapsedEventIds.contains(localId) + } + + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 57baf4fee8..24d5abfd94 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -41,16 +41,15 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent -import im.vector.riotx.EmojiCompatFontProvider import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.linkify.VectorLinkify import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.utils.DebouncedClickListener -import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder +import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider import im.vector.riotx.features.home.room.detail.timeline.item.* import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory @@ -70,7 +69,8 @@ class MessageItemFactory @Inject constructor( private val messageInformationDataFactory: MessageInformationDataFactory, private val messageItemAttributesFactory: MessageItemAttributesFactory, private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder, - private val noticeItemFactory: NoticeItemFactory) { + private val noticeItemFactory: NoticeItemFactory, + private val avatarSizeProvider: AvatarSizeProvider) { fun create(event: TimelineEvent, @@ -90,11 +90,11 @@ class MessageItemFactory @Inject constructor( val messageContent: MessageContent = event.getLastMessageContent() - ?: //Malformed content, we should echo something on screen - return DefaultItem_().text(stringProvider.getString(R.string.malformed_message)) + ?: //Malformed content, we should echo something on screen + return DefaultItem_().text(stringProvider.getString(R.string.malformed_message)) if (messageContent.relatesTo?.type == RelationType.REPLACE - || event.isEncrypted() && event.root.content.toModel()?.relatesTo?.type == RelationType.REPLACE + || event.isEncrypted() && event.root.content.toModel()?.relatesTo?.type == RelationType.REPLACE ) { // This is an edit event, we should it when debugging as a notice event return noticeItemFactory.create(event, highlight, callback) @@ -105,15 +105,15 @@ class MessageItemFactory @Inject constructor( // val ev = all.toModel() return when (messageContent) { is MessageEmoteContent -> buildEmoteMessageItem(messageContent, - informationData, - highlight, - callback, - attributes) + informationData, + highlight, + callback, + attributes) is MessageTextContent -> buildTextMessageItem(messageContent, - informationData, - highlight, - callback, - attributes) + informationData, + highlight, + callback, + attributes) is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes) @@ -131,6 +131,7 @@ class MessageItemFactory @Inject constructor( return MessageFileItem_() .attributes(attributes) .highlighted(highlight) + .leftGuideline(avatarSizeProvider.leftGuideline) .filename(messageContent.body) .iconRes(R.drawable.filetype_audio) .clickListener( @@ -146,6 +147,7 @@ class MessageItemFactory @Inject constructor( attributes: AbsMessageItem.Attributes): MessageFileItem? { return MessageFileItem_() .attributes(attributes) + .leftGuideline(avatarSizeProvider.leftGuideline) .highlighted(highlight) .filename(messageContent.body) .iconRes(R.drawable.filetype_attachment) @@ -158,6 +160,7 @@ class MessageItemFactory @Inject constructor( private fun buildNotHandledMessageItem(messageContent: MessageContent, highlight: Boolean): DefaultItem? { val text = "${messageContent.type} message events are not yet handled" return DefaultItem_() + .leftGuideline(avatarSizeProvider.leftGuideline) .text(text) .highlighted(highlight) } @@ -182,6 +185,7 @@ class MessageItemFactory @Inject constructor( ) return MessageImageVideoItem_() .attributes(attributes) + .leftGuideline(avatarSizeProvider.leftGuideline) .imageContentRenderer(imageContentRenderer) .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) .playable(messageContent.info?.mimeType == "image/gif") @@ -203,7 +207,7 @@ class MessageItemFactory @Inject constructor( val thumbnailData = ImageContentRenderer.Data( filename = messageContent.body, url = messageContent.videoInfo?.thumbnailFile?.url - ?: messageContent.videoInfo?.thumbnailUrl, + ?: messageContent.videoInfo?.thumbnailUrl, elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(), height = messageContent.videoInfo?.height, maxHeight = maxHeight, @@ -220,6 +224,7 @@ class MessageItemFactory @Inject constructor( ) return MessageImageVideoItem_() + .leftGuideline(avatarSizeProvider.leftGuideline) .attributes(attributes) .imageContentRenderer(imageContentRenderer) .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) @@ -250,6 +255,7 @@ class MessageItemFactory @Inject constructor( message(linkifiedBody) } } + .leftGuideline(avatarSizeProvider.leftGuideline) .attributes(attributes) .highlighted(highlight) .urlClickCallback(callback) @@ -282,9 +288,9 @@ class MessageItemFactory @Inject constructor( //nop } }, - editStart, - editEnd, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + editStart, + editEnd, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE) return spannable } @@ -303,6 +309,7 @@ class MessageItemFactory @Inject constructor( linkifyBody(formattedBody, callback) } return MessageTextItem_() + .leftGuideline(avatarSizeProvider.leftGuideline) .attributes(attributes) .message(message) .highlighted(highlight) @@ -328,6 +335,7 @@ class MessageItemFactory @Inject constructor( message(message) } } + .leftGuideline(avatarSizeProvider.leftGuideline) .attributes(attributes) .highlighted(highlight) .urlClickCallback(callback) @@ -336,6 +344,7 @@ class MessageItemFactory @Inject constructor( private fun buildRedactedItem(attributes: AbsMessageItem.Attributes, highlight: Boolean): RedactedMessageItem? { return RedactedMessageItem_() + .leftGuideline(avatarSizeProvider.leftGuideline) .attributes(attributes) .highlighted(highlight) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt index 6955cf3593..8663f87409 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt @@ -20,28 +20,34 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter +import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem_ import javax.inject.Inject -class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEventFormatter, - private val avatarRenderer: AvatarRenderer, - private val informationDataFactory: MessageInformationDataFactory) { +class NoticeItemFactory @Inject constructor( + private val eventFormatter: NoticeEventFormatter, + private val avatarRenderer: AvatarRenderer, + private val informationDataFactory: MessageInformationDataFactory, + private val avatarSizeProvider: AvatarSizeProvider +) { fun create(event: TimelineEvent, highlight: Boolean, callback: TimelineEventController.Callback?): NoticeItem? { val formattedText = eventFormatter.format(event) ?: return null val informationData = informationDataFactory.create(event, null) - + val attributes = NoticeItem.Attributes( + avatarRenderer = avatarRenderer, + informationData = informationData, + noticeText = formattedText, + callback = callback + ) return NoticeItem_() - .avatarRenderer(avatarRenderer) - .noticeText(formattedText) + .leftGuideline(avatarSizeProvider.leftGuideline) .highlighted(highlight) - .informationData(informationData) - .baseCallback(callback) - .readReceiptsCallback(callback) + .attributes(attributes) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/AvatarSizeProvider.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/AvatarSizeProvider.kt new file mode 100644 index 0000000000..9fcfdcfdf6 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/AvatarSizeProvider.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019 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.riotx.features.home.room.detail.timeline.helper + +import androidx.appcompat.app.AppCompatActivity +import im.vector.riotx.core.utils.DimensionUtils.dpToPx +import javax.inject.Inject + +class AvatarSizeProvider @Inject constructor(private val context: AppCompatActivity) { + + private val avatarStyle = AvatarStyle.SMALL + + val leftGuideline: Int by lazy { + dpToPx(avatarStyle.avatarSizeDP + 8, context) + } + + val avatarSize: Int by lazy { + dpToPx(avatarStyle.avatarSizeDP, context) + } + + companion object { + + enum class AvatarStyle(val avatarSizeDP: Int) { + BIG(50), + MEDIUM(40), + SMALL(30), + NONE(0) + } + + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt index 47b5094c95..d69676cb2f 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt @@ -31,10 +31,15 @@ import javax.inject.Inject class MessageItemAttributesFactory @Inject constructor( private val avatarRenderer: AvatarRenderer, private val colorProvider: ColorProvider, + private val avatarSizeProvider: AvatarSizeProvider, private val emojiCompatFontProvider: EmojiCompatFontProvider) { - fun create(messageContent: MessageContent?, informationData: MessageInformationData, callback: TimelineEventController.Callback?): AbsMessageItem.Attributes { + fun create(messageContent: MessageContent?, + informationData: MessageInformationData, + callback: TimelineEventController.Callback?): AbsMessageItem.Attributes { + return AbsMessageItem.Attributes( + avatarSize = avatarSizeProvider.avatarSize, informationData = informationData, avatarRenderer = avatarRenderer, colorProvider = colorProvider, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt index 5431b4ca85..913b1be466 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -81,9 +81,8 @@ abstract class AbsMessageItem : BaseEventItem() { super.bind(holder) if (attributes.informationData.showInformation) { holder.avatarImageView.layoutParams = holder.avatarImageView.layoutParams?.apply { - val size = dpToPx(avatarStyle.avatarSizeDP, holder.view.context) - height = size - width = size + height = attributes.avatarSize + width = attributes.avatarSize } holder.avatarImageView.visibility = View.VISIBLE holder.avatarImageView.setOnClickListener(_avatarClickListener) @@ -162,10 +161,21 @@ abstract class AbsMessageItem : BaseEventItem() { failureIndicator?.isVisible = attributes.informationData.sendState.hasFailed() } + abstract class Holder(@IdRes stubId: Int) : BaseHolder(stubId) { + val avatarImageView by bind(R.id.messageAvatarImageView) + val memberNameView by bind(R.id.messageMemberNameView) + val timeView by bind(R.id.messageTimeView) + val readReceiptsView by bind(R.id.readReceiptsView) + val readMarkerView by bind(R.id.readMarkerView) + var reactionWrapper: ViewGroup? = null + var reactionFlowHelper: Flow? = null + } + /** - * This class holds all the common attributes for message items. + * This class holds all the common attributes for timeline items. */ data class Attributes( + val avatarSize: Int, val informationData: MessageInformationData, val avatarRenderer: AvatarRenderer, val colorProvider: ColorProvider, @@ -178,14 +188,4 @@ abstract class AbsMessageItem : BaseEventItem() { val emojiTypeFace: Typeface? = null ) - abstract class Holder(@IdRes stubId: Int) : BaseHolder(stubId) { - val avatarImageView by bind(R.id.messageAvatarImageView) - val memberNameView by bind(R.id.messageMemberNameView) - val timeView by bind(R.id.messageTimeView) - val readReceiptsView by bind(R.id.readReceiptsView) - val readMarkerView by bind(R.id.readMarkerView) - var reactionWrapper: ViewGroup? = null - var reactionFlowHelper: Flow? = null - } - } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt index 5621f6047a..efdc6d06c6 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt @@ -24,28 +24,23 @@ import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.platform.CheckableView -import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.utils.DimensionUtils.dpToPx -import im.vector.riotx.features.home.AvatarRenderer -import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController +import org.w3c.dom.Attr /** * Children must override getViewType() */ abstract class BaseEventItem : VectorEpoxyModel() { - var avatarStyle: AvatarStyle = AvatarStyle.SMALL - // To use for instance when opening a permalink with an eventId @EpoxyAttribute var highlighted: Boolean = false + @EpoxyAttribute + open var leftGuideline: Int = 0 override fun bind(holder: H) { super.bind(holder) - //optimize? - val px = dpToPx(avatarStyle.avatarSizeDP + 8, holder.view.context) - holder.leftGuideline.setGuidelineBegin(px) - + holder.leftGuideline.setGuidelineBegin(leftGuideline) holder.checkableBackground.isChecked = highlighted } @@ -63,13 +58,4 @@ abstract class BaseEventItem : VectorEpoxyModel } } - companion object { - - enum class AvatarStyle(val avatarSizeDP: Int) { - BIG(50), - MEDIUM(40), - SMALL(30), - NONE(0) - } - } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/EventItemAttributes.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/EventItemAttributes.kt new file mode 100644 index 0000000000..5c1ab5b347 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/EventItemAttributes.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2019 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.riotx.features.home.room.detail.timeline.item + diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt index 4f26f9bb11..a36d9ab7cd 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt @@ -21,29 +21,20 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.core.view.children +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController +@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo) +abstract class MergedHeaderItem : BaseEventItem() { -data class MergedHeaderItem(private val isCollapsed: Boolean, - private val mergeId: String, - private val mergeData: List, - private val avatarRenderer: AvatarRenderer, - private val onCollapsedStateChanged: (Boolean) -> Unit -) : BaseEventItem() { + @EpoxyAttribute + lateinit var attributes: Attributes - private val distinctMergeData = mergeData.distinctBy { it.userId } - - init { - id(mergeId) - } - - override fun getDefaultLayout(): Int { - return R.layout.item_timeline_event_base_noinfo - } - - override fun createNewHolder(): Holder { - return Holder() + private val distinctMergeData by lazy { + attributes.mergeData.distinctBy { it.userId } } override fun getViewType() = STUB_ID @@ -51,10 +42,10 @@ data class MergedHeaderItem(private val isCollapsed: Boolean, override fun bind(holder: Holder) { super.bind(holder) holder.expandView.setOnClickListener { - onCollapsedStateChanged(!isCollapsed) + attributes.onCollapsedStateChanged(!attributes.isCollapsed) } - if (isCollapsed) { - val summary = holder.expandView.resources.getQuantityString(R.plurals.membership_changes, mergeData.size, mergeData.size) + if (attributes.isCollapsed) { + val summary = holder.expandView.resources.getQuantityString(R.plurals.membership_changes, attributes.mergeData.size, attributes.mergeData.size) holder.summaryView.text = summary holder.summaryView.visibility = View.VISIBLE holder.avatarListView.visibility = View.VISIBLE @@ -62,7 +53,7 @@ data class MergedHeaderItem(private val isCollapsed: Boolean, val data = distinctMergeData.getOrNull(index) if (data != null && view is ImageView) { view.visibility = View.VISIBLE - avatarRenderer.render(data.avatarUrl, data.userId, data.memberName, view) + attributes.avatarRenderer.render(data.avatarUrl, data.userId, data.memberName, view) } else { view.visibility = View.GONE } @@ -84,6 +75,13 @@ data class MergedHeaderItem(private val isCollapsed: Boolean, val avatarUrl: String? ) + data class Attributes( + val isCollapsed: Boolean, + val mergeData: List, + val avatarRenderer: AvatarRenderer, + val onCollapsedStateChanged: (Boolean) -> Unit + ) + class Holder : BaseHolder(STUB_ID) { val expandView by bind(R.id.itemMergedExpandTextView) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt index aad090db05..568347e83e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt @@ -32,47 +32,38 @@ import im.vector.riotx.features.home.room.detail.timeline.TimelineEventControlle abstract class NoticeItem : BaseEventItem() { @EpoxyAttribute - lateinit var avatarRenderer: AvatarRenderer - - @EpoxyAttribute - var noticeText: CharSequence? = null - - @EpoxyAttribute - lateinit var informationData: MessageInformationData - - @EpoxyAttribute - var baseCallback: TimelineEventController.BaseCallback? = null + lateinit var attributes: Attributes private var longClickListener = View.OnLongClickListener { - return@OnLongClickListener baseCallback?.onEventLongClicked(informationData, null, it) == true + return@OnLongClickListener attributes.callback?.onEventLongClicked(attributes.informationData, null, it) == true } @EpoxyAttribute var readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener { - readReceiptsCallback?.onReadReceiptsClicked(informationData.readReceipts) + readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts) }) private val _readMarkerCallback = object : ReadMarkerView.Callback { override fun onReadMarkerDisplayed() { - readReceiptsCallback?.onReadMarkerLongDisplayed(informationData) + readReceiptsCallback?.onReadMarkerLongDisplayed(attributes.informationData) } } override fun bind(holder: Holder) { super.bind(holder) - holder.noticeTextView.text = noticeText - avatarRenderer.render( - informationData.avatarUrl, - informationData.senderId, - informationData.memberName?.toString() - ?: informationData.senderId, + holder.noticeTextView.text = attributes.noticeText + attributes.avatarRenderer.render( + attributes.informationData.avatarUrl, + attributes.informationData.senderId, + attributes.informationData.memberName?.toString() + ?: attributes.informationData.senderId, holder.avatarImageView ) holder.view.setOnLongClickListener(longClickListener) - holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener) - holder.readMarkerView.bindView(informationData, _readMarkerCallback) + holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener) + holder.readMarkerView.bindView(attributes.informationData, _readMarkerCallback) } override fun unbind(holder: Holder) { @@ -89,6 +80,13 @@ abstract class NoticeItem : BaseEventItem() { val readMarkerView by bind(R.id.readMarkerView) } + data class Attributes( + val avatarRenderer: AvatarRenderer, + val informationData: MessageInformationData, + val noticeText: CharSequence, + val callback: TimelineEventController.BaseCallback? = null + ) + companion object { private const val STUB_ID = R.id.messageContentNoticeStub } From d4111d053d3063919b98906a7ac8e1f53c945436 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 12 Sep 2019 16:35:45 +0200 Subject: [PATCH 004/197] Read marker: only show banner until scrolled to read marker --- .../home/room/detail/RoomDetailViewModel.kt | 4 ++-- .../timeline/factory/EncryptedItemFactory.kt | 3 +++ .../detail/timeline/factory/NoticeItemFactory.kt | 6 +++++- .../home/room/detail/timeline/item/NoticeItem.kt | 16 +++++----------- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index bbdb7ab619..62c067159e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -637,7 +637,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro .combineLatest( room.rx().liveRoomSummary(), visibleEventsObservable.distinctUntilChanged(), - isEventVisibleObservable { it.hasReadMarker }.startWith(false), + isEventVisibleObservable { it.hasReadMarker }.startWith(false).takeUntil { it }, Function3 { roomSummary, currentVisibleEvent, isReadMarkerViewVisible -> val readMarkerId = roomSummary.readMarkerId if (readMarkerId == null || isReadMarkerViewVisible || !timeline.isLive) { @@ -646,7 +646,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro val readMarkerPosition = timeline.getIndexOfEvent(readMarkerId) ?: Int.MAX_VALUE val currentVisibleEventPosition = timeline.getIndexOfEvent(currentVisibleEvent.event.root.eventId) - ?: Int.MIN_VALUE + ?: Int.MAX_VALUE readMarkerPosition > currentVisibleEventPosition } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt index 82ac4dc4d2..92f586ab7b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt @@ -24,6 +24,7 @@ import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController +import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem_ import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory @@ -34,6 +35,7 @@ import javax.inject.Inject class EncryptedItemFactory @Inject constructor(private val messageInformationDataFactory: MessageInformationDataFactory, private val colorProvider: ColorProvider, private val stringProvider: StringProvider, + private val avatarSizeProvider: AvatarSizeProvider, private val attributesFactory: MessageItemAttributesFactory) { fun create(event: TimelineEvent, @@ -65,6 +67,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat val informationData = messageInformationDataFactory.create(event, nextEvent) val attributes = attributesFactory.create(null, informationData, callback) return MessageTextItem_() + .leftGuideline(avatarSizeProvider.leftGuideline) .highlighted(highlight) .attributes(attributes) .message(spannableStr) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt index 8663f87409..bb301cdcbd 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt @@ -16,6 +16,7 @@ package im.vector.riotx.features.home.room.detail.timeline.factory +import android.view.View import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController @@ -42,7 +43,10 @@ class NoticeItemFactory @Inject constructor( avatarRenderer = avatarRenderer, informationData = informationData, noticeText = formattedText, - callback = callback + itemLongClickListener = View.OnLongClickListener { view -> + callback?.onEventLongClicked(informationData, null, view) ?: false + }, + readReceiptsCallback = callback ) return NoticeItem_() .leftGuideline(avatarSizeProvider.leftGuideline) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt index 568347e83e..b6585ba6f8 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt @@ -34,20 +34,13 @@ abstract class NoticeItem : BaseEventItem() { @EpoxyAttribute lateinit var attributes: Attributes - private var longClickListener = View.OnLongClickListener { - return@OnLongClickListener attributes.callback?.onEventLongClicked(attributes.informationData, null, it) == true - } - - @EpoxyAttribute - var readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null - private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener { - readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts) + attributes.readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts) }) private val _readMarkerCallback = object : ReadMarkerView.Callback { override fun onReadMarkerDisplayed() { - readReceiptsCallback?.onReadMarkerLongDisplayed(attributes.informationData) + attributes.readReceiptsCallback?.onReadMarkerLongDisplayed(attributes.informationData) } } @@ -61,7 +54,7 @@ abstract class NoticeItem : BaseEventItem() { ?: attributes.informationData.senderId, holder.avatarImageView ) - holder.view.setOnLongClickListener(longClickListener) + holder.view.setOnLongClickListener(attributes.itemLongClickListener) holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener) holder.readMarkerView.bindView(attributes.informationData, _readMarkerCallback) } @@ -84,7 +77,8 @@ abstract class NoticeItem : BaseEventItem() { val avatarRenderer: AvatarRenderer, val informationData: MessageInformationData, val noticeText: CharSequence, - val callback: TimelineEventController.BaseCallback? = null + val itemLongClickListener: View.OnLongClickListener? = null, + val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null ) companion object { From 5d6d0202a9ad61a9e3ddf4efe5486e85ce9a8c32 Mon Sep 17 00:00:00 2001 From: ganfra Date: Sat, 14 Sep 2019 14:11:41 +0200 Subject: [PATCH 005/197] Timeline: try to fix some issues with permalink [WIP] --- .../session/room/timeline/DefaultTimeline.kt | 75 ++++++++++--------- .../matrix/android/internal/util/Handler.kt | 4 +- .../im/vector/riotx/core/utils/Debouncer.kt | 44 +++++++++++ .../im/vector/riotx/core/utils/Handler.kt | 31 ++++++++ .../home/createdirect/KnownUsersController.kt | 7 +- .../home/room/detail/RoomDetailFragment.kt | 65 ++++++++-------- .../ScrollOnHighlightedEventCallback.kt | 6 +- .../room/detail/ScrollOnNewMessageCallback.kt | 8 +- .../timeline/TimelineEventController.kt | 31 ++++---- 9 files changed, 178 insertions(+), 93 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt create mode 100644 vector/src/main/java/im/vector/riotx/core/utils/Handler.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index f14df5ada2..dd11b22b64 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -42,10 +42,11 @@ import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference import kotlin.collections.ArrayList import kotlin.collections.HashMap +import kotlin.math.max +import kotlin.math.min private const val MIN_FETCHING_COUNT = 30 -private const val DISPLAY_INDEX_UNKNOWN = Int.MIN_VALUE internal class DefaultTimeline( private val roomId: String, @@ -85,8 +86,8 @@ internal class DefaultTimeline( private var roomEntity: RoomEntity? = null - private var prevDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN - private var nextDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN + private var prevDisplayIndex: Int? = null + private var nextDisplayIndex: Int? = null private val builtEvents = Collections.synchronizedList(ArrayList()) private val builtEventsIdMap = Collections.synchronizedMap(HashMap()) private val backwardsPaginationState = AtomicReference(PaginationState()) @@ -222,6 +223,7 @@ internal class DefaultTimeline( if (isStarted.compareAndSet(true, false)) { eventDecryptor.destroy() Timber.v("Dispose timeline for roomId: $roomId and eventId: $initialEventId") + BACKGROUND_HANDLER.removeCallbacksAndMessages(null) BACKGROUND_HANDLER.post { cancelableBag.cancel() roomEntity?.sendingTimelineEvents?.removeAllChangeListeners() @@ -303,11 +305,8 @@ internal class DefaultTimeline( private fun hasMoreInCache(direction: Timeline.Direction): Boolean { return Realm.getInstance(realmConfiguration).use { localRealm -> val timelineEventEntity = buildEventQuery(localRealm).findFirst(direction) - ?: return false + ?: return false if (direction == Timeline.Direction.FORWARDS) { - if (findCurrentChunk(localRealm)?.isLastForward == true) { - return false - } val firstEvent = builtEvents.firstOrNull() ?: return true firstEvent.displayIndex < timelineEventEntity.root!!.displayIndex } else { @@ -334,16 +333,17 @@ internal class DefaultTimeline( * This has to be called on TimelineThread as it access realm live results * @return true if createSnapshot should be posted */ - private fun paginateInternal(startDisplayIndex: Int, + private fun paginateInternal(startDisplayIndex: Int?, direction: Timeline.Direction, - count: Int): Boolean { + count: Int, + strict: Boolean = false): Boolean { updatePaginationState(direction) { it.copy(requestedCount = count, isPaginating = true) } - val builtCount = buildTimelineEvents(startDisplayIndex, direction, count.toLong()) + val builtCount = buildTimelineEvents(startDisplayIndex, direction, count.toLong(), strict) val shouldFetchMore = builtCount < count && !hasReachedEnd(direction) if (shouldFetchMore) { val newRequestedCount = count - builtCount updatePaginationState(direction) { it.copy(requestedCount = newRequestedCount) } - val fetchingCount = Math.max(MIN_FETCHING_COUNT, newRequestedCount) + val fetchingCount = max(MIN_FETCHING_COUNT, newRequestedCount) executePaginationTask(direction, fetchingCount) } else { updatePaginationState(direction) { it.copy(isPaginating = false, requestedCount = 0) } @@ -404,20 +404,19 @@ internal class DefaultTimeline( .findFirst() shouldFetchInitialEvent = initialEvent == null initialEvent?.root?.displayIndex - } ?: DISPLAY_INDEX_UNKNOWN - + } prevDisplayIndex = initialDisplayIndex nextDisplayIndex = initialDisplayIndex val currentInitialEventId = initialEventId if (currentInitialEventId != null && shouldFetchInitialEvent) { fetchEvent(currentInitialEventId) } else { - val count = Math.min(settings.initialSize, liveEvents.size) + val count = min(settings.initialSize, liveEvents.size) if (isLive) { - paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count) + paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count, strict = false) } else { - paginateInternal(initialDisplayIndex, Timeline.Direction.FORWARDS, count / 2) - paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count / 2) + paginateInternal(initialDisplayIndex, Timeline.Direction.FORWARDS, count / 2, strict = false) + paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count / 2, strict = true) } } postSnapshot() @@ -429,9 +428,9 @@ internal class DefaultTimeline( private fun executePaginationTask(direction: Timeline.Direction, limit: Int) { val token = getTokenLive(direction) ?: return val params = PaginationTask.Params(roomId = roomId, - from = token, - direction = direction.toPaginationDirection(), - limit = limit) + from = token, + direction = direction.toPaginationDirection(), + limit = limit) Timber.v("Should fetch $limit items $direction") cancelableBag += paginationTask @@ -479,14 +478,15 @@ internal class DefaultTimeline( * This has to be called on TimelineThread as it access realm live results * @return number of items who have been added */ - private fun buildTimelineEvents(startDisplayIndex: Int, + private fun buildTimelineEvents(startDisplayIndex: Int?, direction: Timeline.Direction, - count: Long): Int { - if (count < 1) { + count: Long, + strict: Boolean = false): Int { + if (count < 1 || startDisplayIndex == null) { return 0 } val start = System.currentTimeMillis() - val offsetResults = getOffsetResults(startDisplayIndex, direction, count) + val offsetResults = getOffsetResults(startDisplayIndex, direction, count, strict) if (offsetResults.isEmpty()) { return 0 } @@ -501,7 +501,7 @@ internal class DefaultTimeline( val timelineEvent = buildTimelineEvent(eventEntity) if (timelineEvent.isEncrypted() - && timelineEvent.root.mxDecryptionResult == null) { + && timelineEvent.root.mxDecryptionResult == null) { timelineEvent.root.eventId?.let { eventDecryptor.requestDecryption(it) } } @@ -527,16 +527,23 @@ internal class DefaultTimeline( */ private fun getOffsetResults(startDisplayIndex: Int, direction: Timeline.Direction, - count: Long): RealmResults { + count: Long, + strict: Boolean): RealmResults { val offsetQuery = liveEvents.where() if (direction == Timeline.Direction.BACKWARDS) { - offsetQuery - .sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING) - .lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex) + offsetQuery.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING) + if (strict) { + offsetQuery.lessThan(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex) + } else { + offsetQuery.lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex) + } } else { - offsetQuery - .sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.ASCENDING) - .greaterThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex) + offsetQuery.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.ASCENDING) + if (strict) { + offsetQuery.greaterThan(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex) + } else { + offsetQuery.greaterThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex) + } } return offsetQuery .limit(count) @@ -589,8 +596,8 @@ internal class DefaultTimeline( } private fun clearAllValues() { - prevDisplayIndex = DISPLAY_INDEX_UNKNOWN - nextDisplayIndex = DISPLAY_INDEX_UNKNOWN + prevDisplayIndex = null + nextDisplayIndex = null builtEvents.clear() builtEventsIdMap.clear() backwardsPaginationState.set(PaginationState()) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Handler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Handler.kt index 51fdbfe227..e723a908cc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Handler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Handler.kt @@ -20,10 +20,10 @@ import android.os.Handler import android.os.HandlerThread import android.os.Looper -fun createBackgroundHandler(name: String): Handler = Handler( +internal fun createBackgroundHandler(name: String): Handler = Handler( HandlerThread(name).apply { start() }.looper ) -fun createUIHandler(): Handler = Handler( +internal fun createUIHandler(): Handler = Handler( Looper.getMainLooper() ) \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt b/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt new file mode 100644 index 0000000000..8c8bd1266f --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt @@ -0,0 +1,44 @@ +/* + + * Copyright 2019 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.riotx.core.utils + +import android.os.Handler + +internal class Debouncer(private val handler: Handler) { + + private val runnables = HashMap() + + fun debounce(identifier: String, millis: Long, r: Runnable): Boolean { + if (runnables.containsKey(identifier)) { + // debounce + val old = runnables[identifier] + handler.removeCallbacks(old) + } + insertRunnable(identifier, r, millis) + return true + } + + private fun insertRunnable(identifier: String, r: Runnable, millis: Long) { + val chained = Runnable { + handler.post(r) + runnables.remove(identifier) + } + runnables[identifier] = chained + handler.postDelayed(chained, millis) + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/core/utils/Handler.kt b/vector/src/main/java/im/vector/riotx/core/utils/Handler.kt new file mode 100644 index 0000000000..51316d7e2f --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/utils/Handler.kt @@ -0,0 +1,31 @@ +/* + + * Copyright 2019 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.riotx.core.utils + +import android.os.Handler +import android.os.HandlerThread +import android.os.Looper + +internal fun createBackgroundHandler(name: String): Handler = Handler( + HandlerThread(name).apply { start() }.looper +) + +internal fun createUIHandler(): Handler = Handler( + Looper.getMainLooper() +) \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/KnownUsersController.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/KnownUsersController.kt index fbb1cfcc4e..87fd32a784 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/KnownUsersController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/KnownUsersController.kt @@ -19,22 +19,17 @@ package im.vector.riotx.features.home.createdirect import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.paging.PagedListEpoxyController import com.airbnb.mvrx.Async -import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Incomplete -import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.user.model.User -import im.vector.matrix.android.internal.util.createUIHandler import im.vector.matrix.android.internal.util.firstLetterOfDisplayName import im.vector.riotx.R import im.vector.riotx.core.epoxy.EmptyItem_ -import im.vector.riotx.core.epoxy.errorWithRetryItem import im.vector.riotx.core.epoxy.loadingItem import im.vector.riotx.core.epoxy.noResultItem -import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.core.utils.createUIHandler import im.vector.riotx.features.home.AvatarRenderer import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index fd83a6f69e..61338e7858 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -202,6 +202,7 @@ class RoomDetailFragment : } } + private val roomDetailArgs: RoomDetailArgs by args() private val glideRequests by lazy { GlideApp.with(this) @@ -221,11 +222,13 @@ class RoomDetailFragment : @Inject lateinit var roomDetailViewModelFactory: RoomDetailViewModel.Factory @Inject lateinit var textComposerViewModelFactory: TextComposerViewModel.Factory @Inject lateinit var errorFormatter: ErrorFormatter - private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback - private lateinit var scrollOnHighlightedEventCallback: ScrollOnHighlightedEventCallback @Inject lateinit var eventHtmlRenderer: EventHtmlRenderer @Inject lateinit var vectorPreferences: VectorPreferences + private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback + private lateinit var scrollOnHighlightedEventCallback: ScrollOnHighlightedEventCallback + private lateinit var endlessScrollListener: EndlessRecyclerViewScrollListener + override fun getLayoutResId() = R.layout.fragment_room_detail @@ -374,17 +377,17 @@ class RoomDetailFragment : if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { val parser = Parser.builder().build() val document = parser.parse(messageContent.formattedBody - ?: messageContent.body) + ?: messageContent.body) formattedBody = eventHtmlRenderer.render(document) } composerLayout.composerRelatedMessageContent.text = formattedBody - ?: nonFormattedBody + ?: nonFormattedBody composerLayout.composerEditText.setText(if (useText) event.getTextEditableContent() else "") composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes)) avatarRenderer.render(event.senderAvatar, event.root.senderId - ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) + ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length) composerLayout.expand { @@ -413,9 +416,9 @@ class RoomDetailFragment : REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data) REACTION_SELECT_REQUEST_CODE -> { val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID) - ?: return + ?: return val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT) - ?: return + ?: return //TODO check if already reacted with that? roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId)) } @@ -430,7 +433,10 @@ class RoomDetailFragment : epoxyVisibilityTracker.attach(recyclerView) layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true) val stateRestorer = LayoutManagerStateRestorer(layoutManager).register() - scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager) + endlessScrollListener = EndlessRecyclerViewScrollListener(layoutManager, RoomDetailViewModel.PAGINATION_COUNT) { direction -> + roomDetailViewModel.process(RoomDetailActions.LoadMoreTimelineEvents(direction)) + } + scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager, timelineEventController) scrollOnHighlightedEventCallback = ScrollOnHighlightedEventCallback(layoutManager, timelineEventController) recyclerView.layoutManager = layoutManager recyclerView.itemAnimator = null @@ -441,35 +447,32 @@ class RoomDetailFragment : it.dispatchTo(scrollOnHighlightedEventCallback) } - recyclerView.addOnScrollListener( - EndlessRecyclerViewScrollListener(layoutManager, RoomDetailViewModel.PAGINATION_COUNT) { direction -> - roomDetailViewModel.process(RoomDetailActions.LoadMoreTimelineEvents(direction)) - }) + recyclerView.addOnScrollListener(endlessScrollListener) recyclerView.setController(timelineEventController) timelineEventController.callback = this if (vectorPreferences.swipeToReplyIsEnabled()) { val swipeCallback = RoomMessageTouchHelperCallback(requireContext(), - R.drawable.ic_reply, - object : RoomMessageTouchHelperCallback.QuickReplayHandler { - override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { - (model as? AbsMessageItem)?.attributes?.informationData?.let { - val eventId = it.eventId - roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId)) - } - } + R.drawable.ic_reply, + object : RoomMessageTouchHelperCallback.QuickReplayHandler { + override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { + (model as? AbsMessageItem)?.attributes?.informationData?.let { + val eventId = it.eventId + roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId)) + } + } - override fun canSwipeModel(model: EpoxyModel<*>): Boolean { - return when (model) { - is MessageFileItem, - is MessageImageVideoItem, - is MessageTextItem -> { - return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED - } - else -> false - } - } - }) + override fun canSwipeModel(model: EpoxyModel<*>): Boolean { + return when (model) { + is MessageFileItem, + is MessageImageVideoItem, + is MessageTextItem -> { + return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED + } + else -> false + } + } + }) val touchHelper = ItemTouchHelper(swipeCallback) touchHelper.attachToRecyclerView(recyclerView) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt index cf483090f1..c272e611a0 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt @@ -19,6 +19,7 @@ package im.vector.riotx.features.home.room.detail import androidx.recyclerview.widget.LinearLayoutManager import im.vector.riotx.core.platform.DefaultListUpdateCallback import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController +import timber.log.Timber import java.util.concurrent.atomic.AtomicReference class ScrollOnHighlightedEventCallback(private val layoutManager: LinearLayoutManager, @@ -28,17 +29,16 @@ class ScrollOnHighlightedEventCallback(private val layoutManager: LinearLayoutMa override fun onChanged(position: Int, count: Int, tag: Any?) { val eventId = scheduledEventId.get() ?: return - val positionToScroll = timelineEventController.searchPositionOfEvent(eventId) - if (positionToScroll != null) { val firstVisibleItem = layoutManager.findFirstCompletelyVisibleItemPosition() val lastVisibleItem = layoutManager.findLastCompletelyVisibleItemPosition() // Do not scroll it item is already visible if (positionToScroll !in firstVisibleItem..lastVisibleItem) { + Timber.v("Scroll to $positionToScroll") // Note: Offset will be from the bottom, since the layoutManager is reversed - layoutManager.scrollToPosition(position) + layoutManager.scrollToPosition(positionToScroll) } scheduledEventId.set(null) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnNewMessageCallback.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnNewMessageCallback.kt index 8d3a96d8df..f4cfe9eb5a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnNewMessageCallback.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnNewMessageCallback.kt @@ -18,11 +18,15 @@ package im.vector.riotx.features.home.room.detail import androidx.recyclerview.widget.LinearLayoutManager import im.vector.riotx.core.platform.DefaultListUpdateCallback +import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController +import timber.log.Timber -class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager) : DefaultListUpdateCallback { +class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager, + private val timelineEventController: TimelineEventController) : DefaultListUpdateCallback { override fun onInserted(position: Int, count: Int) { - if (position == 0 && layoutManager.findFirstVisibleItemPosition() == 0) { + Timber.v("On inserted $count count at position: $position") + if (position == 0 && layoutManager.findFirstVisibleItemPosition() == 0 && !timelineEventController.isLoadingForward()) { layoutManager.scrollToPosition(0) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index 9b9172a6f9..147666345e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -35,12 +35,7 @@ import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.factory.MergedHeaderItemFactory import im.vector.riotx.features.home.room.detail.timeline.factory.TimelineItemFactory import im.vector.riotx.features.home.room.detail.timeline.helper.* -import im.vector.riotx.features.home.room.detail.timeline.item.DaySeparatorItem -import im.vector.riotx.features.home.room.detail.timeline.item.DaySeparatorItem_ -import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem -import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem_ -import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData -import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData +import im.vector.riotx.features.home.room.detail.timeline.item.* import im.vector.riotx.features.media.ImageContentRenderer import im.vector.riotx.features.media.VideoContentRenderer import org.threeten.bp.LocalDateTime @@ -91,8 +86,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec fun onUrlLongClicked(url: String): Boolean } + private var showingForwardLoader = false private val modelCache = arrayListOf() - private var currentSnapshot: List = emptyList() private var inSubmitList: Boolean = false private var timeline: Timeline? = null @@ -163,7 +158,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec synchronized(modelCache) { for (i in 0 until modelCache.size) { if (modelCache[i]?.eventId == eventIdToHighlight - || modelCache[i]?.eventId == this.eventIdToHighlight) { + || modelCache[i]?.eventId == this.eventIdToHighlight) { modelCache[i] = null } } @@ -182,17 +177,18 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } override fun buildModels() { - val loaderAdded = LoadingItem_() - .id("forward_loading_item") + val timestamp = System.currentTimeMillis() + showingForwardLoader = LoadingItem_() + .id("forward_loading_item_$timestamp") .addWhen(Timeline.Direction.FORWARDS) val timelineModels = getModels() add(timelineModels) // Avoid displaying two loaders if there is no elements between them - if (!loaderAdded || timelineModels.isNotEmpty()) { + if (!showingForwardLoader || timelineModels.isNotEmpty()) { LoadingItem_() - .id("backward_loading_item") + .id("backward_loading_item_$timestamp") .addWhen(Timeline.Direction.BACKWARDS) } } @@ -224,8 +220,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec // Should be build if not cached or if cached but contains mergedHeader or formattedDay // We then are sure we always have items up to date. if (modelCache[position] == null - || modelCache[position]?.mergedHeaderModel != null - || modelCache[position]?.formattedDayModel != null) { + || modelCache[position]?.mergedHeaderModel != null + || modelCache[position]?.formattedDayModel != null) { modelCache[position] = buildItemModels(position, currentSnapshot) } } @@ -255,7 +251,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec it.id(event.localId) it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) } - val mergedHeaderModel = mergedHeaderItemFactory.create(event, nextEvent, items, addDaySeparator, currentPosition, callback){ + val mergedHeaderModel = mergedHeaderItemFactory.create(event, nextEvent, items, addDaySeparator, currentPosition, callback) { requestModelBuild() } val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date) @@ -284,6 +280,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec fun searchPositionOfEvent(eventId: String): Int? = synchronized(modelCache) { // Search in the cache var realPosition = 0 + if (showingForwardLoader) { + realPosition++ + } for (i in 0 until modelCache.size) { val itemCache = modelCache[i] if (itemCache?.eventId == eventId) { @@ -319,6 +318,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec return modelCache.getOrNull(position - offsetValue)?.eventId } + fun isLoadingForward() = showingForwardLoader + private data class CacheItemData( val localId: Long, val eventId: String?, From 69fb7bdf95bdd284423015799a5cae48fd5a8b37 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 16 Sep 2019 18:14:41 +0200 Subject: [PATCH 006/197] Timeline\Read marker: continue fixing potential issues --- .../session/room/timeline/DefaultTimeline.kt | 50 ++++++++++++++----- .../timeline/TimelineHiddenReadReceipts.kt | 2 +- .../home/room/detail/RoomDetailViewModel.kt | 9 ++-- 3 files changed, 43 insertions(+), 18 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index dd11b22b64..e50d25d195 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -27,15 +27,33 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.api.util.CancelableBag import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper import im.vector.matrix.android.internal.database.mapper.asDomain -import im.vector.matrix.android.internal.database.model.* -import im.vector.matrix.android.internal.database.query.* +import im.vector.matrix.android.internal.database.model.ChunkEntity +import im.vector.matrix.android.internal.database.model.ChunkEntityFields +import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity +import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.database.model.EventEntityFields +import im.vector.matrix.android.internal.database.model.RoomEntity +import im.vector.matrix.android.internal.database.model.TimelineEventEntity +import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields +import im.vector.matrix.android.internal.database.query.FilterContent +import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates +import im.vector.matrix.android.internal.database.query.findIncludingEvent +import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom +import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.database.query.whereInRoom import im.vector.matrix.android.internal.task.TaskConstraints import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.Debouncer import im.vector.matrix.android.internal.util.createBackgroundHandler import im.vector.matrix.android.internal.util.createUIHandler -import io.realm.* +import io.realm.OrderedCollectionChangeSet +import io.realm.OrderedRealmCollectionChangeListener +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.RealmQuery +import io.realm.RealmResults +import io.realm.Sort import timber.log.Timber import java.util.* import java.util.concurrent.atomic.AtomicBoolean @@ -221,11 +239,11 @@ internal class DefaultTimeline( override fun dispose() { if (isStarted.compareAndSet(true, false)) { - eventDecryptor.destroy() + isReady.set(false) Timber.v("Dispose timeline for roomId: $roomId and eventId: $initialEventId") + cancelableBag.cancel() BACKGROUND_HANDLER.removeCallbacksAndMessages(null) BACKGROUND_HANDLER.post { - cancelableBag.cancel() roomEntity?.sendingTimelineEvents?.removeAllChangeListeners() eventRelations.removeAllChangeListeners() liveEvents.removeAllChangeListeners() @@ -238,6 +256,7 @@ internal class DefaultTimeline( it.close() } } + eventDecryptor.destroy() } } @@ -305,7 +324,7 @@ internal class DefaultTimeline( private fun hasMoreInCache(direction: Timeline.Direction): Boolean { return Realm.getInstance(realmConfiguration).use { localRealm -> val timelineEventEntity = buildEventQuery(localRealm).findFirst(direction) - ?: return false + ?: return false if (direction == Timeline.Direction.FORWARDS) { val firstEvent = builtEvents.firstOrNull() ?: return true firstEvent.displayIndex < timelineEventEntity.root!!.displayIndex @@ -426,11 +445,15 @@ internal class DefaultTimeline( * This has to be called on TimelineThread as it access realm live results */ private fun executePaginationTask(direction: Timeline.Direction, limit: Int) { - val token = getTokenLive(direction) ?: return + val token = getTokenLive(direction) + if (token == null) { + updatePaginationState(direction) { it.copy(isPaginating = false, requestedCount = 0) } + return + } val params = PaginationTask.Params(roomId = roomId, - from = token, - direction = direction.toPaginationDirection(), - limit = limit) + from = token, + direction = direction.toPaginationDirection(), + limit = limit) Timber.v("Should fetch $limit items $direction") cancelableBag += paginationTask @@ -501,7 +524,7 @@ internal class DefaultTimeline( val timelineEvent = buildTimelineEvent(eventEntity) if (timelineEvent.isEncrypted() - && timelineEvent.root.mxDecryptionResult == null) { + && timelineEvent.root.mxDecryptionResult == null) { timelineEvent.root.eventId?.let { eventDecryptor.requestDecryption(it) } } @@ -584,11 +607,14 @@ internal class DefaultTimeline( private fun fetchEvent(eventId: String) { val params = GetContextOfEventTask.Params(roomId, eventId) - contextOfEventTask.configureWith(params).executeBy(taskExecutor) + cancelableBag += contextOfEventTask.configureWith(params).executeBy(taskExecutor) } private fun postSnapshot() { BACKGROUND_HANDLER.post { + if (isReady.get().not()) { + return@post + } val snapshot = createSnapshot() val runnable = Runnable { listener?.onUpdated(snapshot) } debouncer.debounce("post_snapshot", runnable, 50) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt index 5658210302..5408668576 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt @@ -56,7 +56,7 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu var hasChange = false // Deletion here means we don't have any readReceipts for the given hidden events changeSet.deletions.forEach { - val eventId = correctedReadReceiptsEventByIndex[it] + val eventId = correctedReadReceiptsEventByIndex.get(it, "") val timelineEvent = liveEvents.where() .equalTo(TimelineEventEntityFields.EVENT_ID, eventId) .findFirst() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 62c067159e..a45ea55825 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -643,11 +643,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro if (readMarkerId == null || isReadMarkerViewVisible || !timeline.isLive) { false } else { - val readMarkerPosition = timeline.getIndexOfEvent(readMarkerId) - ?: Int.MAX_VALUE - val currentVisibleEventPosition = timeline.getIndexOfEvent(currentVisibleEvent.event.root.eventId) - ?: Int.MAX_VALUE - readMarkerPosition > currentVisibleEventPosition + val readMarkerPosition = timeline.getTimelineEventWithId(readMarkerId)?.displayIndex + ?: Int.MIN_VALUE + val currentVisibleEventPosition = currentVisibleEvent.event.displayIndex + readMarkerPosition < currentVisibleEventPosition } } ) From 3066d5f3039f7402768f6df629c38be7f016cacc Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 17 Sep 2019 19:38:05 +0200 Subject: [PATCH 007/197] Timeline\ReadMarker: continue fixing issues --- .../api/session/room/timeline/Timeline.kt | 2 +- .../session/room/read/SetReadMarkersTask.kt | 2 - .../session/room/timeline/DefaultTimeline.kt | 8 +- .../session/sync/RoomFullyReadHandler.kt | 12 +- .../riotx/core/extensions/TimelineEvent.kt | 4 + .../riotx/core/ui/views/ReadMarkerView.kt | 4 +- .../home/room/detail/RoomDetailFragment.kt | 121 +++++++++++++----- .../home/room/detail/RoomDetailViewModel.kt | 22 +++- .../timeline/TimelineEventController.kt | 24 +++- .../factory/MergedHeaderItemFactory.kt | 24 +++- .../EndlessRecyclerViewScrollListener.kt | 63 --------- .../helper/MessageInformationDataFactory.kt | 4 +- .../detail/timeline/item/AbsMessageItem.kt | 3 +- .../detail/timeline/item/BaseEventItem.kt | 2 + .../detail/timeline/item/MergedHeaderItem.kt | 1 + .../room/detail/timeline/item/NoticeItem.kt | 3 +- .../res/drawable-hdpi/arrow_up_circle.png | Bin 0 -> 686 bytes .../main/res/drawable-hdpi/chevron_down.png | Bin 0 -> 303 bytes .../res/drawable-mdpi/arrow_up_circle.png | Bin 0 -> 414 bytes .../main/res/drawable-mdpi/chevron_down.png | Bin 0 -> 231 bytes .../res/drawable-xhdpi/arrow_up_circle.png | Bin 0 -> 869 bytes .../main/res/drawable-xhdpi/chevron_down.png | Bin 0 -> 391 bytes .../res/drawable-xxhdpi/arrow_up_circle.png | Bin 0 -> 1332 bytes .../main/res/drawable-xxhdpi/chevron_down.png | Bin 0 -> 454 bytes .../res/drawable-xxxhdpi/arrow_up_circle.png | Bin 0 -> 1910 bytes .../res/drawable-xxxhdpi/chevron_down.png | Bin 0 -> 584 bytes .../main/res/layout/fragment_room_detail.xml | 13 ++ .../res/layout/view_jump_to_read_marker.xml | 5 +- 28 files changed, 181 insertions(+), 136 deletions(-) delete mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.kt create mode 100755 vector/src/main/res/drawable-hdpi/arrow_up_circle.png create mode 100755 vector/src/main/res/drawable-hdpi/chevron_down.png create mode 100755 vector/src/main/res/drawable-mdpi/arrow_up_circle.png create mode 100755 vector/src/main/res/drawable-mdpi/chevron_down.png create mode 100755 vector/src/main/res/drawable-xhdpi/arrow_up_circle.png create mode 100755 vector/src/main/res/drawable-xhdpi/chevron_down.png create mode 100755 vector/src/main/res/drawable-xxhdpi/arrow_up_circle.png create mode 100755 vector/src/main/res/drawable-xxhdpi/chevron_down.png create mode 100755 vector/src/main/res/drawable-xxxhdpi/arrow_up_circle.png create mode 100755 vector/src/main/res/drawable-xxxhdpi/chevron_down.png diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt index 3f90d3cd13..d0f4bff74b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt @@ -45,7 +45,7 @@ interface Timeline { fun dispose() - fun restartWithEventId(eventId: String) + fun restartWithEventId(eventId: String?) /** diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt index 9652faae81..26eb16b15c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt @@ -26,7 +26,6 @@ import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.query.find import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom -import im.vector.matrix.android.internal.database.query.getOrCreate import im.vector.matrix.android.internal.database.query.latestEvent import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.network.executeRequest @@ -36,7 +35,6 @@ import im.vector.matrix.android.internal.session.sync.RoomFullyReadHandler import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.awaitTransaction import io.realm.Realm -import io.realm.RealmConfiguration import timber.log.Timber import javax.inject.Inject diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index e50d25d195..88c13cc056 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -114,7 +114,7 @@ internal class DefaultTimeline( private val timelineID = UUID.randomUUID().toString() override val isLive - get() = initialEventId == null + get() = !hasMoreToLoad(Timeline.Direction.FORWARDS) private val eventDecryptor = TimelineEventDecryptor(realmConfiguration, timelineID, cryptoService) @@ -260,7 +260,7 @@ internal class DefaultTimeline( } } - override fun restartWithEventId(eventId: String) { + override fun restartWithEventId(eventId: String?) { dispose() initialEventId = eventId start() @@ -415,7 +415,7 @@ internal class DefaultTimeline( */ private fun handleInitialLoad() { var shouldFetchInitialEvent = false - val initialDisplayIndex = if (isLive) { + val initialDisplayIndex = if (initialEventId == null) { liveEvents.firstOrNull()?.root?.displayIndex } else { val initialEvent = liveEvents.where() @@ -431,7 +431,7 @@ internal class DefaultTimeline( fetchEvent(currentInitialEventId) } else { val count = min(settings.initialSize, liveEvents.size) - if (isLive) { + if (initialEventId == null) { paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count, strict = false) } else { paginateInternal(initialDisplayIndex, Timeline.Direction.FORWARDS, count / 2, strict = false) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt index 9757d0f421..45fbe7329d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.session.sync import im.vector.matrix.android.api.session.room.read.FullyReadContent +import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.ReadMarkerEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity @@ -37,12 +38,13 @@ internal class RoomFullyReadHandler @Inject constructor() { RoomSummaryEntity.getOrCreate(realm, roomId).apply { readMarkerId = content.eventId } - val readMarkerEntity = ReadMarkerEntity.getOrCreate(realm, roomId).apply { - eventId = content.eventId - } - + val readMarkerEntity = ReadMarkerEntity.getOrCreate(realm, roomId) // Remove the old marker if any - readMarkerEntity.timelineEvent?.firstOrNull()?.readMarker = null + if (readMarkerEntity.eventId.isNotEmpty()) { + val oldReadMarkerEvent = TimelineEventEntity.where(realm, eventId = readMarkerEntity.eventId).findFirst() + oldReadMarkerEvent?.readMarker = null + } + readMarkerEntity.eventId = content.eventId // Attach to timelineEvent if known val timelineEventEntity = TimelineEventEntity.where(realm, eventId = content.eventId).findFirst() timelineEventEntity?.readMarker = readMarkerEntity diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/TimelineEvent.kt b/vector/src/main/java/im/vector/riotx/core/extensions/TimelineEvent.kt index 58fcd0b5cd..6c7a6be1fd 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/TimelineEvent.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/TimelineEvent.kt @@ -23,3 +23,7 @@ fun TimelineEvent.canReact(): Boolean { // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment return root.getClearType() == EventType.MESSAGE && root.sendState.isSent() && !root.isRedacted() } + +fun TimelineEvent.displayReadMarker(myUserId: String): Boolean { + return hasReadMarker && readReceipts.find { it.user.userId == myUserId } == null +} diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt index becab54da3..f5f086ac8b 100644 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt @@ -42,9 +42,9 @@ class ReadMarkerView @JvmOverloads constructor( private var callback: Callback? = null private var callbackDispatcherJob: Job? = null - fun bindView(informationData: MessageInformationData, readMarkerCallback: Callback) { + fun bindView(displayReadMarker: Boolean, readMarkerCallback: Callback) { this.callback = readMarkerCallback - if (informationData.displayReadMarker) { + if (displayReadMarker) { visibility = VISIBLE callbackDispatcherJob = GlobalScope.launch(Dispatchers.Main) { delay(DELAY_IN_MS) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 61338e7858..48cdea6e59 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -44,6 +44,7 @@ import androidx.core.content.ContextCompat import androidx.core.util.Pair import androidx.core.view.ViewCompat import androidx.core.view.forEach +import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager @@ -57,6 +58,7 @@ import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState import com.github.piasy.biv.BigImageViewer import com.github.piasy.biv.loader.ImageLoader import com.google.android.material.snackbar.Snackbar @@ -78,6 +80,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageTextConten import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent import im.vector.matrix.android.api.session.room.send.SendState +import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent @@ -96,6 +99,7 @@ import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.ui.views.JumpToReadMarkerView import im.vector.riotx.core.ui.views.NotificationAreaView +import im.vector.riotx.core.utils.Debouncer import im.vector.riotx.core.utils.PERMISSIONS_FOR_TAKING_PHOTO import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_DOWNLOAD_FILE @@ -105,6 +109,7 @@ import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CA import im.vector.riotx.core.utils.allGranted import im.vector.riotx.core.utils.checkPermissions import im.vector.riotx.core.utils.copyToClipboard +import im.vector.riotx.core.utils.createUIHandler import im.vector.riotx.core.utils.openCamera import im.vector.riotx.core.utils.shareMedia import im.vector.riotx.core.utils.toast @@ -127,7 +132,6 @@ import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsB import im.vector.riotx.features.home.room.detail.timeline.action.SimpleAction import im.vector.riotx.features.home.room.detail.timeline.action.ViewEditHistoryBottomSheet import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionBottomSheet -import im.vector.riotx.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener import im.vector.riotx.features.home.room.detail.timeline.item.AbsMessageItem import im.vector.riotx.features.home.room.detail.timeline.item.MessageFileItem import im.vector.riotx.features.home.room.detail.timeline.item.MessageImageVideoItem @@ -211,6 +215,8 @@ class RoomDetailFragment : private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel() private val textComposerViewModel: TextComposerViewModel by fragmentViewModel() + private val debouncer = Debouncer(createUIHandler()) + @Inject lateinit var session: Session @Inject lateinit var avatarRenderer: AvatarRenderer @Inject lateinit var timelineEventController: TimelineEventController @@ -227,7 +233,6 @@ class RoomDetailFragment : private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback private lateinit var scrollOnHighlightedEventCallback: ScrollOnHighlightedEventCallback - private lateinit var endlessScrollListener: EndlessRecyclerViewScrollListener override fun getLayoutResId() = R.layout.fragment_room_detail @@ -254,6 +259,7 @@ class RoomDetailFragment : setupInviteView() setupNotificationView() setupJumpToReadMarkerView() + setupJumpToBottomView() roomDetailViewModel.subscribe { renderState(it) } textComposerViewModel.subscribe { renderTextComposerState(it) } roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) } @@ -306,6 +312,21 @@ class RoomDetailFragment : } } + private fun setupJumpToBottomView() { + jumpToBottomView.isVisible = false + jumpToBottomView.setOnClickListener { + withState(roomDetailViewModel) { state -> + recyclerView.stopScroll() + if (state.timeline?.isLive == false) { + state.timeline.restartWithEventId(null) + } else { + layoutManager.scrollToPosition(0) + } + jumpToBottomView.isVisible = false + } + } + } + private fun setupJumpToReadMarkerView() { jumpToReadMarkerView.callback = this } @@ -377,17 +398,17 @@ class RoomDetailFragment : if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { val parser = Parser.builder().build() val document = parser.parse(messageContent.formattedBody - ?: messageContent.body) + ?: messageContent.body) formattedBody = eventHtmlRenderer.render(document) } composerLayout.composerRelatedMessageContent.text = formattedBody - ?: nonFormattedBody + ?: nonFormattedBody composerLayout.composerEditText.setText(if (useText) event.getTextEditableContent() else "") composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes)) avatarRenderer.render(event.senderAvatar, event.root.senderId - ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) + ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length) composerLayout.expand { @@ -416,9 +437,9 @@ class RoomDetailFragment : REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data) REACTION_SELECT_REQUEST_CODE -> { val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID) - ?: return + ?: return val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT) - ?: return + ?: return //TODO check if already reacted with that? roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId)) } @@ -428,14 +449,12 @@ class RoomDetailFragment : // PRIVATE METHODS ***************************************************************************** + private fun setupRecyclerView() { val epoxyVisibilityTracker = EpoxyVisibilityTracker() epoxyVisibilityTracker.attach(recyclerView) layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true) val stateRestorer = LayoutManagerStateRestorer(layoutManager).register() - endlessScrollListener = EndlessRecyclerViewScrollListener(layoutManager, RoomDetailViewModel.PAGINATION_COUNT) { direction -> - roomDetailViewModel.process(RoomDetailActions.LoadMoreTimelineEvents(direction)) - } scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager, timelineEventController) scrollOnHighlightedEventCallback = ScrollOnHighlightedEventCallback(layoutManager, timelineEventController) recyclerView.layoutManager = layoutManager @@ -446,38 +465,67 @@ class RoomDetailFragment : it.dispatchTo(scrollOnNewMessageCallback) it.dispatchTo(scrollOnHighlightedEventCallback) } - - recyclerView.addOnScrollListener(endlessScrollListener) recyclerView.setController(timelineEventController) + recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + if (recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE) { + updateJumpToBottomViewVisibility() + } + } + + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + when (newState) { + RecyclerView.SCROLL_STATE_IDLE -> { + updateJumpToBottomViewVisibility() + } + RecyclerView.SCROLL_STATE_DRAGGING, + RecyclerView.SCROLL_STATE_SETTLING -> { + jumpToBottomView.hide() + } + } + } + }) timelineEventController.callback = this if (vectorPreferences.swipeToReplyIsEnabled()) { val swipeCallback = RoomMessageTouchHelperCallback(requireContext(), - R.drawable.ic_reply, - object : RoomMessageTouchHelperCallback.QuickReplayHandler { - override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { - (model as? AbsMessageItem)?.attributes?.informationData?.let { - val eventId = it.eventId - roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId)) - } - } + R.drawable.ic_reply, + object : RoomMessageTouchHelperCallback.QuickReplayHandler { + override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { + (model as? AbsMessageItem)?.attributes?.informationData?.let { + val eventId = it.eventId + roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId)) + } + } - override fun canSwipeModel(model: EpoxyModel<*>): Boolean { - return when (model) { - is MessageFileItem, - is MessageImageVideoItem, - is MessageTextItem -> { - return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED - } - else -> false - } - } - }) + override fun canSwipeModel(model: EpoxyModel<*>): Boolean { + return when (model) { + is MessageFileItem, + is MessageImageVideoItem, + is MessageTextItem -> { + return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED + } + else -> false + } + } + }) val touchHelper = ItemTouchHelper(swipeCallback) touchHelper.attachToRecyclerView(recyclerView) } } + private fun updateJumpToBottomViewVisibility() { + debouncer.debounce("jump_to_bottom_visibility", 100, Runnable { + Timber.v("First visible: ${layoutManager.findFirstCompletelyVisibleItemPosition()}") + if (layoutManager.findFirstCompletelyVisibleItemPosition() != 0) { + jumpToBottomView.show() + } else { + jumpToBottomView.hide() + } + }) + } + private fun setupComposer() { val elevation = 6f val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(requireContext(), R.attr.riotx_background)) @@ -737,7 +785,7 @@ class RoomDetailFragment : .show() } - // TimelineEventController.Callback ************************************************************ +// TimelineEventController.Callback ************************************************************ override fun onUrlClicked(url: String): Boolean { return permalinkHandler.launch(requireActivity(), url, object : NavigateToRoomInterceptor { @@ -835,6 +883,10 @@ class RoomDetailFragment : vectorBaseActivity.notImplemented("open audio file") } + override fun onLoadMore(direction: Timeline.Direction) { + roomDetailViewModel.process(RoomDetailActions.LoadMoreTimelineEvents(direction)) + } + override fun onEventCellClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View) { } @@ -901,7 +953,7 @@ class RoomDetailFragment : } } - // AutocompleteUserPresenter.Callback +// AutocompleteUserPresenter.Callback override fun onQueryUsers(query: CharSequence?) { textComposerViewModel.process(TextComposerActions.QueryUsers(query)) @@ -1066,6 +1118,7 @@ class RoomDetailFragment : snack.show() } + // VectorInviteView.Callback override fun onAcceptInvite() { @@ -1078,7 +1131,7 @@ class RoomDetailFragment : roomDetailViewModel.process(RoomDetailActions.RejectInvite) } - // JumpToReadMarkerView.Callback +// JumpToReadMarkerView.Callback override fun onJumpToReadMarkerClicked(readMarkerId: String) { roomDetailViewModel.process(RoomDetailActions.NavigateToEvent(readMarkerId, false)) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index a45ea55825..ee4b3c0423 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -21,6 +21,8 @@ import android.text.TextUtils import androidx.annotation.IdRes import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import arrow.core.Option +import arrow.core.getOrElse import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success @@ -635,16 +637,22 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private fun observeJumpToReadMarkerViewVisibility() { Observable .combineLatest( - room.rx().liveRoomSummary(), + room.rx().liveRoomSummary().map { + val readMarkerId = it.readMarkerId + if (readMarkerId == null) { + Option.empty() + } else { + val timelineEvent = room.getTimeLineEvent(readMarkerId) + Option.fromNullable(timelineEvent) + } + }.distinctUntilChanged(), visibleEventsObservable.distinctUntilChanged(), - isEventVisibleObservable { it.hasReadMarker }.startWith(false).takeUntil { it }, - Function3 { roomSummary, currentVisibleEvent, isReadMarkerViewVisible -> - val readMarkerId = roomSummary.readMarkerId - if (readMarkerId == null || isReadMarkerViewVisible || !timeline.isLive) { + isEventVisibleObservable { it.hasReadMarker }.startWith(false), + Function3, RoomDetailActions.TimelineEventTurnsVisible, Boolean, Boolean> { readMarkerEvent, currentVisibleEvent, isReadMarkerViewVisible -> + if (readMarkerEvent.isEmpty() || isReadMarkerViewVisible) { false } else { - val readMarkerPosition = timeline.getTimelineEventWithId(readMarkerId)?.displayIndex - ?: Int.MIN_VALUE + val readMarkerPosition = readMarkerEvent.map { it.displayIndex }.getOrElse { Int.MIN_VALUE } val currentVisibleEventPosition = currentVisibleEvent.event.displayIndex readMarkerPosition < currentVisibleEventPosition } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index 147666345e..652f35fb67 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -24,6 +24,7 @@ import androidx.recyclerview.widget.ListUpdateCallback import androidx.recyclerview.widget.RecyclerView import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyModel +import com.airbnb.epoxy.VisibilityState import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent @@ -50,6 +51,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener { interface Callback : BaseCallback, ReactionPillCallback, AvatarCallback, UrlClickCallback, ReadReceiptsCallback { + fun onLoadMore(direction: Timeline.Direction) fun onEventInvisible(event: TimelineEvent) fun onEventVisible(event: TimelineEvent) fun onRoomCreateLinkClicked(url: String) @@ -158,7 +160,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec synchronized(modelCache) { for (i in 0 until modelCache.size) { if (modelCache[i]?.eventId == eventIdToHighlight - || modelCache[i]?.eventId == this.eventIdToHighlight) { + || modelCache[i]?.eventId == this.eventIdToHighlight) { modelCache[i] = null } } @@ -180,6 +182,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec val timestamp = System.currentTimeMillis() showingForwardLoader = LoadingItem_() .id("forward_loading_item_$timestamp") + .setVisibilityStateChangedListener(Timeline.Direction.FORWARDS) .addWhen(Timeline.Direction.FORWARDS) val timelineModels = getModels() @@ -189,6 +192,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec if (!showingForwardLoader || timelineModels.isNotEmpty()) { LoadingItem_() .id("backward_loading_item_$timestamp") + .setVisibilityStateChangedListener(Timeline.Direction.BACKWARDS) .addWhen(Timeline.Direction.BACKWARDS) } } @@ -220,8 +224,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec // Should be build if not cached or if cached but contains mergedHeader or formattedDay // We then are sure we always have items up to date. if (modelCache[position] == null - || modelCache[position]?.mergedHeaderModel != null - || modelCache[position]?.formattedDayModel != null) { + || modelCache[position]?.mergedHeaderModel != null + || modelCache[position]?.formattedDayModel != null) { modelCache[position] = buildItemModels(position, currentSnapshot) } } @@ -251,7 +255,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec it.id(event.localId) it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) } - val mergedHeaderModel = mergedHeaderItemFactory.create(event, nextEvent, items, addDaySeparator, currentPosition, callback) { + val mergedHeaderModel = mergedHeaderItemFactory.create(event, nextEvent, items, addDaySeparator, currentPosition, eventIdToHighlight, callback) { requestModelBuild() } val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date) @@ -277,6 +281,18 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec return shouldAdd } + /** + * Return true if added + */ + private fun LoadingItem_.setVisibilityStateChangedListener(direction: Timeline.Direction): LoadingItem_ { + return onVisibilityStateChanged { model, view, visibilityState -> + if (visibilityState == VisibilityState.VISIBLE) { + callback?.onLoadMore(direction) + } + } + } + + fun searchPositionOfEvent(eventId: String): Int? = synchronized(modelCache) { // Search in the cache var realPosition = 0 diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index 06514e5973..42f0688e50 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -17,6 +17,8 @@ package im.vector.riotx.features.home.room.detail.timeline.factory import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.riotx.core.di.ActiveSessionHolder +import im.vector.riotx.core.extensions.displayReadMarker import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider @@ -29,7 +31,8 @@ import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem_ import javax.inject.Inject -class MergedHeaderItemFactory @Inject constructor(private val avatarRenderer: AvatarRenderer, +class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: ActiveSessionHolder, + private val avatarRenderer: AvatarRenderer, private val avatarSizeProvider: AvatarSizeProvider) { private val collapsedEventIds = linkedSetOf() @@ -40,6 +43,7 @@ class MergedHeaderItemFactory @Inject constructor(private val avatarRenderer: Av items: List, addDaySeparator: Boolean, currentPosition: Int, + eventIdToHighlight: String?, callback: TimelineEventController.Callback?, requestModelBuild: () -> Unit) : MergedHeaderItem? { @@ -47,20 +51,30 @@ class MergedHeaderItemFactory @Inject constructor(private val avatarRenderer: Av return if (!event.canBeMerged() || (nextEvent?.root?.getClearType() == event.root.getClearType() && !addDaySeparator)) { null } else { + var highlighted = false + var showReadMarker = false val prevSameTypeEvents = items.prevSameTypeEvents(currentPosition, 2) if (prevSameTypeEvents.isEmpty()) { null } else { val mergedEvents = (prevSameTypeEvents + listOf(event)).asReversed() - val mergedData = mergedEvents.map { mergedEvent -> + val mergedData = ArrayList(mergedEvents.size) + mergedEvents.forEach { mergedEvent -> + if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) { + highlighted = true + } + if (!showReadMarker && mergedEvent.displayReadMarker(sessionHolder.getActiveSession().myUserId)) { + showReadMarker = true + } val senderAvatar = mergedEvent.senderAvatar() val senderName = mergedEvent.senderName() - MergedHeaderItem.Data( + val data = MergedHeaderItem.Data( userId = mergedEvent.root.senderId ?: "", avatarUrl = senderAvatar, memberName = senderName ?: "", eventId = mergedEvent.localId ) + mergedData.add(data) } val mergedEventIds = mergedEvents.map { it.localId } // We try to find if one of the item id were used as mergeItemCollapseStates key @@ -82,11 +96,13 @@ class MergedHeaderItemFactory @Inject constructor(private val avatarRenderer: Av onCollapsedStateChanged = { mergeItemCollapseStates[event.localId] = it requestModelBuild() - } + }, + showReadMarker = showReadMarker ) MergedHeaderItem_() .id(mergeId) .leftGuideline(avatarSizeProvider.leftGuideline) + .highlighted(highlighted) .attributes(attributes) .also { it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents)) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.kt deleted file mode 100644 index 9bcb7c634f..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2019 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.riotx.features.home.room.detail.timeline.helper - -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import im.vector.matrix.android.api.session.room.timeline.Timeline - -class EndlessRecyclerViewScrollListener(private val layoutManager: LinearLayoutManager, - private val visibleThreshold: Int, - private val onLoadMore: (Timeline.Direction) -> Unit -) : RecyclerView.OnScrollListener() { - - // The total number of items in the dataset after the last load - private var previousTotalItemCount = 0 - // True if we are still waiting for the last set of data to load. - private var loadingBackwards = true - private var loadingForwards = true - - // This happens many times a second during a scroll, so be wary of the code you place here. - // We are given a few useful parameters to help us work out if we need to load some more data, - // but first we check if we are waiting for the previous load to finish. - - override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) { - val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition() - val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() - val totalItemCount = layoutManager.itemCount - - // We check to see if the dataset count has - // changed, if so we conclude it has finished loading - if (totalItemCount != previousTotalItemCount) { - previousTotalItemCount = totalItemCount - loadingBackwards = false - loadingForwards = false - } - // If it isn’t currently loading, we check to see if we have reached - // the visibleThreshold and need to reload more data. - if (!loadingBackwards && lastVisibleItemPosition + visibleThreshold > totalItemCount) { - loadingBackwards = true - onLoadMore(Timeline.Direction.BACKWARDS) - } - if (!loadingForwards && firstVisibleItemPosition < visibleThreshold) { - loadingForwards = true - onLoadMore(Timeline.Direction.FORWARDS) - } - } - - -} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index 7ca5204766..b8a89a4669 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -27,6 +27,7 @@ import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.utils.isSingleEmoji import im.vector.riotx.features.home.getColorFromUserId import im.vector.riotx.core.date.VectorDateFormatter +import im.vector.riotx.core.extensions.displayReadMarker import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.home.room.detail.timeline.item.ReactionInfoData import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData @@ -64,8 +65,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId ?: "")) } - val displayReadMarker = event.hasReadMarker - && event.readReceipts.find { it.user.userId == session.myUserId } == null + val displayReadMarker = event.displayReadMarker(session.myUserId) return MessageInformationData( eventId = eventId, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt index b299d61540..44a5e2bdfb 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -106,7 +106,7 @@ abstract class AbsMessageItem : BaseEventItem() { holder.memberNameView.setOnLongClickListener(null) } holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener) - holder.readMarkerView.bindView(attributes.informationData, _readMarkerCallback) + holder.readMarkerView.bindView(attributes.informationData.displayReadMarker, _readMarkerCallback) if (!shouldShowReactionAtBottom() || attributes.informationData.orderedReactionList.isNullOrEmpty()) { holder.reactionWrapper?.isVisible = false @@ -162,7 +162,6 @@ abstract class AbsMessageItem : BaseEventItem() { val avatarImageView by bind(R.id.messageAvatarImageView) val memberNameView by bind(R.id.messageMemberNameView) val timeView by bind(R.id.messageTimeView) - val readMarkerView by bind(R.id.readMarkerView) var reactionWrapper: ViewGroup? = null var reactionFlowHelper: Flow? = null } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt index 532d56f580..a97ec23c97 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt @@ -24,6 +24,7 @@ import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.platform.CheckableView +import im.vector.riotx.core.ui.views.ReadMarkerView import im.vector.riotx.core.ui.views.ReadReceiptsView import im.vector.riotx.core.utils.DimensionUtils.dpToPx import org.w3c.dom.Attr @@ -49,6 +50,7 @@ abstract class BaseEventItem : VectorEpoxyModel val leftGuideline by bind(R.id.messageStartGuideline) val checkableBackground by bind(R.id.messageSelectedBackground) val readReceiptsView by bind(R.id.readReceiptsView) + val readMarkerView by bind(R.id.readMarkerView) override fun bindView(itemView: View) { super.bindView(itemView) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt index 0ac068c379..f07575e1a5 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt @@ -79,6 +79,7 @@ abstract class MergedHeaderItem : BaseEventItem() { data class Attributes( val isCollapsed: Boolean, + val showReadMarker: Boolean, val mergeData: List, val avatarRenderer: AvatarRenderer, val onCollapsedStateChanged: (Boolean) -> Unit diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt index d3443cb0fb..8e61a3be1f 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt @@ -55,7 +55,7 @@ abstract class NoticeItem : BaseEventItem() { ) holder.view.setOnLongClickListener(attributes.itemLongClickListener) holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener) - holder.readMarkerView.bindView(attributes.informationData, _readMarkerCallback) + holder.readMarkerView.bindView(attributes.informationData.displayReadMarker, _readMarkerCallback) } override fun unbind(holder: Holder) { @@ -68,7 +68,6 @@ abstract class NoticeItem : BaseEventItem() { class Holder : BaseHolder(STUB_ID) { val avatarImageView by bind(R.id.itemNoticeAvatarView) val noticeTextView by bind(R.id.itemNoticeTextView) - val readMarkerView by bind(R.id.readMarkerView) } data class Attributes( diff --git a/vector/src/main/res/drawable-hdpi/arrow_up_circle.png b/vector/src/main/res/drawable-hdpi/arrow_up_circle.png new file mode 100755 index 0000000000000000000000000000000000000000..c7fba081c151926a44f8b955da6857a2c9a7e6f7 GIT binary patch literal 686 zcmV;f0#W^mP)Px%Xh}ptR7efImcL2^K@i40u?z}EEJR645yiq+5L+P@K7obcGx!ERfTfMCwu(g_ zKr1bUfQq0fhzM$9{C$@hZ|~eoOoV*!?d<&7nZ4a*ZSKLb9)fhAh58)RK@qlsxPfSV=p?Hm({8Lc^7vME?Jki;O zi?IZ=Lvx%uF;w6O6n1kX0tH_UwqS@BO1FYK#e(uzMraz+3}wf= zhQDPoS{A`&i+;2q_C@Guj52v$LBbUt6+rPpE8;^=0ue2->X zQZe0i!fi)nEeWR4ny?O)cpKh8K~j2HQ1M0FnPEo7s;1~-En*!*9j1kt>3$esdRS2L zMLe0II!4Efc+oKU2xS?bk+^fBLN9$4WCb4Ru0q8ZF=d8l71M7*nCpnFC6Q*elUd?D zn@DsrjF`1i_gY5Z@M&fkJ&o}ay{(qd_wiE2PT{8wZS+pjSHy-ze;`$d-Ot}36REMJ zAy#`NKvb5SCapGU3o!ClKH#Q|96<8kiX}Px#=}AOER5%f(l*U*pi^`R4u}PiI57YxMnD{x2c#?kV_@>94 zf-h#ewhLTye7VAulU{ zq}U|zYv6qzmb`@`T6mrsL_3kDbw9Ee&p|l(`UGE`(4IFjC?Eg;002ovPDHLkV1g32 BdZPdU literal 0 HcmV?d00001 diff --git a/vector/src/main/res/drawable-mdpi/arrow_up_circle.png b/vector/src/main/res/drawable-mdpi/arrow_up_circle.png new file mode 100755 index 0000000000000000000000000000000000000000..aad94e9a4e8492b15a30423dd8965ea33cf2eff4 GIT binary patch literal 414 zcmV;P0b%}$P)Px$SV=@dR5%f>l)XyBU=)TML8uPhN{3#Flel#DCfY>@ucEuVV`uRS3S9)DH{jx+ zb)lYC!NbQ(~WWuNddJcPq2dBiT@RZ$er5o%mp9m+`drKzsWgE@LQ0S)j#N^vXAdrX+23xzg5)ox>(3y*-F}kPx#p-DtRR45f=)H5*N%E-XL&&0?Tc>et1*9=4eLql89fB(O&|IfhikqM;v|NsBi zK!N=r2}1gzn)ieBfGq~u%*e=i6)2!h$PgsW+8{k(y&xO0NI0T<2bUV4DTDF(p%{S85Re!y%^*1p13-evP6jK21Q?3VAaM}DY5+(=&&o=S h@!QXJAU3`<0s!RilQx$3s?Pub002ovPDHLkV1lR@TOPx&A4x<(R9Fekn7eKhK@^6wQEE!0q_UMVHHc8e6(S8ygp?*UNzX&@47>md4?qzq za!I5hA-{lyWZ{ZPAaSXJP>A{dWlr|&usiEZVtl0EXXaf0-LYq9oh?lvqtR%Ew0och zR>2yOd;;fS2zptTU7~MCL;~!9L+}+eiDDG199_W8NhsP!Pbt3REfe+VB&!k727Z~oGk=s! zn!u@AKs8O&OW)IK!FgtpXkE>Xk+F`d8ET_)IWu2LhhCA@^-;Rz-}_wIf?gQboDMaPbTr*8SB?Xo_;|;fr-YGfBJ;!L*-*A|N%?*1FmFb(mdihz4r*BhhZ=$^GMIB2ixiRCU`@nfWr>eXv!H*2 z+8tPY@W*7XT)rPSl2R|Ao+g`m-n7&x$bjd}XPd;DEGsRnSE>@u7m!LWs6BEw@!S|RT0wD*~B}|X8I)q ze`of&!dK=r9c(cv<<^Psxh6uBimtwrj1{UNWv)i8&8WmMk)ciR8R*jReIhP*i4@D+ zXYv>vCE`mfBtkj^n|P<$a!msrs$P_g4U<5;iM;Q5t|c3~y6aq3@@Ma2XC8)LI$fF! zMW_e91&iiS!gwA^z0^}84!vb=t6o)(WeM`s26NE9*Ri-5yEm@+Hn(!DXHm$xaJhc$ z%>NEUk8S5EIN+S+__0C$AYj8aNDhgwKYl2afcOaV{|;z&DUe#6BW$=!i*?8kq&nQG zdA$a0+3F-Ugcj#T(U?L?cgnw`G0jEC&RJijYhMsEBT%Jj+{V5be>Ql65Yv{>2h)mj vi?<42eA+Pu=kkYDa>?q*iyXdN`AhE)_8o00001b5ch_0Itp) z=>Px$K}keGR7efAl|6F8Fc3y}p9qAIaDkr{xJJSiZWZv6@PTIvxJdZI!I15KIkEue?W5z>?MTlh z0EksWa{p($8yt0ywjnxfVcViIK0Q6XF0Px(?ny*JRA>d=n$K@cQ545#RMY}pS+F90MErq3HO0r}r{F&U^2^H#4oyNxr?k_nz}T=lg!Y`=(y& zku{sm0jRfvEnov!4`iRgCom1B>h=0B+OAu@TZ{-<3)eAl6dVD&!5WZfb4*Tx26zG< zBlH{X@*9ELU0B@@#_15pWE*GI3K)F1-RgNuq!=!V{vyTiOuS8lQNV$M=dRFe7 zgW&}5E);JV-&vu)jzW|ZD7Q7$s4I1`;i&u6jAfv&t+wQ%V#UfGYM@zUs(ztGrj76fhc65<56Klb2n?wA!@?#PNd7>hJWpNPb@ynX7kb2== zFgho#2mOgSU$!MSMN~}LiMWYK($=0-y{kgedY!lS&<>>ZB?`Zh6yM-85@Je({BiYa z{Hq0GPkHjIM@*vvK~*cg=SZYi9&0aJ^)NaBzMI*1vDnm>_$*Pn#9Q;mjUw|t80PZCi95x-T8AWTofGYC z1;?#+$r&sZ6=Mp-dE7Cyz4dBcCt=;RWKl6`fw<^ax|OgmPL!6&QtS#&^TF{e?U}s8 zFN?5EoH%6S6nBbwwSHbr!>rk8mbB07Kv@6AItyTGt zOG^~?nDXq^aX-7d@{4A#<%Zh!n>^AWEem^$V^W_-A85eXRnxE1{pkdGV~$(ykXPIN zpyh8MTOeyONB5P=2Jc=l529)Hc5h`u0*Oyw%uyl&dbRv^4sN|S3FkTy73xf1ehUHp zxnLT^_1_YA5Z0d`C4#vA`-~)EGEVaT`|!6QttGDiKGlZ3mhy4^aNH2xYB^eTpYks0 z47cnle+N6tCjyYQ=UQ3#qnE^vAHT#AxM(s>wR9$`eTzW0qJF?qZ!f;+f5i3QPmZE! z#I1-QiN$(Lw+QU7pY_=-fIz(!jDi`^sfj~e|9*0{i3|Fr&apf$iutd8I@>j2Ry0D} qf9yQOd|XP`0Px$fJsC_R9Fekm^)6yFc5~vLBJ7Khz?LFx1e3YEOzAbEH&yOdIXcj_Y*vMk$wd3}4LnzBsCo6-XwIh_U2 z*+>Enb>Rt*v|a*8;4Hxa&m!O?!357HU?sr_&njRf!3@tXFeniK9{1b9jx(yN`n@<` w6d&)OyXqTpqP8#f1M2ZKJ-#l>lKpSr9|{bpV53r)BLDyZ07*qoM6N<$f{+fpK>z>% literal 0 HcmV?d00001 diff --git a/vector/src/main/res/drawable-xxxhdpi/arrow_up_circle.png b/vector/src/main/res/drawable-xxxhdpi/arrow_up_circle.png new file mode 100755 index 0000000000000000000000000000000000000000..61fd2b1e48dd4a03e94839c014b2de8fa0176a87 GIT binary patch literal 1910 zcmV-+2Z{KJP)Px+FiAu~RCodHoXu-pMHt3&rLD9W!4|SHHQ0q(m#u|T$fiV)NLStH#-(lwVg+}F zKyjyFn?g(6g)06DMX}U{BJ^Wf{2-B1>!O>Akyw*L{5>}_m)_iY=giDK_apVb@Z{V( z^UnM6oO5Q*nVFl$*nnkYV`C>Fj6ViNwBpfdG}cuxC~5T5F=R~P?>IOFUUt~e(TZb( zy1U?Khg%%4LfRdU+g~800j9x4umtK7*^*7!w#Xe`0ttEo%z92wp)^lJ2;&0!uhA* zR6!6L#;*fEI{nUPKeWr7ANF;-_|fi&D_4DvTtN9lM!vP2MqTMb&H-=}b$yXxkaD)Jq4#QCepBtpCy?(dQ4aNzgh5@-Raed< zbWW!x)GIy302E71luJFg8-~;&SM@o6qVq$#YDO3aivUXJRh@8-FM)*GV8r>rPy4I988%niUf0SnkF|ZbasZ89G|pe z1XWj3-V-+l^==h=ebD_qbhni!W$RI~f>@uVT+`p(P}a+yB^n`yrbFIJhV@-BgV^YH z)@s{Uj*&?adn+IKP~H}B#uxI^BY-8!x%$BtCC#Z=axEjCWL2E)1Gj+Au%OTTk_}`i zVmJ0Mc`j;-#uGpjPa#>Ou=XcpY>tD_bC$~VR$r?q2cTR+qTKD7Nh&pUY03FoBgGhw zM9z2m;45ZOVmTu-h#;KD63M!l0&aEzv6Q z*i$ZFEeBijja89aP6X;?uYhBzZ6bQIamCq;s3ld!a%*zp3DR15NVL*$BUF)ERx>%? zZCxZOH{}?TCMTXCEvi|+}k{)|HbGYw5vK~C47QQ{M#zaPA8Ql`OX!s>V`y|jEkUWLba~U>cApR z#y+M}m%b&*a`qoi$xhM@bXolP1odjn-wI5eEX)>F@6WpsqO`qoV2W3+}HxM}P)ixAeLE23k8p)ow0<_9G!>iQ2gR}QJ zYFX$v%4EM*!R8P6cnPB~f(LwmQ0Ty#=TgioJ21SCBW<;PA>g2RX~zXz8gw9(&LVs$ ziRI3ktP<-j;;y*yPtw-IlGQjXf+1J7a%^%^MbHthF5QMtq`2I)wJ#)X1Zf9*NYumE zvrQyqeeh+<%^Jm;ocJr~Y9!XzBX!%oDwbQ5lO{oQMt8{6>g?X$?kq&&w{lx{oU~>X zWy6|XJIKBI#NB2tr-KH0U;A=@i|P9pzA$inH&C|Si{6VQ@MT9GjP`Zm&^rT(KL!cF#a#2npS#(@<*1>GwH!mm( z{9X=RLYF%u?7jp)<(n5=u!GqCq8HrHz#k6!oc-dNLLYX^x4$@obbo3yktL^70vSdf z66$^fl_P1i1uu4nVS2QQGe(fRibmUj6G)o3IwDv8|J_LLuAS1s@mi}-}*DJg(G#`H{0A-f6<0X?Fl>zvKIo>#7)|I(bWFfx1Mt`tmQjibt`M6GBo`v^x&8TA|TN&<&*@fA!T!T)8b7aR2}S07*qoM6N<$f;Z-&p8x;= literal 0 HcmV?d00001 diff --git a/vector/src/main/res/drawable-xxxhdpi/chevron_down.png b/vector/src/main/res/drawable-xxxhdpi/chevron_down.png new file mode 100755 index 0000000000000000000000000000000000000000..31b1eb464ad2c87757a9a00c66b152a57548b87a GIT binary patch literal 584 zcmV-O0=NB%P)Px%0!c(cRA>d=nNLo_Kop1HD+w+!Bu0%JPXG%R#-%IWdKgdPHN1fvS-CWt5Ca=e zK!_Wa#1#pS{gpq0ly;``X4+vApzTcGyzeVa%M4Ro+U@fb#gki7bb)Gz+8 z$$sE(S$v#S(qurSnyZ}5^rdQiHR$!8SC5Cb-RgACc&Z+hqW$&U`Gcw6ijph;4;~G1 z0#B)n0P81)vp0jM?k0Rf*aUmD{Mx5*DWw{rO8A3UjTE%mq4*~sZLOSFgeXA^e$E$R zz}qDWCh%a7Fxq3l+XV?G@L°j*d(duASoEWr$($6y9KFQCl>&}CI_kv6$61we#N zpoK`^pSC%#2?$MsEj)NP1cV|%0)DH2s!Nc9&lFHq36k)+0;(oK8eU64RV288*A`H= z1Xu7z0?Lx$65d#VZwap93kmQg!3TU{0j?$Zg0~XjN`g;#YXQ;{e8Yajs)NE$SHSq7>%R~ zR+8`_vt=#vQ~DuCC-0h0Bgr=JqtWo_@br%O=QzFz9mKD1V!mhEyB@s0yh-+@6YCcf WVtT2XUdo;T0000 + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/view_jump_to_read_marker.xml b/vector/src/main/res/layout/view_jump_to_read_marker.xml index 35e14a649d..4ded65e8f8 100644 --- a/vector/src/main/res/layout/view_jump_to_read_marker.xml +++ b/vector/src/main/res/layout/view_jump_to_read_marker.xml @@ -11,11 +11,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginStart="16dp" - android:layout_marginLeft="16dp" android:layout_toStartOf="@+id/closeJumpToReadMarkerView" - android:layout_toLeftOf="@+id/closeJumpToReadMarkerView" - android:drawableStart="@drawable/jump_to_unread" - android:drawableLeft="@drawable/jump_to_unread" + android:drawableStart="@drawable/arrow_up_circle" android:drawablePadding="10dp" android:gravity="center_vertical" android:paddingTop="12dp" From 88fb9667a3e51cb90031ae8aa2ded9167974b7f7 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 18 Sep 2019 20:21:42 +0200 Subject: [PATCH 008/197] Timeline: continue fixing issues + read marker --- .../main/java/im/vector/matrix/rx/RxRoom.kt | 13 +++- .../matrix/android/api/session/room/Room.kt | 2 +- .../api/session/room/read/ReadService.kt | 17 ++++- .../session/room/timeline/TimelineService.kt | 2 +- .../matrix/android/api/util/Optional.kt | 40 ++++++++++ .../internal/session/room/DefaultRoom.kt | 2 +- .../session/room/read/DefaultReadService.kt | 22 ++++-- .../session/room/read/FullyReadContent.kt | 2 +- .../session/room/read/SetReadMarkersTask.kt | 73 +++++++++++------- .../room/timeline/DefaultTimelineService.kt | 2 +- .../session/sync/RoomFullyReadHandler.kt | 3 +- .../internal/session/sync/RoomSyncHandler.kt | 2 +- .../api/pushrules/PushrulesConditionTest.kt | 4 +- .../riotx/core/ui/views/ReadMarkerView.kt | 25 ++++--- .../im/vector/riotx/core/utils/Debouncer.kt | 6 ++ .../home/room/detail/RoomDetailFragment.kt | 74 ++++++++++--------- .../home/room/detail/RoomDetailViewModel.kt | 70 ++++++++++++------ .../home/room/detail/RoomDetailViewState.kt | 3 +- .../room/detail/ScrollOnNewMessageCallback.kt | 2 +- .../timeline/TimelineEventController.kt | 24 +++--- .../timeline/factory/DefaultItemFactory.kt | 3 +- .../timeline/factory/EncryptedItemFactory.kt | 3 +- .../factory/MergedHeaderItemFactory.kt | 16 ++-- .../timeline/factory/MessageItemFactory.kt | 5 +- .../timeline/factory/NoticeItemFactory.kt | 4 +- .../timeline/factory/TimelineItemFactory.kt | 13 ++-- .../helper/MessageInformationDataFactory.kt | 5 +- .../detail/timeline/item/AbsMessageItem.kt | 12 ++- .../detail/timeline/item/MergedHeaderItem.kt | 21 ++++++ .../timeline/item/MessageInformationData.kt | 1 + .../room/detail/timeline/item/NoticeItem.kt | 13 +++- 31 files changed, 331 insertions(+), 153 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Optional.kt rename matrix-sdk-android/src/main/java/im/vector/matrix/android/{api => internal}/session/room/read/FullyReadContent.kt (92%) diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt index 0ff0987dfe..c8cc430c65 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt @@ -21,13 +21,14 @@ import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary import im.vector.matrix.android.api.session.room.model.ReadReceipt import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.matrix.android.api.util.Optional import io.reactivex.Observable import io.reactivex.Single class RxRoom(private val room: Room) { fun liveRoomSummary(): Observable { - return room.liveRoomSummary().asObservable() + return room.getRoomSummaryLive().asObservable() } fun liveRoomMemberIds(): Observable> { @@ -39,7 +40,15 @@ class RxRoom(private val room: Room) { } fun liveTimelineEvent(eventId: String): Observable { - return room.liveTimeLineEvent(eventId).asObservable() + return room.getTimeLineEventLive(eventId).asObservable() + } + + fun liveReadMarker(): Observable> { + return room.getReadMarkerLive().asObservable() + } + + fun liveReadReceipt(): Observable> { + return room.getMyReadReceiptLive().asObservable() } fun loadRoomMembersIfNeeded(): Single = Single.create { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt index ec6b382f8f..9a4e0131d8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt @@ -47,7 +47,7 @@ interface Room : * A live [RoomSummary] associated with the room * You can observe this summary to get dynamic data from this room. */ - fun liveRoomSummary(): LiveData + fun getRoomSummaryLive(): LiveData fun roomSummary(): RoomSummary? diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt index 0ff0298b44..e315224880 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.api.session.room.read import androidx.lifecycle.LiveData import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.room.model.ReadReceipt +import im.vector.matrix.android.api.util.Optional /** * This interface defines methods to handle read receipts and read marker in a room. It's implemented at the room level. @@ -40,12 +41,24 @@ interface ReadService { */ fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback) + /** + * Check if an event is already read, ie. your read receipt is set on a more recent event. + */ fun isEventRead(eventId: String): Boolean /** - * Returns a nullable read marker for the room. + * Returns a live read marker id for the room. */ - fun getReadMarkerLive(): LiveData + fun getReadMarkerLive(): LiveData> + /** + * Returns a live read receipt id for the room. + */ + fun getMyReadReceiptLive(): LiveData> + + /** + * Returns a live list of read receipts for a given event + * @param eventId: the event + */ fun getEventReadReceiptsLive(eventId: String): LiveData> } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt index fdf99bd22c..b55bc17946 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt @@ -36,5 +36,5 @@ interface TimelineService { fun getTimeLineEvent(eventId: String): TimelineEvent? - fun liveTimeLineEvent(eventId: String): LiveData + fun getTimeLineEventLive(eventId: String): LiveData } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Optional.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Optional.kt new file mode 100644 index 0000000000..abe2d23993 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Optional.kt @@ -0,0 +1,40 @@ +/* + + * Copyright 2019 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.matrix.android.api.util + +data class Optional constructor(private val value: T?) { + + fun get(): T { + return value!! + } + + fun getOrNull(): T? { + return value + } + + fun getOrElse(fn: () -> T): T { + return value ?: fn() + } + + companion object { + fun from(value: T?): Optional { + return Optional(value) + } + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt index 492dd03543..10262ccebd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt @@ -53,7 +53,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String, RelationService by relationService, MembershipService by roomMembersService { - override fun liveRoomSummary(): LiveData { + override fun getRoomSummaryLive(): LiveData { val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm -> RoomSummaryEntity.where(realm, roomId).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt index 3709521cc3..1f0bae6fea 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt @@ -25,6 +25,7 @@ import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.room.model.ReadReceipt import im.vector.matrix.android.api.session.room.read.ReadService +import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.internal.database.RealmLiveData import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper import im.vector.matrix.android.internal.database.model.ChunkEntity @@ -83,24 +84,33 @@ internal class DefaultReadService @AssistedInject constructor(@Assisted private var isEventRead = false monarchy.doWithRealm { val readReceipt = ReadReceiptEntity.where(it, roomId, credentials.userId).findFirst() - ?: return@doWithRealm + ?: return@doWithRealm val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(it, roomId) - ?: return@doWithRealm + ?: return@doWithRealm val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex - ?: Int.MIN_VALUE + ?: Int.MIN_VALUE val eventToCheckIndex = liveChunk.timelineEvents.find(eventId)?.root?.displayIndex - ?: Int.MAX_VALUE + ?: Int.MAX_VALUE isEventRead = eventToCheckIndex <= readReceiptIndex } return isEventRead } - override fun getReadMarkerLive(): LiveData { + override fun getReadMarkerLive(): LiveData> { val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm -> ReadMarkerEntity.where(realm, roomId) } return Transformations.map(liveRealmData) { results -> - results.firstOrNull()?.eventId + Optional.from(results.firstOrNull()?.eventId) + } + } + + override fun getMyReadReceiptLive(): LiveData> { + val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm -> + ReadReceiptEntity.where(realm, roomId = roomId, userId = credentials.userId) + } + return Transformations.map(liveRealmData) { results -> + Optional.from(results.firstOrNull()?.eventId) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/FullyReadContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/FullyReadContent.kt similarity index 92% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/FullyReadContent.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/FullyReadContent.kt index a73b9ef5b7..6790ea658c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/FullyReadContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/FullyReadContent.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.api.session.room.read +package im.vector.matrix.android.internal.session.room.read import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt index 26eb16b15c..beaf4eb0af 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt @@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.session.room.read import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.auth.data.Credentials -import im.vector.matrix.android.api.session.room.read.FullyReadContent import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ReadMarkerEntity import im.vector.matrix.android.internal.database.model.ReadReceiptEntity @@ -76,29 +75,49 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI if (fullyReadEventId != null && isReadMarkerMoreRecent(params.roomId, fullyReadEventId)) { if (LocalEchoEventFactory.isLocalEchoId(fullyReadEventId)) { - Timber.w("Can't set read marker for local event ${params.fullyReadEventId}") + Timber.w("Can't set read marker for local event $fullyReadEventId") } else { - updateReadMarker(params.roomId, fullyReadEventId) markers[READ_MARKER] = fullyReadEventId } } + if (readReceiptEventId != null - && !isEventRead(params.roomId, readReceiptEventId)) { + && !isEventRead(params.roomId, readReceiptEventId)) { if (LocalEchoEventFactory.isLocalEchoId(readReceiptEventId)) { - Timber.w("Can't set read receipt for local event ${params.fullyReadEventId}") + Timber.w("Can't set read receipt for local event $readReceiptEventId") } else { - updateNotificationCountIfNecessary(params.roomId, readReceiptEventId) markers[READ_RECEIPT] = readReceiptEventId } } if (markers.isEmpty()) { return } + updateDatabase(params.roomId, markers) executeRequest { apiCall = roomAPI.sendReadMarker(params.roomId, markers) } } + private suspend fun updateDatabase(roomId: String, markers: HashMap) { + monarchy.awaitTransaction { realm -> + val readMarkerId = markers[READ_MARKER] + val readReceiptId = markers[READ_RECEIPT] + + if (readMarkerId != null) { + roomFullyReadHandler.handle(realm, roomId, FullyReadContent(readMarkerId)) + } + if (readReceiptId != null) { + val isLatestReceived = TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId == readReceiptId + if (isLatestReceived) { + val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() + ?: return@awaitTransaction + roomSummary.notificationCount = 0 + roomSummary.highlightCount = 0 + } + } + } + } + private fun isReadMarkerMoreRecent(roomId: String, fullyReadEventId: String): Boolean { return Realm.getInstance(monarchy.realmConfiguration).use { realm -> @@ -111,36 +130,36 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI } } - private suspend fun updateReadMarker(roomId: String, eventId: String) { - monarchy.awaitTransaction { realm -> - roomFullyReadHandler.handle(realm, roomId, FullyReadContent(eventId)) - } - } - - private suspend fun updateNotificationCountIfNecessary(roomId: String, eventId: String) { - monarchy.awaitTransaction { realm -> - val isLatestReceived = TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId == eventId - if (isLatestReceived) { - val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() - ?: return@awaitTransaction - roomSummary.notificationCount = 0 - roomSummary.highlightCount = 0 - } - } - } private fun isEventRead(roomId: String, eventId: String): Boolean { return Realm.getInstance(monarchy.realmConfiguration).use { realm -> val readReceipt = ReadReceiptEntity.where(realm, roomId, credentials.userId).findFirst() - ?: return false + ?: return false val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) - ?: return false + ?: return false val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex - ?: Int.MIN_VALUE + ?: Int.MIN_VALUE val eventToCheckIndex = liveChunk.timelineEvents.find(eventId)?.root?.displayIndex - ?: Int.MAX_VALUE + ?: Int.MAX_VALUE eventToCheckIndex <= readReceiptIndex } } + private fun SetReadMarkersTask.Params.fullyReadEventId(): String? { + if (fullyReadEventId != null) { + return this.fullyReadEventId + } else { + Realm.getInstance(monarchy.realmConfiguration).use { realm -> + val readReceipt = ReadReceiptEntity.where(realm, roomId, credentials.userId).findFirst() + val readMarker = ReadMarkerEntity.where(realm, roomId).findFirst() + return if (readMarker?.eventId == readReceipt?.eventId) { + readReceiptEventId + } else { + null + } + } + } + } + + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt index 59d37a8062..0ded458a20 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt @@ -73,7 +73,7 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv }) } - override fun liveTimeLineEvent(eventId: String): LiveData { + override fun getTimeLineEventLive(eventId: String): LiveData { val liveData = RealmLiveData(monarchy.realmConfiguration) { TimelineEventEntity.where(it, eventId = eventId) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt index 45fbe7329d..99fbc5750d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt @@ -16,8 +16,7 @@ package im.vector.matrix.android.internal.session.sync -import im.vector.matrix.android.api.session.room.read.FullyReadContent -import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.session.room.read.FullyReadContent import im.vector.matrix.android.internal.database.model.ReadMarkerEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt index 344833cca7..906963d83a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt @@ -23,7 +23,7 @@ import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.tag.RoomTagContent -import im.vector.matrix.android.api.session.room.read.FullyReadContent +import im.vector.matrix.android.internal.session.room.read.FullyReadContent import im.vector.matrix.android.internal.database.helper.add import im.vector.matrix.android.internal.database.helper.addOrUpdate import im.vector.matrix.android.internal.database.helper.addStateEvent diff --git a/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt b/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt index 3d4df602b7..2c518fa6ee 100644 --- a/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt +++ b/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt @@ -184,7 +184,7 @@ class PushrulesConditionTest { } class MockRoom(override val roomId: String, val _numberOfJoinedMembers: Int) : Room { - override fun liveTimeLineEvent(eventId: String): LiveData { + override fun getTimeLineEventLive(eventId: String): LiveData { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } @@ -193,7 +193,7 @@ class PushrulesConditionTest { return _numberOfJoinedMembers } - override fun liveRoomSummary(): LiveData { + override fun getRoomSummaryLive(): LiveData { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt index f5f086ac8b..986acef616 100644 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt @@ -23,8 +23,8 @@ import android.util.AttributeSet import android.view.View import android.view.animation.Animation import android.view.animation.AnimationUtils +import androidx.core.view.isInvisible import im.vector.riotx.R -import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import kotlinx.coroutines.* private const val DELAY_IN_MS = 1_500L @@ -36,30 +36,34 @@ class ReadMarkerView @JvmOverloads constructor( ) : View(context, attrs, defStyleAttr) { interface Callback { - fun onReadMarkerDisplayed() + fun onReadMarkerLongBound() } + private var eventId: String? = null private var callback: Callback? = null private var callbackDispatcherJob: Job? = null - fun bindView(displayReadMarker: Boolean, readMarkerCallback: Callback) { + fun bindView(eventId: String?, hasReadMarker: Boolean, displayReadMarker: Boolean, readMarkerCallback: Callback) { + this.eventId = eventId this.callback = readMarkerCallback if (displayReadMarker) { - visibility = VISIBLE - callbackDispatcherJob = GlobalScope.launch(Dispatchers.Main) { - delay(DELAY_IN_MS) - callback?.onReadMarkerDisplayed() - } startAnimation() } else { - visibility = INVISIBLE + this.animation?.cancel() + this.visibility = INVISIBLE + } + if (hasReadMarker) { + callbackDispatcherJob = GlobalScope.launch(Dispatchers.Main) { + delay(DELAY_IN_MS) + callback?.onReadMarkerLongBound() + } } - } fun unbind() { this.callbackDispatcherJob?.cancel() this.callback = null + this.eventId = null this.animation?.cancel() this.visibility = INVISIBLE } @@ -80,6 +84,7 @@ class ReadMarkerView @JvmOverloads constructor( override fun onAnimationRepeat(animation: Animation) {} }) } + visibility = VISIBLE animation.start() } diff --git a/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt b/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt index 8c8bd1266f..5001449c3f 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt @@ -33,6 +33,10 @@ internal class Debouncer(private val handler: Handler) { return true } + fun cancelAll() { + handler.removeCallbacksAndMessages(null) + } + private fun insertRunnable(identifier: String, r: Runnable, millis: Long) { val chained = Runnable { handler.post(r) @@ -41,4 +45,6 @@ internal class Debouncer(private val handler: Handler) { runnables[identifier] = chained handler.postDelayed(chained, millis) } + + } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 48cdea6e59..0bb45ccfb8 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -44,7 +44,6 @@ import androidx.core.content.ContextCompat import androidx.core.util.Pair import androidx.core.view.ViewCompat import androidx.core.view.forEach -import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager @@ -312,17 +311,21 @@ class RoomDetailFragment : } } + override fun onDestroy() { + debouncer.cancelAll() + super.onDestroy() + } + private fun setupJumpToBottomView() { - jumpToBottomView.isVisible = false + jumpToBottomView.visibility = View.INVISIBLE jumpToBottomView.setOnClickListener { + jumpToBottomView.visibility = View.INVISIBLE withState(roomDetailViewModel) { state -> - recyclerView.stopScroll() if (state.timeline?.isLive == false) { state.timeline.restartWithEventId(null) } else { layoutManager.scrollToPosition(0) } - jumpToBottomView.isVisible = false } } } @@ -398,17 +401,17 @@ class RoomDetailFragment : if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { val parser = Parser.builder().build() val document = parser.parse(messageContent.formattedBody - ?: messageContent.body) + ?: messageContent.body) formattedBody = eventHtmlRenderer.render(document) } composerLayout.composerRelatedMessageContent.text = formattedBody - ?: nonFormattedBody + ?: nonFormattedBody composerLayout.composerEditText.setText(if (useText) event.getTextEditableContent() else "") composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes)) avatarRenderer.render(event.senderAvatar, event.root.senderId - ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) + ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length) composerLayout.expand { @@ -437,9 +440,9 @@ class RoomDetailFragment : REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data) REACTION_SELECT_REQUEST_CODE -> { val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID) - ?: return + ?: return val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT) - ?: return + ?: return //TODO check if already reacted with that? roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId)) } @@ -490,33 +493,33 @@ class RoomDetailFragment : if (vectorPreferences.swipeToReplyIsEnabled()) { val swipeCallback = RoomMessageTouchHelperCallback(requireContext(), - R.drawable.ic_reply, - object : RoomMessageTouchHelperCallback.QuickReplayHandler { - override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { - (model as? AbsMessageItem)?.attributes?.informationData?.let { - val eventId = it.eventId - roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId)) - } - } + R.drawable.ic_reply, + object : RoomMessageTouchHelperCallback.QuickReplayHandler { + override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { + (model as? AbsMessageItem)?.attributes?.informationData?.let { + val eventId = it.eventId + roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId)) + } + } - override fun canSwipeModel(model: EpoxyModel<*>): Boolean { - return when (model) { - is MessageFileItem, - is MessageImageVideoItem, - is MessageTextItem -> { - return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED - } - else -> false - } - } - }) + override fun canSwipeModel(model: EpoxyModel<*>): Boolean { + return when (model) { + is MessageFileItem, + is MessageImageVideoItem, + is MessageTextItem -> { + return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED + } + else -> false + } + } + }) val touchHelper = ItemTouchHelper(swipeCallback) touchHelper.attachToRecyclerView(recyclerView) } } private fun updateJumpToBottomViewVisibility() { - debouncer.debounce("jump_to_bottom_visibility", 100, Runnable { + debouncer.debounce("jump_to_bottom_visibility", 250, Runnable { Timber.v("First visible: ${layoutManager.findFirstCompletelyVisibleItemPosition()}") if (layoutManager.findFirstCompletelyVisibleItemPosition() != 0) { jumpToBottomView.show() @@ -684,7 +687,7 @@ class RoomDetailFragment : val summary = state.asyncRoomSummary() val inviter = state.asyncInviter() if (summary?.membership == Membership.JOIN) { - timelineEventController.setTimeline(state.timeline, state.highlightedEventId) + timelineEventController.update(state.timeline, state.highlightedEventId, state.hideReadMarker) inviteView.visibility = View.GONE val uid = session.myUserId val meMember = session.getRoom(state.roomId)?.getRoomMember(uid) @@ -747,7 +750,6 @@ class RoomDetailFragment : } } - private fun renderSendMessageResult(sendMessageResult: SendMessageResult) { when (sendMessageResult) { is SendMessageResult.MessageSent -> { @@ -945,15 +947,15 @@ class RoomDetailFragment : .show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS") } - override fun onReadMarkerLongDisplayed(informationData: MessageInformationData) { + override fun onReadMarkerLongDisplayed() = withState(roomDetailViewModel) { state -> val firstVisibleItem = layoutManager.findFirstVisibleItemPosition() - val eventId = timelineEventController.searchEventIdAtPosition(firstVisibleItem) - if (eventId != null) { - roomDetailViewModel.process(RoomDetailActions.SetReadMarkerAction(eventId)) + val nextReadMarkerId = timelineEventController.searchEventIdAtPosition(firstVisibleItem) + if (nextReadMarkerId != null) { + roomDetailViewModel.process(RoomDetailActions.SetReadMarkerAction(nextReadMarkerId)) } } -// AutocompleteUserPresenter.Callback + // AutocompleteUserPresenter.Callback override fun onQueryUsers(query: CharSequence?) { textComposerViewModel.process(TextComposerActions.QueryUsers(query)) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index ee4b3c0423..afe4ea6681 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -40,13 +40,13 @@ import im.vector.matrix.android.api.session.events.model.isTextMessage import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.room.model.Membership -import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineSettings +import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.matrix.rx.rx @@ -62,6 +62,7 @@ import im.vector.riotx.features.command.ParsedCommand import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents import im.vector.riotx.features.settings.VectorPreferences import io.reactivex.Observable +import io.reactivex.functions.BiFunction import io.reactivex.functions.Function3 import io.reactivex.rxkotlin.subscribeBy import org.commonmark.parser.Parser @@ -116,6 +117,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro observeEventDisplayedActions() observeSummaryState() observeJumpToReadMarkerViewVisibility() + observeReadMarkerVisibility() room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear() timeline.start() setState { copy(timeline = this@RoomDetailViewModel.timeline) } @@ -156,7 +158,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private fun handleTombstoneEvent(action: RoomDetailActions.HandleTombstoneEvent) { val tombstoneContent = action.event.getClearContent().toModel() - ?: return + ?: return val roomId = tombstoneContent.replacementRoom ?: "" val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN @@ -303,7 +305,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro //is original event a reply? val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel()?.relatesTo?.inReplyTo?.eventId - ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId + ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId if (inReplyTo != null) { //TODO check if same content? room.getTimeLineEvent(inReplyTo)?.let { @@ -312,12 +314,12 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } else { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val existingBody = messageContent?.body ?: "" if (existingBody != action.text) { room.editTextMessage(state.sendMode.timelineEvent.root.eventId - ?: "", messageContent?.type - ?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown) + ?: "", messageContent?.type + ?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown) } else { Timber.w("Same message content, do not send edition") } @@ -332,7 +334,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is SendMode.QUOTE -> { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val textMsg = messageContent?.body val finalText = legacyRiotQuoteText(textMsg, action.text) @@ -635,29 +637,30 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } private fun observeJumpToReadMarkerViewVisibility() { - Observable - .combineLatest( - room.rx().liveRoomSummary().map { + Observable.combineLatest( + room.rx().liveRoomSummary() + .map { val readMarkerId = it.readMarkerId if (readMarkerId == null) { Option.empty() } else { - val timelineEvent = room.getTimeLineEvent(readMarkerId) - Option.fromNullable(timelineEvent) - } - }.distinctUntilChanged(), - visibleEventsObservable.distinctUntilChanged(), - isEventVisibleObservable { it.hasReadMarker }.startWith(false), - Function3, RoomDetailActions.TimelineEventTurnsVisible, Boolean, Boolean> { readMarkerEvent, currentVisibleEvent, isReadMarkerViewVisible -> - if (readMarkerEvent.isEmpty() || isReadMarkerViewVisible) { - false - } else { - val readMarkerPosition = readMarkerEvent.map { it.displayIndex }.getOrElse { Int.MIN_VALUE } - val currentVisibleEventPosition = currentVisibleEvent.event.displayIndex - readMarkerPosition < currentVisibleEventPosition + val readMarkerIndex = room.getTimeLineEvent(readMarkerId)?.displayIndex + ?: Int.MIN_VALUE + Option.just(readMarkerIndex) } } - ) + .distinctUntilChanged(), + visibleEventsObservable.distinctUntilChanged(), + isEventVisibleObservable { it.hasReadMarker }.startWith(false).takeUntil { it }, + Function3, RoomDetailActions.TimelineEventTurnsVisible, Boolean, Boolean> { readMarkerIndex, currentVisibleEvent, isReadMarkerViewVisible -> + if (readMarkerIndex.isEmpty() || isReadMarkerViewVisible) { + false + } else { + val currentVisibleEventPosition = currentVisibleEvent.event.displayIndex + readMarkerIndex.getOrElse { Int.MIN_VALUE } < currentVisibleEventPosition + } + } + ) .distinctUntilChanged() .subscribe { setState { copy(showJumpToReadMarker = it) } @@ -682,6 +685,25 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } + private fun observeReadMarkerVisibility() { + Observable + .combineLatest( + room.rx().liveReadMarker(), + room.rx().liveReadReceipt(), + BiFunction, Optional, Boolean> { readMarker, readReceipt -> + readMarker.getOrNull() == readReceipt.getOrNull() + } + ) + .throttleLast(250, TimeUnit.MILLISECONDS) + .distinctUntilChanged() + .startWith(false) + .subscribe { + setState { copy(hideReadMarker = it) } + } + .disposeOnClear() + } + + private fun observeSummaryState() { asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary -> if (summary.membership == Membership.INVITE) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt index 5e36cf42dc..bf11740fc0 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt @@ -53,7 +53,8 @@ data class RoomDetailViewState( val tombstoneEventHandling: Async = Uninitialized, val syncState: SyncState = SyncState.IDLE, val showJumpToReadMarker: Boolean = false, - val highlightedEventId: String? = null + val highlightedEventId: String? = null, + val hideReadMarker: Boolean = false ) : MvRxState { constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnNewMessageCallback.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnNewMessageCallback.kt index f4cfe9eb5a..998428477b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnNewMessageCallback.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnNewMessageCallback.kt @@ -26,7 +26,7 @@ class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager, override fun onInserted(position: Int, count: Int) { Timber.v("On inserted $count count at position: $position") - if (position == 0 && layoutManager.findFirstVisibleItemPosition() == 0 && !timelineEventController.isLoadingForward()) { + if (position == 0 && layoutManager.findFirstCompletelyVisibleItemPosition() == 0 && !timelineEventController.isLoadingForward()) { layoutManager.scrollToPosition(0) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index 652f35fb67..b5a5fe8ca8 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -31,8 +31,6 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.epoxy.LoadingItem_ import im.vector.riotx.core.extensions.localDateTime -import im.vector.riotx.core.resources.UserPreferencesProvider -import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.factory.MergedHeaderItemFactory import im.vector.riotx.features.home.room.detail.timeline.factory.TimelineItemFactory import im.vector.riotx.features.home.room.detail.timeline.helper.* @@ -80,7 +78,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec interface ReadReceiptsCallback { fun onReadReceiptsClicked(readReceipts: List) - fun onReadMarkerLongDisplayed(informationData: MessageInformationData) + fun onReadMarkerLongDisplayed() } interface UrlClickCallback { @@ -142,7 +140,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec requestModelBuild() } - fun setTimeline(timeline: Timeline?, eventIdToHighlight: String?) { + fun update(timeline: Timeline?, eventIdToHighlight: String?, hideReadMarker: Boolean) { if (this.timeline != timeline) { this.timeline = timeline this.timeline?.listener = this @@ -155,22 +153,30 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } } + var requestModelBuild = false if (this.eventIdToHighlight != eventIdToHighlight) { // Clear cache to force a refresh synchronized(modelCache) { for (i in 0 until modelCache.size) { if (modelCache[i]?.eventId == eventIdToHighlight - || modelCache[i]?.eventId == this.eventIdToHighlight) { + || modelCache[i]?.eventId == this.eventIdToHighlight) { modelCache[i] = null } } } this.eventIdToHighlight = eventIdToHighlight - + requestModelBuild = true + } + if (this.hideReadMarker != hideReadMarker) { + this.hideReadMarker = hideReadMarker + requestModelBuild = true + } + if (requestModelBuild) { requestModelBuild() } } + private var hideReadMarker: Boolean = false private var eventIdToHighlight: String? = null override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { @@ -224,8 +230,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec // Should be build if not cached or if cached but contains mergedHeader or formattedDay // We then are sure we always have items up to date. if (modelCache[position] == null - || modelCache[position]?.mergedHeaderModel != null - || modelCache[position]?.formattedDayModel != null) { + || modelCache[position]?.mergedHeaderModel != null + || modelCache[position]?.formattedDayModel != null) { modelCache[position] = buildItemModels(position, currentSnapshot) } } @@ -251,7 +257,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec val nextDate = nextEvent?.root?.localDateTime() val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate() - val eventModel = timelineItemFactory.create(event, nextEvent, eventIdToHighlight, callback).also { + val eventModel = timelineItemFactory.create(event, nextEvent, eventIdToHighlight, hideReadMarker, callback).also { it.id(event.localId) it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt index dd50bbf190..a387f3f496 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt @@ -31,6 +31,7 @@ class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: Ava fun create(event: TimelineEvent, highlight: Boolean, + hideReadMarker: Boolean, callback: TimelineEventController.Callback?, exception: Exception? = null): DefaultItem? { val text = if (exception == null) { @@ -39,7 +40,7 @@ class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: Ava "an exception occurred when rendering the event ${event.root.eventId}" } - val informationData = informationDataFactory.create(event, null) + val informationData = informationDataFactory.create(event, null, hideReadMarker) return DefaultItem_() .leftGuideline(avatarSizeProvider.leftGuideline) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt index 92f586ab7b..663762850a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt @@ -41,6 +41,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat fun create(event: TimelineEvent, nextEvent: TimelineEvent?, highlight: Boolean, + hideReadMarker: Boolean, callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? { event.root.eventId ?: return null @@ -64,7 +65,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat // TODO This is not correct format for error, change it - val informationData = messageInformationDataFactory.create(event, nextEvent) + val informationData = messageInformationDataFactory.create(event, nextEvent, hideReadMarker) val attributes = attributesFactory.create(null, informationData, callback) return MessageTextItem_() .leftGuideline(avatarSizeProvider.leftGuideline) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index 42f0688e50..80b3aa261b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -51,18 +51,22 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act return if (!event.canBeMerged() || (nextEvent?.root?.getClearType() == event.root.getClearType() && !addDaySeparator)) { null } else { - var highlighted = false - var showReadMarker = false val prevSameTypeEvents = items.prevSameTypeEvents(currentPosition, 2) if (prevSameTypeEvents.isEmpty()) { null } else { + var highlighted = false + var readMarkerId: String? = null + var showReadMarker = false val mergedEvents = (prevSameTypeEvents + listOf(event)).asReversed() val mergedData = ArrayList(mergedEvents.size) mergedEvents.forEach { mergedEvent -> if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) { highlighted = true } + if (readMarkerId == null && mergedEvent.hasReadMarker) { + readMarkerId = mergedEvent.root.eventId + } if (!showReadMarker && mergedEvent.displayReadMarker(sessionHolder.getActiveSession().myUserId)) { showReadMarker = true } @@ -81,7 +85,7 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act // => handle case where paginating from mergeable events and we get more val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull() val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey) - ?: true + ?: true val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState } if (isCollapsed) { collapsedEventIds.addAll(mergedEventIds) @@ -97,12 +101,14 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act mergeItemCollapseStates[event.localId] = it requestModelBuild() }, - showReadMarker = showReadMarker + readMarkerId = readMarkerId, + showReadMarker = isCollapsed && showReadMarker, + readReceiptsCallback = callback ) MergedHeaderItem_() .id(mergeId) .leftGuideline(avatarSizeProvider.leftGuideline) - .highlighted(highlighted) + .highlighted(isCollapsed && highlighted) .attributes(attributes) .also { it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents)) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 24d5abfd94..2cf5a60c44 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -76,11 +76,12 @@ class MessageItemFactory @Inject constructor( fun create(event: TimelineEvent, nextEvent: TimelineEvent?, highlight: Boolean, + hideReadMarker: Boolean, callback: TimelineEventController.Callback? ): VectorEpoxyModel<*>? { event.root.eventId ?: return null - val informationData = messageInformationDataFactory.create(event, nextEvent) + val informationData = messageInformationDataFactory.create(event, nextEvent, hideReadMarker) if (event.root.isRedacted()) { //message is redacted @@ -97,7 +98,7 @@ class MessageItemFactory @Inject constructor( || event.isEncrypted() && event.root.content.toModel()?.relatesTo?.type == RelationType.REPLACE ) { // This is an edit event, we should it when debugging as a notice event - return noticeItemFactory.create(event, highlight, callback) + return noticeItemFactory.create(event, highlight, hideReadMarker, callback) } val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt index bb301cdcbd..2f774cd9ec 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt @@ -36,9 +36,11 @@ class NoticeItemFactory @Inject constructor( fun create(event: TimelineEvent, highlight: Boolean, + hideReadMarker: Boolean, callback: TimelineEventController.Callback?): NoticeItem? { + val formattedText = eventFormatter.format(event) ?: return null - val informationData = informationDataFactory.create(event, null) + val informationData = informationDataFactory.create(event, null, hideReadMarker) val attributes = NoticeItem.Attributes( avatarRenderer = avatarRenderer, informationData = informationData, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index d75d43f840..18254120af 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -33,13 +33,14 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me fun create(event: TimelineEvent, nextEvent: TimelineEvent?, eventIdToHighlight: String?, + hideReadMarker: Boolean, callback: TimelineEventController.Callback?): VectorEpoxyModel<*> { val highlight = event.root.eventId == eventIdToHighlight val computedModel = try { when (event.root.getClearType()) { - EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, callback) + EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, hideReadMarker, callback) // State and call EventType.STATE_ROOM_TOMBSTONE, EventType.STATE_ROOM_NAME, @@ -51,22 +52,22 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.CALL_ANSWER, EventType.REACTION, EventType.REDACTION, - EventType.ENCRYPTION -> noticeItemFactory.create(event, highlight, callback) + EventType.ENCRYPTION -> noticeItemFactory.create(event, highlight, hideReadMarker, callback) // State room create EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback) // Crypto EventType.ENCRYPTED -> { if (event.root.isRedacted()) { // Redacted event, let the MessageItemFactory handle it - messageItemFactory.create(event, nextEvent, highlight, callback) + messageItemFactory.create(event, nextEvent, highlight, hideReadMarker, callback) } else { - encryptedItemFactory.create(event, nextEvent, highlight, callback) + encryptedItemFactory.create(event, nextEvent, highlight, hideReadMarker, callback) } } // Unhandled event types (yet) EventType.STATE_ROOM_THIRD_PARTY_INVITE, - EventType.STICKER -> defaultItemFactory.create(event, highlight, callback) + EventType.STICKER -> defaultItemFactory.create(event, highlight, hideReadMarker, callback) else -> { Timber.v("Type ${event.root.getClearType()} not handled") null @@ -74,7 +75,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me } } catch (e: Exception) { Timber.e(e, "failed to create message item") - defaultItemFactory.create(event, highlight, callback, e) + defaultItemFactory.create(event, highlight, hideReadMarker, callback, e) } return (computedModel ?: EmptyItem_()) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index b8a89a4669..453f7e4cd9 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -41,7 +41,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses private val dateFormatter: VectorDateFormatter, private val colorProvider: ColorProvider) { - fun create(event: TimelineEvent, nextEvent: TimelineEvent?): MessageInformationData { + fun create(event: TimelineEvent, nextEvent: TimelineEvent?, hideReadMarker: Boolean): MessageInformationData { // Non nullability has been tested before val eventId = event.root.eventId!! @@ -65,7 +65,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId ?: "")) } - val displayReadMarker = event.displayReadMarker(session.myUserId) + val displayReadMarker = !hideReadMarker && event.displayReadMarker(session.myUserId) return MessageInformationData( eventId = eventId, @@ -91,6 +91,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses ReadReceiptData(it.user.userId, it.user.avatarUrl, it.user.displayName, it.originServerTs) } .toList(), + hasReadMarker = event.hasReadMarker, displayReadMarker = displayReadMarker ) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt index 44a5e2bdfb..408a997efd 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -56,8 +56,9 @@ abstract class AbsMessageItem : BaseEventItem() { }) private val _readMarkerCallback = object : ReadMarkerView.Callback { - override fun onReadMarkerDisplayed() { - attributes.readReceiptsCallback?.onReadMarkerLongDisplayed(attributes.informationData) + + override fun onReadMarkerLongBound() { + attributes.readReceiptsCallback?.onReadMarkerLongDisplayed() } } @@ -106,7 +107,12 @@ abstract class AbsMessageItem : BaseEventItem() { holder.memberNameView.setOnLongClickListener(null) } holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener) - holder.readMarkerView.bindView(attributes.informationData.displayReadMarker, _readMarkerCallback) + holder.readMarkerView.bindView( + attributes.informationData.eventId, + attributes.informationData.hasReadMarker, + attributes.informationData.displayReadMarker, + _readMarkerCallback + ) if (!shouldShowReactionAtBottom() || attributes.informationData.orderedReactionList.isNullOrEmpty()) { holder.reactionWrapper?.isVisible = false diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt index f07575e1a5..de105b2261 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt @@ -25,7 +25,9 @@ import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R +import im.vector.riotx.core.ui.views.ReadMarkerView import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController @EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo) abstract class MergedHeaderItem : BaseEventItem() { @@ -37,6 +39,13 @@ abstract class MergedHeaderItem : BaseEventItem() { attributes.mergeData.distinctBy { it.userId } } + private val _readMarkerCallback = object : ReadMarkerView.Callback { + + override fun onReadMarkerLongBound() { + attributes.readReceiptsCallback?.onReadMarkerLongDisplayed() + } + } + override fun getViewType() = STUB_ID override fun bind(holder: Holder) { @@ -68,6 +77,16 @@ abstract class MergedHeaderItem : BaseEventItem() { } // No read receipt for this item holder.readReceiptsView.isVisible = false + holder.readMarkerView.bindView( + attributes.readMarkerId, + !attributes.readMarkerId.isNullOrEmpty(), + attributes.showReadMarker, + _readMarkerCallback) + } + + override fun unbind(holder: Holder) { + holder.readMarkerView.unbind() + super.unbind(holder) } data class Data( @@ -78,10 +97,12 @@ abstract class MergedHeaderItem : BaseEventItem() { ) data class Attributes( + val readMarkerId: String?, val isCollapsed: Boolean, val showReadMarker: Boolean, val mergeData: List, val avatarRenderer: AvatarRenderer, + val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null, val onCollapsedStateChanged: (Boolean) -> Unit ) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt index 041b6dbddd..09d51cacfd 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt @@ -34,6 +34,7 @@ data class MessageInformationData( val hasBeenEdited: Boolean = false, val hasPendingEdits: Boolean = false, val readReceipts: List = emptyList(), + val hasReadMarker: Boolean = false, val displayReadMarker: Boolean = false ) : Parcelable diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt index 8e61a3be1f..89270ce026 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt @@ -38,8 +38,8 @@ abstract class NoticeItem : BaseEventItem() { }) private val _readMarkerCallback = object : ReadMarkerView.Callback { - override fun onReadMarkerDisplayed() { - attributes.readReceiptsCallback?.onReadMarkerLongDisplayed(attributes.informationData) + override fun onReadMarkerLongBound() { + attributes.readReceiptsCallback?.onReadMarkerLongDisplayed() } } @@ -50,12 +50,17 @@ abstract class NoticeItem : BaseEventItem() { attributes.informationData.avatarUrl, attributes.informationData.senderId, attributes.informationData.memberName?.toString() - ?: attributes.informationData.senderId, + ?: attributes.informationData.senderId, holder.avatarImageView ) holder.view.setOnLongClickListener(attributes.itemLongClickListener) holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener) - holder.readMarkerView.bindView(attributes.informationData.displayReadMarker, _readMarkerCallback) + holder.readMarkerView.bindView( + attributes.informationData.eventId, + attributes.informationData.hasReadMarker, + attributes.informationData.displayReadMarker, + _readMarkerCallback + ) } override fun unbind(holder: Holder) { From 9668487b6b7dc80ed384e3a6da0dcb065d4baaa1 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 19 Sep 2019 16:17:58 +0200 Subject: [PATCH 009/197] Timeline/Read: update read receipt locally to --- .../session/room/read/DefaultReadService.kt | 1 - .../session/room/read/SetReadMarkersTask.kt | 37 ++++++------------- .../session/sync/ReadReceiptHandler.kt | 16 ++++++++ .../riotx/core/ui/views/ReadMarkerView.kt | 2 + .../home/room/detail/RoomDetailFragment.kt | 2 +- .../home/room/detail/RoomDetailViewModel.kt | 16 ++++---- .../timeline/TimelineEventController.kt | 29 +++++++-------- 7 files changed, 50 insertions(+), 53 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt index 1f0bae6fea..24263c1047 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt @@ -32,7 +32,6 @@ import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ReadMarkerEntity import im.vector.matrix.android.internal.database.model.ReadReceiptEntity import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity -import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.query.find import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom import im.vector.matrix.android.internal.database.query.where diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt index beaf4eb0af..16be19a867 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt @@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory +import im.vector.matrix.android.internal.session.sync.ReadReceiptHandler import im.vector.matrix.android.internal.session.sync.RoomFullyReadHandler import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.awaitTransaction @@ -53,7 +54,8 @@ private const val READ_RECEIPT = "m.read" internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI: RoomAPI, private val credentials: Credentials, private val monarchy: Monarchy, - private val roomFullyReadHandler: RoomFullyReadHandler + private val roomFullyReadHandler: RoomFullyReadHandler, + private val readReceiptHandler: ReadReceiptHandler ) : SetReadMarkersTask { override suspend fun execute(params: SetReadMarkersTask.Params) { @@ -82,7 +84,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI } if (readReceiptEventId != null - && !isEventRead(params.roomId, readReceiptEventId)) { + && !isEventRead(params.roomId, readReceiptEventId)) { if (LocalEchoEventFactory.isLocalEchoId(readReceiptEventId)) { Timber.w("Can't set read receipt for local event $readReceiptEventId") } else { @@ -102,15 +104,16 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI monarchy.awaitTransaction { realm -> val readMarkerId = markers[READ_MARKER] val readReceiptId = markers[READ_RECEIPT] - if (readMarkerId != null) { roomFullyReadHandler.handle(realm, roomId, FullyReadContent(readMarkerId)) } if (readReceiptId != null) { + val readReceiptContent = ReadReceiptHandler.createContent(credentials.userId, readReceiptId) + readReceiptHandler.handle(realm, roomId, readReceiptContent, false) val isLatestReceived = TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId == readReceiptId if (isLatestReceived) { val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() - ?: return@awaitTransaction + ?: return@awaitTransaction roomSummary.notificationCount = 0 roomSummary.highlightCount = 0 } @@ -118,7 +121,6 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI } } - private fun isReadMarkerMoreRecent(roomId: String, fullyReadEventId: String): Boolean { return Realm.getInstance(monarchy.realmConfiguration).use { realm -> val readMarkerEntity = ReadMarkerEntity.where(realm, roomId = roomId).findFirst() @@ -130,36 +132,19 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI } } - private fun isEventRead(roomId: String, eventId: String): Boolean { return Realm.getInstance(monarchy.realmConfiguration).use { realm -> val readReceipt = ReadReceiptEntity.where(realm, roomId, credentials.userId).findFirst() - ?: return false + ?: return false val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) - ?: return false + ?: return false val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex - ?: Int.MIN_VALUE + ?: Int.MIN_VALUE val eventToCheckIndex = liveChunk.timelineEvents.find(eventId)?.root?.displayIndex - ?: Int.MAX_VALUE + ?: Int.MAX_VALUE eventToCheckIndex <= readReceiptIndex } } - private fun SetReadMarkersTask.Params.fullyReadEventId(): String? { - if (fullyReadEventId != null) { - return this.fullyReadEventId - } else { - Realm.getInstance(monarchy.realmConfiguration).use { realm -> - val readReceipt = ReadReceiptEntity.where(realm, roomId, credentials.userId).findFirst() - val readMarker = ReadMarkerEntity.where(realm, roomId).findFirst() - return if (readMarker?.eventId == readReceipt?.eventId) { - readReceiptEventId - } else { - null - } - } - } - } - } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/ReadReceiptHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/ReadReceiptHandler.kt index e61e81dd16..192a11fa68 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/ReadReceiptHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/ReadReceiptHandler.kt @@ -38,6 +38,22 @@ private const val TIMESTAMP_KEY = "ts" internal class ReadReceiptHandler @Inject constructor() { + companion object { + + fun createContent(userId: String, eventId: String): ReadReceiptContent { + return mapOf( + eventId to mapOf( + READ_KEY to mapOf( + userId to mapOf( + TIMESTAMP_KEY to System.currentTimeMillis().toDouble() + ) + ) + ) + ) + } + + } + fun handle(realm: Realm, roomId: String, content: ReadReceiptContent?, isInitialSync: Boolean) { if (content == null) { return diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt index 986acef616..55665ca27f 100644 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt @@ -26,6 +26,7 @@ import android.view.animation.AnimationUtils import androidx.core.view.isInvisible import im.vector.riotx.R import kotlinx.coroutines.* +import timber.log.Timber private const val DELAY_IN_MS = 1_500L @@ -44,6 +45,7 @@ class ReadMarkerView @JvmOverloads constructor( private var callbackDispatcherJob: Job? = null fun bindView(eventId: String?, hasReadMarker: Boolean, displayReadMarker: Boolean, readMarkerCallback: Callback) { + Timber.v("Bind event $eventId - hasReadMarker: $hasReadMarker - displayReadMarker: $displayReadMarker") this.eventId = eventId this.callback = readMarkerCallback if (displayReadMarker) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 0bb45ccfb8..d689f019e7 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -687,7 +687,7 @@ class RoomDetailFragment : val summary = state.asyncRoomSummary() val inviter = state.asyncInviter() if (summary?.membership == Membership.JOIN) { - timelineEventController.update(state.timeline, state.highlightedEventId, state.hideReadMarker) + timelineEventController.update(state) inviteView.visibility = View.GONE val uid = session.myUserId val meMember = session.getRoom(state.roomId)?.getRoomMember(uid) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index afe4ea6681..978e63f7f1 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -158,7 +158,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private fun handleTombstoneEvent(action: RoomDetailActions.HandleTombstoneEvent) { val tombstoneContent = action.event.getClearContent().toModel() - ?: return + ?: return val roomId = tombstoneContent.replacementRoom ?: "" val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN @@ -305,7 +305,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro //is original event a reply? val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel()?.relatesTo?.inReplyTo?.eventId - ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId + ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId if (inReplyTo != null) { //TODO check if same content? room.getTimeLineEvent(inReplyTo)?.let { @@ -314,12 +314,12 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } else { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val existingBody = messageContent?.body ?: "" if (existingBody != action.text) { room.editTextMessage(state.sendMode.timelineEvent.root.eventId - ?: "", messageContent?.type - ?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown) + ?: "", messageContent?.type + ?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown) } else { Timber.w("Same message content, do not send edition") } @@ -334,7 +334,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is SendMode.QUOTE -> { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val textMsg = messageContent?.body val finalText = legacyRiotQuoteText(textMsg, action.text) @@ -645,7 +645,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro Option.empty() } else { val readMarkerIndex = room.getTimeLineEvent(readMarkerId)?.displayIndex - ?: Int.MIN_VALUE + ?: Int.MIN_VALUE Option.just(readMarkerIndex) } } @@ -694,8 +694,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro readMarker.getOrNull() == readReceipt.getOrNull() } ) - .throttleLast(250, TimeUnit.MILLISECONDS) - .distinctUntilChanged() .startWith(false) .subscribe { setState { copy(hideReadMarker = it) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index b5a5fe8ca8..3aeac06f12 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -31,6 +31,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.epoxy.LoadingItem_ import im.vector.riotx.core.extensions.localDateTime +import im.vector.riotx.features.home.room.detail.RoomDetailViewState import im.vector.riotx.features.home.room.detail.timeline.factory.MergedHeaderItemFactory import im.vector.riotx.features.home.room.detail.timeline.factory.TimelineItemFactory import im.vector.riotx.features.home.room.detail.timeline.helper.* @@ -140,11 +141,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec requestModelBuild() } - fun update(timeline: Timeline?, eventIdToHighlight: String?, hideReadMarker: Boolean) { - if (this.timeline != timeline) { - this.timeline = timeline - this.timeline?.listener = this - + fun update(viewState: RoomDetailViewState) { + if (timeline != viewState.timeline) { + timeline = viewState.timeline + timeline?.listener = this // Clear cache synchronized(modelCache) { for (i in 0 until modelCache.size) { @@ -152,23 +152,22 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } } } - var requestModelBuild = false - if (this.eventIdToHighlight != eventIdToHighlight) { + if (eventIdToHighlight != viewState.highlightedEventId) { // Clear cache to force a refresh synchronized(modelCache) { for (i in 0 until modelCache.size) { - if (modelCache[i]?.eventId == eventIdToHighlight - || modelCache[i]?.eventId == this.eventIdToHighlight) { + if (modelCache[i]?.eventId == viewState.highlightedEventId + || modelCache[i]?.eventId == eventIdToHighlight) { modelCache[i] = null } } } - this.eventIdToHighlight = eventIdToHighlight + eventIdToHighlight = viewState.highlightedEventId requestModelBuild = true } - if (this.hideReadMarker != hideReadMarker) { - this.hideReadMarker = hideReadMarker + if (hideReadMarker != viewState.hideReadMarker) { + hideReadMarker = viewState.hideReadMarker requestModelBuild = true } if (requestModelBuild) { @@ -230,8 +229,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec // Should be build if not cached or if cached but contains mergedHeader or formattedDay // We then are sure we always have items up to date. if (modelCache[position] == null - || modelCache[position]?.mergedHeaderModel != null - || modelCache[position]?.formattedDayModel != null) { + || modelCache[position]?.mergedHeaderModel != null + || modelCache[position]?.formattedDayModel != null) { modelCache[position] = buildItemModels(position, currentSnapshot) } } @@ -256,7 +255,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec val date = event.root.localDateTime() val nextDate = nextEvent?.root?.localDateTime() val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate() - val eventModel = timelineItemFactory.create(event, nextEvent, eventIdToHighlight, hideReadMarker, callback).also { it.id(event.localId) it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) @@ -298,7 +296,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } } - fun searchPositionOfEvent(eventId: String): Int? = synchronized(modelCache) { // Search in the cache var realPosition = 0 From d1ff3314a74a28618b1e4b46cc6d96312f933c0f Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 19 Sep 2019 19:12:45 +0200 Subject: [PATCH 010/197] Timeline : add badge on jump to bottom view --- .../platform/BadgeFloatingActionButton.kt | 186 ++++++++++++++++++ .../home/room/detail/RoomDetailFragment.kt | 2 + .../main/res/layout/fragment_room_detail.xml | 6 +- .../src/main/res/values/attrs_badge_fab.xml | 12 ++ 4 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 vector/src/main/java/im/vector/riotx/core/platform/BadgeFloatingActionButton.kt create mode 100644 vector/src/main/res/values/attrs_badge_fab.xml diff --git a/vector/src/main/java/im/vector/riotx/core/platform/BadgeFloatingActionButton.kt b/vector/src/main/java/im/vector/riotx/core/platform/BadgeFloatingActionButton.kt new file mode 100644 index 0000000000..545fd9409f --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/platform/BadgeFloatingActionButton.kt @@ -0,0 +1,186 @@ +/* + * Copyright 2019 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.riotx.core.platform + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Paint.ANTI_ALIAS_FLAG +import android.graphics.PointF +import android.graphics.Rect +import android.graphics.RectF +import android.text.TextPaint +import android.util.AttributeSet +import androidx.core.content.res.use +import com.google.android.material.floatingactionbutton.FloatingActionButton +import im.vector.riotx.R +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.max +import kotlin.math.min +import kotlin.math.sin + +class BadgeFloatingActionButton @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : FloatingActionButton(context, attrs, defStyleAttr) { + + private val textPaint = TextPaint(ANTI_ALIAS_FLAG).apply { + textAlign = Paint.Align.LEFT + } + private val tintPaint = Paint(ANTI_ALIAS_FLAG) + + private var countStr: String + private var countMaxStr: String + private var counterBounds: RectF = RectF() + private var counterTextBounds: Rect = Rect() + private var counterMaxTextBounds: Rect = Rect() + private var counterPossibleCenter: PointF = PointF() + + private var fabBounds: Rect = Rect() + + var counterTextColor: Int + get() = textPaint.color + set(value) { + val was = textPaint.color + if (was != value) { + textPaint.color = value + invalidate() + } + } + + var counterBackgroundColor: Int + get() = tintPaint.color + set(value) { + val was = tintPaint.color + if (was != value) { + tintPaint.color = value + invalidate() + } + } + + var counterTextSize: Float + get() = textPaint.textSize + set(value) { + val was = textPaint.textSize + if (was != value) { + textPaint.textSize = value + invalidate() + requestLayout() + } + } + + var counterTextPadding: Float = 0f + set(value) { + if (field != value) { + field = value + invalidate() + requestLayout() + } + } + + + var maxCount: Int = 99 + set(value) { + if (field != value) { + field = value + countMaxStr = "$value+" + + requestLayout() + } + } + + var count: Int = 0 + set(value) { + if (field != value) { + field = value + countStr = countStr(value) + textPaint.getTextBounds(countStr, 0, countStr.length, counterTextBounds) + invalidate() + } + } + + init { + countStr = countStr(count) + textPaint.getTextBounds(countStr, 0, countStr.length, counterTextBounds) + countMaxStr = "$maxCount+" + + attrs?.let { initAttrs(attrs) } + } + + @SuppressWarnings("ResourceType", "Recycle") + private fun initAttrs(attrs: AttributeSet) { + context.obtainStyledAttributes(attrs, R.styleable.BadgeFloatingActionButton).use { + counterBackgroundColor = it.getColor(R.styleable.BadgeFloatingActionButton_badgeBackgroundColor, 0) + counterTextPadding = it.getDimension(R.styleable.BadgeFloatingActionButton_badgeTextPadding, 0f) + counterTextSize = it.getDimension(R.styleable.BadgeFloatingActionButton_badgeTextSize, 14f) + counterTextColor = it.getColor(R.styleable.BadgeFloatingActionButton_badgeTextColor, Color.WHITE) + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + calculateCounterBounds(counterBounds) + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + if (count > 0) { + canvas.drawCircle(counterBounds.centerX(), counterBounds.centerY(), counterBounds.width() / 2f, tintPaint) + + val textX = counterBounds.centerX() - counterTextBounds.width() / 2f - counterTextBounds.left + val textY = counterBounds.centerY() + counterTextBounds.height() / 2f - counterTextBounds.bottom + canvas.drawText(countStr, textX, textY, textPaint) + } + } + + private fun calculateCounterBounds(outRect: RectF) { + getMeasuredContentRect(fabBounds) + calculateCounterCenter(fabBounds, counterPossibleCenter) + + textPaint.getTextBounds(countMaxStr, 0, countMaxStr.length, counterMaxTextBounds) + val counterDiameter = max(counterMaxTextBounds.width(), counterMaxTextBounds.height()) + 2 * counterTextPadding + + val counterRight = min(counterPossibleCenter.x + counterDiameter / 2, fabBounds.right.toFloat()) + val counterTop = max(counterPossibleCenter.y - counterDiameter / 2, fabBounds.top.toFloat()) + + outRect.set(counterRight - counterDiameter, counterTop, counterRight, counterTop + counterDiameter) + } + + private fun calculateCounterCenter(inBounds: Rect, outPoint: PointF) { + val radius = min(inBounds.width(), inBounds.height()) / 2f + calculateCounterCenter(radius, outPoint) + outPoint.x = inBounds.centerX() + outPoint.x + outPoint.y = inBounds.centerY() - outPoint.y + } + + private fun calculateCounterCenter(radius: Float, outPoint: PointF) = + calculateCounterCenter(radius, (PI / 4).toFloat(), outPoint) + + private fun calculateCounterCenter(radius: Float, angle: Float, outPoint: PointF) { + outPoint.x = radius * cos(angle) + outPoint.y = radius * sin(angle) + } + + private fun countStr(count: Int) = if (count > maxCount) "$maxCount+" else count.toString() + + companion object { + val TEXT_APPEARANCE_SUPPORTED_ATTRS = intArrayOf(android.R.attr.textSize, android.R.attr.textColor) + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 8051d42611..58ab211eb4 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -739,6 +739,8 @@ class RoomDetailFragment : avatarRenderer.render(it, roomToolbarAvatarImageView) roomToolbarSubtitleView.setTextOrHide(it.topic) } + + jumpToBottomView.count = it.notificationCount } } diff --git a/vector/src/main/res/layout/fragment_room_detail.xml b/vector/src/main/res/layout/fragment_room_detail.xml index 3a68814eb1..f95afbd647 100644 --- a/vector/src/main/res/layout/fragment_room_detail.xml +++ b/vector/src/main/res/layout/fragment_room_detail.xml @@ -161,13 +161,17 @@ app:layout_constraintTop_toBottomOf="@+id/roomToolbar" tools:visibility="visible" /> - + + + + + + + + + + + \ No newline at end of file From 90eeb68d362cd1be0ef6592f7dd904c3cb67cf7a Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 20 Sep 2019 17:22:04 +0200 Subject: [PATCH 011/197] Timeline: fix permalink towards an hidden event --- .../api/session/room/timeline/Timeline.kt | 4 +- .../session/room/timeline/DefaultTimeline.kt | 79 +++++++++++++------ .../room/timeline/TokenChunkEventPersistor.kt | 10 +-- .../home/room/detail/RoomDetailFragment.kt | 1 + .../home/room/detail/RoomDetailViewModel.kt | 23 ++---- .../ScrollOnHighlightedEventCallback.kt | 7 +- .../timeline/TimelineEventController.kt | 5 +- 7 files changed, 81 insertions(+), 48 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt index d0f4bff74b..13eca813c7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt @@ -44,7 +44,6 @@ interface Timeline { */ fun dispose() - fun restartWithEventId(eventId: String?) @@ -73,8 +72,9 @@ interface Timeline { fun getTimelineEventWithId(eventId: String?): TimelineEvent? + fun getFirstDisplayableEventId(eventId: String): String? - interface Listener { + interface Listener { /** * Call when the timeline has been updated through pagination or sync. * @param snapshot the most uptodate snapshot diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index 88c13cc056..b3d83a2286 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -99,7 +99,8 @@ internal class DefaultTimeline( private val cancelableBag = CancelableBag() private val debouncer = Debouncer(mainHandler) - private lateinit var liveEvents: RealmResults + private lateinit var nonFilteredEvents: RealmResults + private lateinit var filteredEvents: RealmResults private lateinit var eventRelations: RealmResults private var roomEntity: RoomEntity? = null @@ -128,9 +129,9 @@ internal class DefaultTimeline( } changeSet.insertionRanges.forEach { range -> val (startDisplayIndex, direction) = if (range.startIndex == 0) { - Pair(liveEvents[range.length - 1]!!.root!!.displayIndex, Timeline.Direction.FORWARDS) + Pair(filteredEvents[range.length - 1]!!.root!!.displayIndex, Timeline.Direction.FORWARDS) } else { - Pair(liveEvents[range.startIndex]!!.root!!.displayIndex, Timeline.Direction.BACKWARDS) + Pair(filteredEvents[range.startIndex]!!.root!!.displayIndex, Timeline.Direction.BACKWARDS) } val state = getPaginationState(direction) if (state.isPaginating) { @@ -218,9 +219,9 @@ internal class DefaultTimeline( } } - liveEvents = buildEventQuery(realm) + nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING).findAll() + filteredEvents = nonFilteredEvents.where() .filterEventsWithSettings() - .sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING) .findAllAsync() .also { it.addChangeListener(eventsChangeListener) } @@ -229,9 +230,9 @@ internal class DefaultTimeline( .also { it.addChangeListener(relationsListener) } if (settings.buildReadReceipts) { - hiddenReadReceipts.start(realm, liveEvents, this) + hiddenReadReceipts.start(realm, filteredEvents, this) } - hiddenReadMarker.start(realm, liveEvents, this) + hiddenReadMarker.start(realm, filteredEvents, this) isReady.set(true) } } @@ -246,7 +247,7 @@ internal class DefaultTimeline( BACKGROUND_HANDLER.post { roomEntity?.sendingTimelineEvents?.removeAllChangeListeners() eventRelations.removeAllChangeListeners() - liveEvents.removeAllChangeListeners() + filteredEvents.removeAllChangeListeners() hiddenReadMarker.dispose() if (settings.buildReadReceipts) { hiddenReadReceipts.dispose() @@ -267,25 +268,56 @@ internal class DefaultTimeline( postSnapshot() } - override fun getIndexOfEvent(eventId: String?): Int? { - return builtEventsIdMap[eventId] - } - override fun getTimelineEventAtIndex(index: Int): TimelineEvent? { return builtEvents.getOrNull(index) } + override fun getIndexOfEvent(eventId: String?): Int? { + return builtEventsIdMap[eventId] + } + override fun getTimelineEventWithId(eventId: String?): TimelineEvent? { return builtEventsIdMap[eventId]?.let { getTimelineEventAtIndex(it) } } + override fun getFirstDisplayableEventId(eventId: String): String? { + // If the item is built, the id is obviously displayable + val builtIndex = builtEventsIdMap[eventId] + if (builtIndex != null) { + return eventId + } + // Otherwise, we should check if the event is in the db, but is hidden because of filters + return Realm.getInstance(realmConfiguration).use { localRealm -> + val nonFilteredEvents = buildEventQuery(localRealm).sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING).findAll() + val nonFilteredEvent = nonFilteredEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, eventId).findFirst() + val filteredEvents = nonFilteredEvents.where().filterEventsWithSettings().findAll() + val isEventInDb = nonFilteredEvent != null + + val isHidden = isEventInDb && filteredEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, eventId).findFirst() == null + if (isHidden) { + val displayIndex = nonFilteredEvent?.root?.displayIndex + if (displayIndex != null) { + // Then we are looking for the first displayable event after the hidden one + val firstDisplayedEvent = filteredEvents.where() + .lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex) + .findFirst() + firstDisplayedEvent?.eventId + } else { + null + } + } else { + null + } + } + } + override fun hasMoreToLoad(direction: Timeline.Direction): Boolean { return hasMoreInCache(direction) || !hasReachedEnd(direction) } - // TimelineHiddenReadReceipts.Delegate +// TimelineHiddenReadReceipts.Delegate override fun rebuildEvent(eventId: String, readReceipts: List): Boolean { return rebuildEvent(eventId) { te -> @@ -297,7 +329,7 @@ internal class DefaultTimeline( postSnapshot() } - // TimelineHiddenReadMarker.Delegate +// TimelineHiddenReadMarker.Delegate override fun rebuildEvent(eventId: String, hasReadMarker: Boolean): Boolean { return rebuildEvent(eventId) { te -> @@ -309,7 +341,7 @@ internal class DefaultTimeline( postSnapshot() } - // Private methods ***************************************************************************** +// Private methods ***************************************************************************** private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean { return builtEventsIdMap[eventId]?.let { builtIndex -> @@ -415,22 +447,23 @@ internal class DefaultTimeline( */ private fun handleInitialLoad() { var shouldFetchInitialEvent = false - val initialDisplayIndex = if (initialEventId == null) { - liveEvents.firstOrNull()?.root?.displayIndex + val currentInitialEventId = initialEventId + val initialDisplayIndex = if (currentInitialEventId == null) { + filteredEvents.firstOrNull()?.root?.displayIndex } else { - val initialEvent = liveEvents.where() + val initialEvent = nonFilteredEvents.where() .equalTo(TimelineEventEntityFields.EVENT_ID, initialEventId) .findFirst() + shouldFetchInitialEvent = initialEvent == null initialEvent?.root?.displayIndex } prevDisplayIndex = initialDisplayIndex nextDisplayIndex = initialDisplayIndex - val currentInitialEventId = initialEventId if (currentInitialEventId != null && shouldFetchInitialEvent) { fetchEvent(currentInitialEventId) } else { - val count = min(settings.initialSize, liveEvents.size) + val count = min(settings.initialSize, filteredEvents.size) if (initialEventId == null) { paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count, strict = false) } else { @@ -494,7 +527,7 @@ internal class DefaultTimeline( * This has to be called on TimelineThread as it access realm live results */ private fun getLiveChunk(): ChunkEntity? { - return liveEvents.firstOrNull()?.chunk?.firstOrNull() + return filteredEvents.firstOrNull()?.chunk?.firstOrNull() } /** @@ -552,7 +585,7 @@ internal class DefaultTimeline( direction: Timeline.Direction, count: Long, strict: Boolean): RealmResults { - val offsetQuery = liveEvents.where() + val offsetQuery = filteredEvents.where() if (direction == Timeline.Direction.BACKWARDS) { offsetQuery.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING) if (strict) { @@ -631,7 +664,7 @@ internal class DefaultTimeline( } -// Extension methods *************************************************************************** + // Extension methods *************************************************************************** private fun Timeline.Direction.toPaginationDirection(): PaginationDirection { return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt index af845040ae..0305002959 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -112,7 +112,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction") val roomEntity = RoomEntity.where(realm, roomId).findFirst() - ?: realm.createObject(roomId) + ?: realm.createObject(roomId) val nextToken: String? val prevToken: String? @@ -141,7 +141,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy } else { nextChunk?.apply { this.prevToken = prevToken } } - ?: ChunkEntity.create(realm, prevToken, nextToken) + ?: ChunkEntity.create(realm, prevToken, nextToken) if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) { Timber.v("Reach end of $roomId") @@ -163,8 +163,8 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy currentChunk = handleMerge(roomEntity, direction, currentChunk, nextChunk) } else { val newEventIds = receivedChunk.events.mapNotNull { it.eventId } - ChunkEntity - .findAllIncludingEvents(realm, newEventIds) + val overlappedChunks = ChunkEntity.findAllIncludingEvents(realm, newEventIds) + overlappedChunks .filter { it != currentChunk } .forEach { overlapped -> currentChunk = handleMerge(roomEntity, direction, currentChunk, overlapped) @@ -194,7 +194,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy // We always merge the bottom chunk into top chunk, so we are always merging backwards Timber.v("Merge ${currentChunk.prevToken} | ${currentChunk.nextToken} with ${otherChunk.prevToken} | ${otherChunk.nextToken}") - return if (direction == PaginationDirection.BACKWARDS) { + return if (direction == PaginationDirection.BACKWARDS && !otherChunk.isLastForward) { currentChunk.merge(roomEntity.roomId, otherChunk, PaginationDirection.BACKWARDS) roomEntity.deleteOnCascade(otherChunk) currentChunk diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 58ab211eb4..f6d50046a9 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -702,6 +702,7 @@ class RoomDetailFragment : val summary = state.asyncRoomSummary() val inviter = state.asyncInviter() if (summary?.membership == Membership.JOIN) { + scrollOnHighlightedEventCallback.timeline = state.timeline timelineEventController.update(state) inviteView.visibility = View.GONE val uid = session.myUserId diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 401411dfde..72a09fbad6 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -43,6 +43,7 @@ import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.getFileUrl +import im.vector.matrix.android.api.session.room.model.relation.ReactionContent import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent import im.vector.matrix.android.api.session.room.send.UserDraft import im.vector.matrix.android.api.session.room.timeline.TimelineEvent @@ -613,18 +614,18 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } - private fun handleNavigateToEvent(action: RoomDetailActions.NavigateToEvent) { - val targetEventId = action.eventId - val indexOfEvent = timeline.getIndexOfEvent(targetEventId) + val targetEventId: String = action.eventId + val correctedEventId = timeline.getFirstDisplayableEventId(targetEventId) ?: targetEventId + val indexOfEvent = timeline.getIndexOfEvent(correctedEventId) if (indexOfEvent == null) { // Event is not already in RAM timeline.restartWithEventId(targetEventId) } if (action.highlight) { - setState { copy(highlightedEventId = targetEventId) } + setState { copy(highlightedEventId = correctedEventId) } } - _navigateToEvent.postLiveEvent(targetEventId) + _navigateToEvent.postLiveEvent(correctedEventId) } private fun handleResendEvent(action: RoomDetailActions.ResendMessage) { @@ -683,17 +684,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } private fun handleSetReadMarkerAction(action: RoomDetailActions.SetReadMarkerAction) = withState { state -> - var readMarkerId = action.eventId - if (readMarkerId == state.asyncRoomSummary()?.readMarkerId) { - val indexOfEvent = timeline.getIndexOfEvent(action.eventId) - // force to set the read marker on the next event - if (indexOfEvent != null) { - timeline.getTimelineEventAtIndex(indexOfEvent - 1)?.root?.eventId?.also { eventIdOfNext -> - readMarkerId = eventIdOfNext - } - } - } - room.setReadMarker(readMarkerId, callback = object : MatrixCallback {}) + room.setReadMarker(action.eventId, callback = object : MatrixCallback {}) } private fun handleMarkAllAsRead() { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt index c272e611a0..62d80408d2 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt @@ -17,6 +17,7 @@ package im.vector.riotx.features.home.room.detail import androidx.recyclerview.widget.LinearLayoutManager +import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.riotx.core.platform.DefaultListUpdateCallback import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import timber.log.Timber @@ -27,9 +28,13 @@ class ScrollOnHighlightedEventCallback(private val layoutManager: LinearLayoutMa private val scheduledEventId = AtomicReference() + var timeline: Timeline? = null + override fun onChanged(position: Int, count: Int, tag: Any?) { val eventId = scheduledEventId.get() ?: return - val positionToScroll = timelineEventController.searchPositionOfEvent(eventId) + val nonNullTimeline = timeline ?: return + val correctedEventId = nonNullTimeline.getFirstDisplayableEventId(eventId) + val positionToScroll = timelineEventController.searchPositionOfEvent(correctedEventId) if (positionToScroll != null) { val firstVisibleItem = layoutManager.findFirstCompletelyVisibleItemPosition() val lastVisibleItem = layoutManager.findLastCompletelyVisibleItemPosition() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index 3aeac06f12..701e412b1d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -296,8 +296,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } } - fun searchPositionOfEvent(eventId: String): Int? = synchronized(modelCache) { + fun searchPositionOfEvent(eventId: String?): Int? = synchronized(modelCache) { // Search in the cache + if (eventId == null) { + return null + } var realPosition = 0 if (showingForwardLoader) { realPosition++ From 7e29665fd0d5d8c3ebaf3745d4e77428390b53b1 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 20 Sep 2019 18:33:57 +0200 Subject: [PATCH 012/197] Timeline: add some comments and checks --- .../api/session/room/timeline/Timeline.kt | 26 +- .../session/room/timeline/DefaultTimeline.kt | 3 + .../room/timeline/TimelineHiddenReadMarker.kt | 5 +- .../timeline/TimelineHiddenReadReceipts.kt | 3 + .../home/room/detail/RoomDetailFragment.kt | 1140 ++++++++--------- .../ScrollOnHighlightedEventCallback.kt | 14 + 6 files changed, 619 insertions(+), 572 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt index 13eca813c7..9873b75e70 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt @@ -44,6 +44,10 @@ interface Timeline { */ fun dispose() + /** + * This method restarts the timeline, erases all built events and pagination states. + * It then loads events around the eventId. If eventId is null, it does restart the live timeline. + */ fun restartWithEventId(eventId: String?) @@ -62,19 +66,39 @@ interface Timeline { */ fun paginate(direction: Direction, count: Int) + /** + * Returns the number of sending events + */ fun pendingEventCount(): Int + /** + * Returns the number of failed sending events. + */ fun failedToDeliverEventCount(): Int + /** + * Returns the index of a built event or null. + */ fun getIndexOfEvent(eventId: String?): Int? + /** + * Returns the built [TimelineEvent] at index or null + */ fun getTimelineEventAtIndex(index: Int): TimelineEvent? + /** + * Returns the built [TimelineEvent] with eventId or null + */ fun getTimelineEventWithId(eventId: String?): TimelineEvent? + /** + * Returns the first displayable events starting from eventId. + * It does depend on the provided [TimelineSettings]. + */ fun getFirstDisplayableEventId(eventId: String): String? - interface Listener { + + interface Listener { /** * Call when the timeline has been updated through pagination or sync. * @param snapshot the most uptodate snapshot diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index b3d83a2286..f22be74f70 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -120,6 +120,9 @@ internal class DefaultTimeline( private val eventDecryptor = TimelineEventDecryptor(realmConfiguration, timelineID, cryptoService) private val eventsChangeListener = OrderedRealmCollectionChangeListener> { results, changeSet -> + if (!results.isLoaded || !results.isValid) { + return@OrderedRealmCollectionChangeListener + } if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL) { handleInitialLoad() } else { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt index 532a66140e..7ae6cbcfe1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt @@ -45,6 +45,9 @@ internal class TimelineHiddenReadMarker constructor(private val roomId: String) private lateinit var delegate: Delegate private val readMarkerListener = RealmObjectChangeListener { readMarker, _ -> + if (!readMarker.isLoaded || !readMarker.isValid) { + return@RealmObjectChangeListener + } var hasChange = false previousDisplayedEventId?.also { hasChange = delegate.rebuildEvent(it, false) @@ -53,7 +56,7 @@ internal class TimelineHiddenReadMarker constructor(private val roomId: String) val isEventHidden = liveEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, readMarker.eventId).findFirst() == null if (isEventHidden) { val hiddenEvent = readMarker.timelineEvent?.firstOrNull() - ?: return@RealmObjectChangeListener + ?: return@RealmObjectChangeListener val displayIndex = hiddenEvent.root?.displayIndex if (displayIndex != null) { // Then we are looking for the first displayable event after the hidden one diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt index 5408668576..f932e6f3c0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt @@ -53,6 +53,9 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu private lateinit var delegate: Delegate private val hiddenReadReceiptsListener = OrderedRealmCollectionChangeListener> { collection, changeSet -> + if (!collection.isLoaded || !collection.isValid) { + return@OrderedRealmCollectionChangeListener + } var hasChange = false // Deletion here means we don't have any readReceipts for the given hidden events changeSet.deletions.forEach { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index f6d50046a9..72f0005d4c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -507,614 +507,614 @@ class RoomDetailFragment : R.drawable.ic_reply, object : RoomMessageTouchHelperCallback.QuickReplayHandler { override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { - (model as? AbsMessageItem)?.attributes?.informationData?.let { - val eventId = it.eventId - roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId, composerLayout.composerEditText.text.toString())) - } + (model as? AbsMessageItem)?.attributes?.informationData?.let { + val eventId = it.eventId + roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId, composerLayout.composerEditText.text.toString())) } + } - override fun canSwipeModel(model: EpoxyModel<*>): Boolean { - return when (model) { - is MessageFileItem, - is MessageImageVideoItem, - is MessageTextItem -> { - return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED - } - else -> false + override fun canSwipeModel(model: EpoxyModel<*>): Boolean { + return when (model) { + is MessageFileItem, + is MessageImageVideoItem, + is MessageTextItem -> { + return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED } + else -> false } - }) - val touchHelper = ItemTouchHelper(swipeCallback) - touchHelper.attachToRecyclerView(recyclerView) - } + } + }) + val touchHelper = ItemTouchHelper(swipeCallback) + touchHelper.attachToRecyclerView(recyclerView) } + } - private fun updateJumpToBottomViewVisibility() { - debouncer.debounce("jump_to_bottom_visibility", 250, Runnable { - Timber.v("First visible: ${layoutManager.findFirstCompletelyVisibleItemPosition()}") - if (layoutManager.findFirstCompletelyVisibleItemPosition() != 0) { - jumpToBottomView.show() - } else { - jumpToBottomView.hide() - } - }) - } - - private fun setupComposer() { - val elevation = 6f - val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(requireContext(), R.attr.riotx_background)) - Autocomplete.on(composerLayout.composerEditText) - .with(commandAutocompletePolicy) - .with(autocompleteCommandPresenter) - .with(elevation) - .with(backgroundDrawable) - .with(object : AutocompleteCallback { - override fun onPopupItemClicked(editable: Editable, item: Command): Boolean { - editable.clear() - editable - .append(item.command) - .append(" ") - return true - } - - override fun onPopupVisibilityChanged(shown: Boolean) { - } - }) - .build() - - autocompleteUserPresenter.callback = this - Autocomplete.on(composerLayout.composerEditText) - .with(CharPolicy('@', true)) - .with(autocompleteUserPresenter) - .with(elevation) - .with(backgroundDrawable) - .with(object : AutocompleteCallback { - override fun onPopupItemClicked(editable: Editable, item: User): Boolean { - // Detect last '@' and remove it - var startIndex = editable.lastIndexOf("@") - if (startIndex == -1) { - startIndex = 0 - } - - // Detect next word separator - var endIndex = editable.indexOf(" ", startIndex) - if (endIndex == -1) { - endIndex = editable.length - } - - // Replace the word by its completion - val displayName = item.displayName ?: item.userId - - // with a trailing space - editable.replace(startIndex, endIndex, "$displayName ") - - // Add the span - val user = session.getUser(item.userId) - val span = PillImageSpan(glideRequests, avatarRenderer, requireContext(), item.userId, user) - span.bind(composerLayout.composerEditText) - - editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - - return true - } - - override fun onPopupVisibilityChanged(shown: Boolean) { - } - }) - .build() - - composerLayout.sendButton.setOnClickListener { - if (lockSendButton) { - Timber.w("Send button is locked") - return@setOnClickListener - } - val textMessage = composerLayout.composerEditText.text.toString() - if (textMessage.isNotBlank()) { - lockSendButton = true - roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage, vectorPreferences.isMarkdownEnabled())) - } - } - composerLayout.composerRelatedMessageCloseButton.setOnClickListener { - roomDetailViewModel.process(RoomDetailActions.ExitSpecialMode(composerLayout.composerEditText.text.toString())) - } - } - - private fun setupAttachmentButton() { - composerLayout.attachmentButton.setOnClickListener { - val intent = Intent(requireContext(), FilePickerActivity::class.java) - intent.putExtra(FilePickerActivity.CONFIGS, Configurations.Builder() - .setCheckPermission(true) - .setShowFiles(true) - .setShowAudios(true) - .setSkipZeroSizeFiles(true) - .build()) - startActivityForResult(intent, REQUEST_FILES_REQUEST_CODE) - /* - val items = ArrayList() - // Send file - items.add(DialogListItem.SendFile) - // Send voice - - if (vectorPreferences.isSendVoiceFeatureEnabled()) { - items.add(DialogListItem.SendVoice.INSTANCE) - } - - - // Send sticker - //items.add(DialogListItem.SendSticker) - // Camera - - //if (vectorPreferences.useNativeCamera()) { - items.add(DialogListItem.TakePhoto) - items.add(DialogListItem.TakeVideo) - //} else { - // items.add(DialogListItem.TakePhotoVideo.INSTANCE) - // } - val adapter = DialogSendItemAdapter(requireContext(), items) - AlertDialog.Builder(requireContext()) - .setAdapter(adapter) { _, position -> - onSendChoiceClicked(items[position]) - } - .setNegativeButton(R.string.cancel, null) - .show() - */ - } - } - - private fun setupInviteView() { - inviteView.callback = this - } - - private fun onSendChoiceClicked(dialogListItem: DialogListItem) { - Timber.v("On send choice clicked: $dialogListItem") - when (dialogListItem) { - is DialogListItem.SendFile -> { - // launchFileIntent - } - is DialogListItem.SendVoice -> { - //launchAudioRecorderIntent() - } - is DialogListItem.SendSticker -> { - //startStickerPickerActivity() - } - is DialogListItem.TakePhotoVideo -> - if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) { - // launchCamera() - } - is DialogListItem.TakePhoto -> - if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_CAMERA)) { - openCamera(requireActivity(), CAMERA_VALUE_TITLE, TAKE_IMAGE_REQUEST_CODE) - } - is DialogListItem.TakeVideo -> - if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CAMERA)) { - // launchNativeVideoRecorder() - } - } - } - - private fun handleMediaIntent(data: Intent) { - val files: ArrayList = data.getParcelableArrayListExtra(FilePickerActivity.MEDIA_FILES) - roomDetailViewModel.process(RoomDetailActions.SendMedia(files)) - } - - private fun renderState(state: RoomDetailViewState) { - renderRoomSummary(state) - val summary = state.asyncRoomSummary() - val inviter = state.asyncInviter() - if (summary?.membership == Membership.JOIN) { - scrollOnHighlightedEventCallback.timeline = state.timeline - timelineEventController.update(state) - inviteView.visibility = View.GONE - val uid = session.myUserId - val meMember = session.getRoom(state.roomId)?.getRoomMember(uid) - avatarRenderer.render(meMember?.avatarUrl, uid, meMember?.displayName, composerLayout.composerAvatarImageView) - - } else if (summary?.membership == Membership.INVITE && inviter != null) { - inviteView.visibility = View.VISIBLE - inviteView.render(inviter, VectorInviteView.Mode.LARGE) - - // Intercept click event - inviteView.setOnClickListener { } - } else if (state.asyncInviter.complete) { - vectorBaseActivity.finish() - } - if (state.tombstoneEvent == null) { - composerLayout.visibility = View.VISIBLE - composerLayout.setRoomEncrypted(state.isEncrypted) - notificationAreaView.render(NotificationAreaView.State.Hidden) + private fun updateJumpToBottomViewVisibility() { + debouncer.debounce("jump_to_bottom_visibility", 250, Runnable { + Timber.v("First visible: ${layoutManager.findFirstCompletelyVisibleItemPosition()}") + if (layoutManager.findFirstCompletelyVisibleItemPosition() != 0) { + jumpToBottomView.show() } else { - composerLayout.visibility = View.GONE - notificationAreaView.render(NotificationAreaView.State.Tombstone(state.tombstoneEvent)) + jumpToBottomView.hide() } - jumpToReadMarkerView.render(state.showJumpToReadMarker, summary?.readMarkerId) - } + }) + } - private fun renderRoomSummary(state: RoomDetailViewState) { - state.asyncRoomSummary()?.let { - - if (it.membership.isLeft()) { - Timber.w("The room has been left") - activity?.finish() - } else { - roomToolbarTitleView.text = it.displayName - avatarRenderer.render(it, roomToolbarAvatarImageView) - roomToolbarSubtitleView.setTextOrHide(it.topic) - } - - jumpToBottomView.count = it.notificationCount - } - } - - private fun renderTextComposerState(state: TextComposerViewState) { - autocompleteUserPresenter.render(state.asyncUsers) - } - - private fun renderTombstoneEventHandling(async: Async) { - when (async) { - is Loading -> { - // TODO Better handling progress - vectorBaseActivity.showWaitingView() - vectorBaseActivity.waiting_view_status_text.visibility = View.VISIBLE - vectorBaseActivity.waiting_view_status_text.text = getString(R.string.joining_room) - } - is Success -> { - navigator.openRoom(vectorBaseActivity, async()) - vectorBaseActivity.finish() - } - is Fail -> { - vectorBaseActivity.hideWaitingView() - vectorBaseActivity.toast(errorFormatter.toHumanReadable(async.error)) - } - } - } - - private fun renderSendMessageResult(sendMessageResult: SendMessageResult) { - when (sendMessageResult) { - is SendMessageResult.MessageSent -> { - updateComposerText("") - } - is SendMessageResult.SlashCommandHandled -> { - sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) } - updateComposerText("") - } - is SendMessageResult.SlashCommandError -> { - displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command)) - } - is SendMessageResult.SlashCommandUnknown -> { - displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command)) - } - is SendMessageResult.SlashCommandResultOk -> { - updateComposerText("") - } - is SendMessageResult.SlashCommandResultError -> { - displayCommandError(sendMessageResult.throwable.localizedMessage) - } - is SendMessageResult.SlashCommandNotImplemented -> { - displayCommandError(getString(R.string.not_implemented)) - } - } - - lockSendButton = false - } - - private fun displayCommandError(message: String) { - AlertDialog.Builder(activity!!) - .setTitle(R.string.command_error) - .setMessage(message) - .setPositiveButton(R.string.ok, null) - .show() - } - -// TimelineEventController.Callback ************************************************************ - - override fun onUrlClicked(url: String): Boolean { - return permalinkHandler.launch(requireActivity(), url, object : NavigateToRoomInterceptor { - override fun navToRoom(roomId: String, eventId: String?): Boolean { - // Same room? - if (roomId == roomDetailArgs.roomId) { - // Navigation to same room - if (eventId == null) { - showSnackWithMessage(getString(R.string.navigate_to_room_when_already_in_the_room)) - } else { - // Highlight and scroll to this event - roomDetailViewModel.process(RoomDetailActions.NavigateToEvent(eventId, true)) - } + private fun setupComposer() { + val elevation = 6f + val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(requireContext(), R.attr.riotx_background)) + Autocomplete.on(composerLayout.composerEditText) + .with(commandAutocompletePolicy) + .with(autocompleteCommandPresenter) + .with(elevation) + .with(backgroundDrawable) + .with(object : AutocompleteCallback { + override fun onPopupItemClicked(editable: Editable, item: Command): Boolean { + editable.clear() + editable + .append(item.command) + .append(" ") return true } - // Not handled - return false - } - }) - } - - override fun onUrlLongClicked(url: String): Boolean { - if (url != getString(R.string.edited_suffix)) { - // Copy the url to the clipboard - copyToClipboard(requireContext(), url, true, R.string.link_copied_to_clipboard) - } - return true - } - - override fun onEventVisible(event: TimelineEvent) { - roomDetailViewModel.process(RoomDetailActions.TimelineEventTurnsVisible(event)) - } - - override fun onEventInvisible(event: TimelineEvent) { - roomDetailViewModel.process(RoomDetailActions.TimelineEventTurnsInvisible(event)) - } - - override fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View) { - vectorBaseActivity.notImplemented("encrypted message click") - } - - override fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) { - // TODO Use navigator - - val intent = ImageMediaViewerActivity.newIntent(vectorBaseActivity, mediaData, ViewCompat.getTransitionName(view)) - val pairs = ArrayList>() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - requireActivity().window.decorView.findViewById(android.R.id.statusBarBackground)?.let { - pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME)) - } - requireActivity().window.decorView.findViewById(android.R.id.navigationBarBackground)?.let { - pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME)) - } - } - pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: "")) - pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: "")) - pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: "")) - - val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation( - requireActivity(), *pairs.toTypedArray()).toBundle() - startActivity(intent, bundle) - } - - override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) { - // TODO Use navigator - val intent = VideoMediaViewerActivity.newIntent(vectorBaseActivity, mediaData) - startActivity(intent) - } - - override fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) { - val action = RoomDetailActions.DownloadFile(eventId, messageFileContent) - // We need WRITE_EXTERNAL permission - if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_DOWNLOAD_FILE)) { - roomDetailViewModel.process(action) - } else { - roomDetailViewModel.pendingAction = action - } - } - - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { - if (allGranted(grantResults)) { - if (requestCode == PERMISSION_REQUEST_CODE_DOWNLOAD_FILE) { - val action = roomDetailViewModel.pendingAction - - if (action != null) { - roomDetailViewModel.pendingAction = null - roomDetailViewModel.process(action) + override fun onPopupVisibilityChanged(shown: Boolean) { } - } + }) + .build() + + autocompleteUserPresenter.callback = this + Autocomplete.on(composerLayout.composerEditText) + .with(CharPolicy('@', true)) + .with(autocompleteUserPresenter) + .with(elevation) + .with(backgroundDrawable) + .with(object : AutocompleteCallback { + override fun onPopupItemClicked(editable: Editable, item: User): Boolean { + // Detect last '@' and remove it + var startIndex = editable.lastIndexOf("@") + if (startIndex == -1) { + startIndex = 0 + } + + // Detect next word separator + var endIndex = editable.indexOf(" ", startIndex) + if (endIndex == -1) { + endIndex = editable.length + } + + // Replace the word by its completion + val displayName = item.displayName ?: item.userId + + // with a trailing space + editable.replace(startIndex, endIndex, "$displayName ") + + // Add the span + val user = session.getUser(item.userId) + val span = PillImageSpan(glideRequests, avatarRenderer, requireContext(), item.userId, user) + span.bind(composerLayout.composerEditText) + + editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + + return true + } + + override fun onPopupVisibilityChanged(shown: Boolean) { + } + }) + .build() + + composerLayout.sendButton.setOnClickListener { + if (lockSendButton) { + Timber.w("Send button is locked") + return@setOnClickListener + } + val textMessage = composerLayout.composerEditText.text.toString() + if (textMessage.isNotBlank()) { + lockSendButton = true + roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage, vectorPreferences.isMarkdownEnabled())) } } - - override fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) { - vectorBaseActivity.notImplemented("open audio file") + composerLayout.composerRelatedMessageCloseButton.setOnClickListener { + roomDetailViewModel.process(RoomDetailActions.ExitSpecialMode(composerLayout.composerEditText.text.toString())) } + } - override fun onLoadMore(direction: Timeline.Direction) { - roomDetailViewModel.process(RoomDetailActions.LoadMoreTimelineEvents(direction)) + private fun setupAttachmentButton() { + composerLayout.attachmentButton.setOnClickListener { + val intent = Intent(requireContext(), FilePickerActivity::class.java) + intent.putExtra(FilePickerActivity.CONFIGS, Configurations.Builder() + .setCheckPermission(true) + .setShowFiles(true) + .setShowAudios(true) + .setSkipZeroSizeFiles(true) + .build()) + startActivityForResult(intent, REQUEST_FILES_REQUEST_CODE) + /* + val items = ArrayList() + // Send file + items.add(DialogListItem.SendFile) + // Send voice + + if (vectorPreferences.isSendVoiceFeatureEnabled()) { + items.add(DialogListItem.SendVoice.INSTANCE) + } + + + // Send sticker + //items.add(DialogListItem.SendSticker) + // Camera + + //if (vectorPreferences.useNativeCamera()) { + items.add(DialogListItem.TakePhoto) + items.add(DialogListItem.TakeVideo) + //} else { + // items.add(DialogListItem.TakePhotoVideo.INSTANCE) + // } + val adapter = DialogSendItemAdapter(requireContext(), items) + AlertDialog.Builder(requireContext()) + .setAdapter(adapter) { _, position -> + onSendChoiceClicked(items[position]) + } + .setNegativeButton(R.string.cancel, null) + .show() + */ } + } - override fun onEventCellClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View) { + private fun setupInviteView() { + inviteView.callback = this + } + private fun onSendChoiceClicked(dialogListItem: DialogListItem) { + Timber.v("On send choice clicked: $dialogListItem") + when (dialogListItem) { + is DialogListItem.SendFile -> { + // launchFileIntent + } + is DialogListItem.SendVoice -> { + //launchAudioRecorderIntent() + } + is DialogListItem.SendSticker -> { + //startStickerPickerActivity() + } + is DialogListItem.TakePhotoVideo -> + if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) { + // launchCamera() + } + is DialogListItem.TakePhoto -> + if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_CAMERA)) { + openCamera(requireActivity(), CAMERA_VALUE_TITLE, TAKE_IMAGE_REQUEST_CODE) + } + is DialogListItem.TakeVideo -> + if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CAMERA)) { + // launchNativeVideoRecorder() + } } + } - override fun onEventLongClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View): Boolean { - view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) - val roomId = roomDetailArgs.roomId + private fun handleMediaIntent(data: Intent) { + val files: ArrayList = data.getParcelableArrayListExtra(FilePickerActivity.MEDIA_FILES) + roomDetailViewModel.process(RoomDetailActions.SendMedia(files)) + } - this.view?.hideKeyboard() - MessageActionsBottomSheet - .newInstance(roomId, informationData) - .show(requireActivity().supportFragmentManager, "MESSAGE_CONTEXTUAL_ACTIONS") - return true + private fun renderState(state: RoomDetailViewState) { + renderRoomSummary(state) + val summary = state.asyncRoomSummary() + val inviter = state.asyncInviter() + if (summary?.membership == Membership.JOIN) { + scrollOnHighlightedEventCallback.timeline = state.timeline + timelineEventController.update(state) + inviteView.visibility = View.GONE + val uid = session.myUserId + val meMember = session.getRoom(state.roomId)?.getRoomMember(uid) + avatarRenderer.render(meMember?.avatarUrl, uid, meMember?.displayName, composerLayout.composerAvatarImageView) + + } else if (summary?.membership == Membership.INVITE && inviter != null) { + inviteView.visibility = View.VISIBLE + inviteView.render(inviter, VectorInviteView.Mode.LARGE) + + // Intercept click event + inviteView.setOnClickListener { } + } else if (state.asyncInviter.complete) { + vectorBaseActivity.finish() } - - override fun onAvatarClicked(informationData: MessageInformationData) { - vectorBaseActivity.notImplemented("Click on user avatar") + if (state.tombstoneEvent == null) { + composerLayout.visibility = View.VISIBLE + composerLayout.setRoomEncrypted(state.isEncrypted) + notificationAreaView.render(NotificationAreaView.State.Hidden) + } else { + composerLayout.visibility = View.GONE + notificationAreaView.render(NotificationAreaView.State.Tombstone(state.tombstoneEvent)) } + jumpToReadMarkerView.render(state.showJumpToReadMarker, summary?.readMarkerId) + } - @SuppressLint("SetTextI18n") - override fun onMemberNameClicked(informationData: MessageInformationData) { - insertUserDisplayNameInTextEditor(informationData.memberName?.toString()) - } + private fun renderRoomSummary(state: RoomDetailViewState) { + state.asyncRoomSummary()?.let { - override fun onClickOnReactionPill(informationData: MessageInformationData, reaction: String, on: Boolean) { - if (on) { - //we should test the current real state of reaction on this event - roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, informationData.eventId)) + if (it.membership.isLeft()) { + Timber.w("The room has been left") + activity?.finish() } else { - //I need to redact a reaction - roomDetailViewModel.process(RoomDetailActions.UndoReaction(informationData.eventId, reaction)) + roomToolbarTitleView.text = it.displayName + avatarRenderer.render(it, roomToolbarAvatarImageView) + roomToolbarSubtitleView.setTextOrHide(it.topic) + } + jumpToBottomView.count = it.notificationCount + jumpToBottomView.drawBadge = it.hasUnreadMessages + } + } + + private fun renderTextComposerState(state: TextComposerViewState) { + autocompleteUserPresenter.render(state.asyncUsers) + } + + private fun renderTombstoneEventHandling(async: Async) { + when (async) { + is Loading -> { + // TODO Better handling progress + vectorBaseActivity.showWaitingView() + vectorBaseActivity.waiting_view_status_text.visibility = View.VISIBLE + vectorBaseActivity.waiting_view_status_text.text = getString(R.string.joining_room) + } + is Success -> { + navigator.openRoom(vectorBaseActivity, async()) + vectorBaseActivity.finish() + } + is Fail -> { + vectorBaseActivity.hideWaitingView() + vectorBaseActivity.toast(errorFormatter.toHumanReadable(async.error)) + } + } + } + + private fun renderSendMessageResult(sendMessageResult: SendMessageResult) { + when (sendMessageResult) { + is SendMessageResult.MessageSent -> { + updateComposerText("") + } + is SendMessageResult.SlashCommandHandled -> { + sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) } + updateComposerText("") + } + is SendMessageResult.SlashCommandError -> { + displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command)) + } + is SendMessageResult.SlashCommandUnknown -> { + displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command)) + } + is SendMessageResult.SlashCommandResultOk -> { + updateComposerText("") + } + is SendMessageResult.SlashCommandResultError -> { + displayCommandError(sendMessageResult.throwable.localizedMessage) + } + is SendMessageResult.SlashCommandNotImplemented -> { + displayCommandError(getString(R.string.not_implemented)) } } - override fun onLongClickOnReactionPill(informationData: MessageInformationData, reaction: String) { - ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, informationData) - .show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS") - } + lockSendButton = false + } - override fun onEditedDecorationClicked(informationData: MessageInformationData) { - ViewEditHistoryBottomSheet.newInstance(roomDetailArgs.roomId, informationData) - .show(requireActivity().supportFragmentManager, "DISPLAY_EDITS") - } + private fun displayCommandError(message: String) { + AlertDialog.Builder(activity!!) + .setTitle(R.string.command_error) + .setMessage(message) + .setPositiveButton(R.string.ok, null) + .show() + } - override fun onRoomCreateLinkClicked(url: String) { - permalinkHandler.launch(requireContext(), url, object : NavigateToRoomInterceptor { - override fun navToRoom(roomId: String, eventId: String?): Boolean { - requireActivity().finish() - return false +// TimelineEventController.Callback ************************************************************ + + override fun onUrlClicked(url: String): Boolean { + return permalinkHandler.launch(requireActivity(), url, object : NavigateToRoomInterceptor { + override fun navToRoom(roomId: String, eventId: String?): Boolean { + // Same room? + if (roomId == roomDetailArgs.roomId) { + // Navigation to same room + if (eventId == null) { + showSnackWithMessage(getString(R.string.navigate_to_room_when_already_in_the_room)) + } else { + // Highlight and scroll to this event + roomDetailViewModel.process(RoomDetailActions.NavigateToEvent(eventId, true)) + } + return true } - }) - } - override fun onReadReceiptsClicked(readReceipts: List) { - DisplayReadReceiptsBottomSheet.newInstance(readReceipts) - .show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS") - } + // Not handled + return false + } + }) + } - override fun onReadMarkerLongDisplayed() = withState(roomDetailViewModel) { state -> - val firstVisibleItem = layoutManager.findFirstVisibleItemPosition() - val nextReadMarkerId = timelineEventController.searchEventIdAtPosition(firstVisibleItem) - if (nextReadMarkerId != null) { - roomDetailViewModel.process(RoomDetailActions.SetReadMarkerAction(nextReadMarkerId)) + override fun onUrlLongClicked(url: String): Boolean { + if (url != getString(R.string.edited_suffix)) { + // Copy the url to the clipboard + copyToClipboard(requireContext(), url, true, R.string.link_copied_to_clipboard) + } + return true + } + + override fun onEventVisible(event: TimelineEvent) { + roomDetailViewModel.process(RoomDetailActions.TimelineEventTurnsVisible(event)) + } + + override fun onEventInvisible(event: TimelineEvent) { + roomDetailViewModel.process(RoomDetailActions.TimelineEventTurnsInvisible(event)) + } + + override fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View) { + vectorBaseActivity.notImplemented("encrypted message click") + } + + override fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) { + // TODO Use navigator + + val intent = ImageMediaViewerActivity.newIntent(vectorBaseActivity, mediaData, ViewCompat.getTransitionName(view)) + val pairs = ArrayList>() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + requireActivity().window.decorView.findViewById(android.R.id.statusBarBackground)?.let { + pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME)) + } + requireActivity().window.decorView.findViewById(android.R.id.navigationBarBackground)?.let { + pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME)) } } + pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: "")) + pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: "")) + pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: "")) - // AutocompleteUserPresenter.Callback + val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation( + requireActivity(), *pairs.toTypedArray()).toBundle() + startActivity(intent, bundle) + } - override fun onQueryUsers(query: CharSequence?) { - textComposerViewModel.process(TextComposerActions.QueryUsers(query)) + override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) { + // TODO Use navigator + val intent = VideoMediaViewerActivity.newIntent(vectorBaseActivity, mediaData) + startActivity(intent) + } + + override fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) { + val action = RoomDetailActions.DownloadFile(eventId, messageFileContent) + // We need WRITE_EXTERNAL permission + if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_DOWNLOAD_FILE)) { + roomDetailViewModel.process(action) + } else { + roomDetailViewModel.pendingAction = action } + } - private fun handleActions(action: SimpleAction) { - when (action) { - is SimpleAction.AddReaction -> { - startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), action.eventId), REACTION_SELECT_REQUEST_CODE) + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + if (allGranted(grantResults)) { + if (requestCode == PERMISSION_REQUEST_CODE_DOWNLOAD_FILE) { + val action = roomDetailViewModel.pendingAction + + if (action != null) { + roomDetailViewModel.pendingAction = null + roomDetailViewModel.process(action) } - is SimpleAction.ViewReactions -> { - ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData) - .show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS") - } - is SimpleAction.Copy -> { - //I need info about the current selected message :/ - copyToClipboard(requireContext(), action.content, false) - val msg = requireContext().getString(R.string.copied_to_clipboard) - showSnackWithMessage(msg, Snackbar.LENGTH_SHORT) - } - is SimpleAction.Delete -> { - roomDetailViewModel.process(RoomDetailActions.RedactAction(action.eventId, context?.getString(R.string.event_redacted_by_user_reason))) - } - is SimpleAction.Share -> { - //TODO current data communication is too limited - //Need to now the media type - //TODO bad, just POC - BigImageViewer.imageLoader().loadImage( - action.hashCode(), - Uri.parse(action.imageUrl), - object : ImageLoader.Callback { - override fun onFinish() {} + } + } + } - override fun onSuccess(image: File?) { - if (image != null) - shareMedia(requireContext(), image, "image/*") - } + override fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) { + vectorBaseActivity.notImplemented("open audio file") + } - override fun onFail(error: Exception?) {} + override fun onLoadMore(direction: Timeline.Direction) { + roomDetailViewModel.process(RoomDetailActions.LoadMoreTimelineEvents(direction)) + } - override fun onCacheHit(imageType: Int, image: File?) {} + override fun onEventCellClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View) { - override fun onCacheMiss(imageType: Int, image: File?) {} + } - override fun onProgress(progress: Int) {} + override fun onEventLongClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View): Boolean { + view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + val roomId = roomDetailArgs.roomId - override fun onStart() {} + this.view?.hideKeyboard() + MessageActionsBottomSheet + .newInstance(roomId, informationData) + .show(requireActivity().supportFragmentManager, "MESSAGE_CONTEXTUAL_ACTIONS") + return true + } + override fun onAvatarClicked(informationData: MessageInformationData) { + vectorBaseActivity.notImplemented("Click on user avatar") + } + + @SuppressLint("SetTextI18n") + override fun onMemberNameClicked(informationData: MessageInformationData) { + insertUserDisplayNameInTextEditor(informationData.memberName?.toString()) + } + + override fun onClickOnReactionPill(informationData: MessageInformationData, reaction: String, on: Boolean) { + if (on) { + //we should test the current real state of reaction on this event + roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, informationData.eventId)) + } else { + //I need to redact a reaction + roomDetailViewModel.process(RoomDetailActions.UndoReaction(informationData.eventId, reaction)) + } + } + + override fun onLongClickOnReactionPill(informationData: MessageInformationData, reaction: String) { + ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, informationData) + .show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS") + } + + override fun onEditedDecorationClicked(informationData: MessageInformationData) { + ViewEditHistoryBottomSheet.newInstance(roomDetailArgs.roomId, informationData) + .show(requireActivity().supportFragmentManager, "DISPLAY_EDITS") + } + + override fun onRoomCreateLinkClicked(url: String) { + permalinkHandler.launch(requireContext(), url, object : NavigateToRoomInterceptor { + override fun navToRoom(roomId: String, eventId: String?): Boolean { + requireActivity().finish() + return false + } + }) + } + + override fun onReadReceiptsClicked(readReceipts: List) { + DisplayReadReceiptsBottomSheet.newInstance(readReceipts) + .show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS") + } + + override fun onReadMarkerLongDisplayed() = withState(roomDetailViewModel) { state -> + val firstVisibleItem = layoutManager.findFirstVisibleItemPosition() + val nextReadMarkerId = timelineEventController.searchEventIdAtPosition(firstVisibleItem) + if (nextReadMarkerId != null) { + roomDetailViewModel.process(RoomDetailActions.SetReadMarkerAction(nextReadMarkerId)) + } + } + + // AutocompleteUserPresenter.Callback + + override fun onQueryUsers(query: CharSequence?) { + textComposerViewModel.process(TextComposerActions.QueryUsers(query)) + } + + private fun handleActions(action: SimpleAction) { + when (action) { + is SimpleAction.AddReaction -> { + startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), action.eventId), REACTION_SELECT_REQUEST_CODE) + } + is SimpleAction.ViewReactions -> { + ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData) + .show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS") + } + is SimpleAction.Copy -> { + //I need info about the current selected message :/ + copyToClipboard(requireContext(), action.content, false) + val msg = requireContext().getString(R.string.copied_to_clipboard) + showSnackWithMessage(msg, Snackbar.LENGTH_SHORT) + } + is SimpleAction.Delete -> { + roomDetailViewModel.process(RoomDetailActions.RedactAction(action.eventId, context?.getString(R.string.event_redacted_by_user_reason))) + } + is SimpleAction.Share -> { + //TODO current data communication is too limited + //Need to now the media type + //TODO bad, just POC + BigImageViewer.imageLoader().loadImage( + action.hashCode(), + Uri.parse(action.imageUrl), + object : ImageLoader.Callback { + override fun onFinish() {} + + override fun onSuccess(image: File?) { + if (image != null) + shareMedia(requireContext(), image, "image/*") } - ) - } - is SimpleAction.ViewEditHistory -> { - onEditedDecorationClicked(action.messageInformationData) - } - is SimpleAction.ViewSource -> { - val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null) - view.findViewById(R.id.event_content_text_view)?.let { - it.text = action.content - } - AlertDialog.Builder(requireActivity()) - .setView(view) - .setPositiveButton(R.string.ok, null) - .show() - } - is SimpleAction.ViewDecryptedSource -> { - val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null) - view.findViewById(R.id.event_content_text_view)?.let { - it.text = action.content - } + override fun onFail(error: Exception?) {} - AlertDialog.Builder(requireActivity()) - .setView(view) - .setPositiveButton(R.string.ok, null) - .show() - } - is SimpleAction.QuickReact -> { - //eventId,ClickedOn,Add - roomDetailViewModel.process(RoomDetailActions.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add)) - } - is SimpleAction.Edit -> { - roomDetailViewModel.process(RoomDetailActions.EnterEditMode(action.eventId, composerLayout.composerEditText.text.toString())) - } - is SimpleAction.Quote -> { - roomDetailViewModel.process(RoomDetailActions.EnterQuoteMode(action.eventId, composerLayout.composerEditText.text.toString())) - } - is SimpleAction.Reply -> { - roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(action.eventId, composerLayout.composerEditText.text.toString())) - } - is SimpleAction.CopyPermalink -> { - val permalink = PermalinkFactory.createPermalink(roomDetailArgs.roomId, action.eventId) - copyToClipboard(requireContext(), permalink, false) - showSnackWithMessage(requireContext().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT) + override fun onCacheHit(imageType: Int, image: File?) {} + override fun onCacheMiss(imageType: Int, image: File?) {} + + override fun onProgress(progress: Int) {} + + override fun onStart() {} + + } + ) + } + is SimpleAction.ViewEditHistory -> { + onEditedDecorationClicked(action.messageInformationData) + } + is SimpleAction.ViewSource -> { + val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null) + view.findViewById(R.id.event_content_text_view)?.let { + it.text = action.content } - is SimpleAction.Resend -> { - roomDetailViewModel.process(RoomDetailActions.ResendMessage(action.eventId)) - } - is SimpleAction.Remove -> { - roomDetailViewModel.process(RoomDetailActions.RemoveFailedEcho(action.eventId)) - } - else -> { - Toast.makeText(context, "Action $action is not implemented yet", Toast.LENGTH_LONG).show() + + AlertDialog.Builder(requireActivity()) + .setView(view) + .setPositiveButton(R.string.ok, null) + .show() + } + is SimpleAction.ViewDecryptedSource -> { + val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null) + view.findViewById(R.id.event_content_text_view)?.let { + it.text = action.content } + + AlertDialog.Builder(requireActivity()) + .setView(view) + .setPositiveButton(R.string.ok, null) + .show() + } + is SimpleAction.QuickReact -> { + //eventId,ClickedOn,Add + roomDetailViewModel.process(RoomDetailActions.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add)) + } + is SimpleAction.Edit -> { + roomDetailViewModel.process(RoomDetailActions.EnterEditMode(action.eventId, composerLayout.composerEditText.text.toString())) + } + is SimpleAction.Quote -> { + roomDetailViewModel.process(RoomDetailActions.EnterQuoteMode(action.eventId, composerLayout.composerEditText.text.toString())) + } + is SimpleAction.Reply -> { + roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(action.eventId, composerLayout.composerEditText.text.toString())) + } + is SimpleAction.CopyPermalink -> { + val permalink = PermalinkFactory.createPermalink(roomDetailArgs.roomId, action.eventId) + copyToClipboard(requireContext(), permalink, false) + showSnackWithMessage(requireContext().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT) + + } + is SimpleAction.Resend -> { + roomDetailViewModel.process(RoomDetailActions.ResendMessage(action.eventId)) + } + is SimpleAction.Remove -> { + roomDetailViewModel.process(RoomDetailActions.RemoveFailedEcho(action.eventId)) + } + else -> { + Toast.makeText(context, "Action $action is not implemented yet", Toast.LENGTH_LONG).show() } } + } //utils - /** - * Insert an user displayname in the message editor. - * - * @param text the text to insert. - */ + /** + * Insert an user displayname in the message editor. + * + * @param text the text to insert. + */ //TODO legacy, refactor - private fun insertUserDisplayNameInTextEditor(text: String?) { - //TODO move logic outside of fragment - if (null != text) { + private fun insertUserDisplayNameInTextEditor(text: String?) { + //TODO move logic outside of fragment + if (null != text) { // var vibrate = false - val myDisplayName = session.getUser(session.myUserId)?.displayName - if (TextUtils.equals(myDisplayName, text)) { - // current user - if (TextUtils.isEmpty(composerLayout.composerEditText.text)) { - composerLayout.composerEditText.append(Command.EMOTE.command + " ") - composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length) + val myDisplayName = session.getUser(session.myUserId)?.displayName + if (TextUtils.equals(myDisplayName, text)) { + // current user + if (TextUtils.isEmpty(composerLayout.composerEditText.text)) { + composerLayout.composerEditText.append(Command.EMOTE.command + " ") + composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length) // vibrate = true + } + } else { + // another user + if (TextUtils.isEmpty(composerLayout.composerEditText.text)) { + // Ensure displayName will not be interpreted as a Slash command + if (text.startsWith("/")) { + composerLayout.composerEditText.append("\\") } + composerLayout.composerEditText.append(sanitizeDisplayname(text)!! + ": ") } else { - // another user - if (TextUtils.isEmpty(composerLayout.composerEditText.text)) { - // Ensure displayName will not be interpreted as a Slash command - if (text.startsWith("/")) { - composerLayout.composerEditText.append("\\") - } - composerLayout.composerEditText.append(sanitizeDisplayname(text)!! + ": ") - } else { - composerLayout.composerEditText.text.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayname(text)!! + " ") - } + composerLayout.composerEditText.text.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayname(text)!! + " ") + } // vibrate = true - } + } // if (vibrate && vectorPreferences.vibrateWhenMentioning()) { // val v= context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator @@ -1122,44 +1122,44 @@ class RoomDetailFragment : // v.vibrate(100) // } // } - focusComposerAndShowKeyboard() - } + focusComposerAndShowKeyboard() } + } - private fun focusComposerAndShowKeyboard() { - composerLayout.composerEditText.requestFocus() - val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager - imm?.showSoftInput(composerLayout.composerEditText, InputMethodManager.SHOW_IMPLICIT) - } + private fun focusComposerAndShowKeyboard() { + composerLayout.composerEditText.requestFocus() + val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager + imm?.showSoftInput(composerLayout.composerEditText, InputMethodManager.SHOW_IMPLICIT) + } - private fun showSnackWithMessage(message: String, duration: Int = Snackbar.LENGTH_SHORT) { - val snack = Snackbar.make(view!!, message, duration) - snack.view.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.notification_accent_color)) - snack.show() - } + private fun showSnackWithMessage(message: String, duration: Int = Snackbar.LENGTH_SHORT) { + val snack = Snackbar.make(view!!, message, duration) + snack.view.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.notification_accent_color)) + snack.show() + } - // VectorInviteView.Callback + // VectorInviteView.Callback - override fun onAcceptInvite() { - notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId) - roomDetailViewModel.process(RoomDetailActions.AcceptInvite) - } + override fun onAcceptInvite() { + notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId) + roomDetailViewModel.process(RoomDetailActions.AcceptInvite) + } - override fun onRejectInvite() { - notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId) - roomDetailViewModel.process(RoomDetailActions.RejectInvite) - } + override fun onRejectInvite() { + notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId) + roomDetailViewModel.process(RoomDetailActions.RejectInvite) + } // JumpToReadMarkerView.Callback - override fun onJumpToReadMarkerClicked(readMarkerId: String) { - roomDetailViewModel.process(RoomDetailActions.NavigateToEvent(readMarkerId, false)) - } - - override fun onClearReadMarkerClicked() { - roomDetailViewModel.process(RoomDetailActions.MarkAllAsRead) - } - - + override fun onJumpToReadMarkerClicked(readMarkerId: String) { + roomDetailViewModel.process(RoomDetailActions.NavigateToEvent(readMarkerId, false)) } + + override fun onClearReadMarkerClicked() { + roomDetailViewModel.process(RoomDetailActions.MarkAllAsRead) + } + + +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt index 62d80408d2..92e38c112b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt @@ -20,9 +20,15 @@ import androidx.recyclerview.widget.LinearLayoutManager import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.riotx.core.platform.DefaultListUpdateCallback import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import timber.log.Timber import java.util.concurrent.atomic.AtomicReference +/** + * This handles scrolling to an event which wasn't yet loaded when scheduled. + */ class ScrollOnHighlightedEventCallback(private val layoutManager: LinearLayoutManager, private val timelineEventController: TimelineEventController) : DefaultListUpdateCallback { @@ -30,7 +36,15 @@ class ScrollOnHighlightedEventCallback(private val layoutManager: LinearLayoutMa var timeline: Timeline? = null + override fun onInserted(position: Int, count: Int) { + scrollIfNeeded() + } + override fun onChanged(position: Int, count: Int, tag: Any?) { + scrollIfNeeded() + } + + private fun scrollIfNeeded() { val eventId = scheduledEventId.get() ?: return val nonNullTimeline = timeline ?: return val correctedEventId = nonNullTimeline.getFirstDisplayableEventId(eventId) From b6e18e4a8f0d5132fe2c6f1972836514c5adfc22 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 20 Sep 2019 18:34:58 +0200 Subject: [PATCH 013/197] Timeline: add badge also when unread without notif --- .../core/platform/BadgeFloatingActionButton.kt | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/core/platform/BadgeFloatingActionButton.kt b/vector/src/main/java/im/vector/riotx/core/platform/BadgeFloatingActionButton.kt index 545fd9409f..4de26cd657 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/BadgeFloatingActionButton.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/BadgeFloatingActionButton.kt @@ -114,6 +114,14 @@ class BadgeFloatingActionButton @JvmOverloads constructor( } } + var drawBadge: Boolean = false + set(value) { + if (field != value) { + field = value + invalidate() + } + } + init { countStr = countStr(count) textPaint.getTextBounds(countStr, 0, countStr.length, counterTextBounds) @@ -139,10 +147,10 @@ class BadgeFloatingActionButton @JvmOverloads constructor( override fun onDraw(canvas: Canvas) { super.onDraw(canvas) - - if (count > 0) { + if (count > 0 || drawBadge) { canvas.drawCircle(counterBounds.centerX(), counterBounds.centerY(), counterBounds.width() / 2f, tintPaint) - + } + if (count > 0) { val textX = counterBounds.centerX() - counterTextBounds.width() / 2f - counterTextBounds.left val textY = counterBounds.centerY() + counterTextBounds.height() / 2f - counterTextBounds.bottom canvas.drawText(countStr, textX, textY, textPaint) From f6d34ec7fdff94d1fa5c414f2dee9ae502dd3f44 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 23 Sep 2019 17:43:37 +0200 Subject: [PATCH 014/197] Timeline: update state management --- .../database/mapper/TimelineEventMapper.kt | 2 +- .../session/room/timeline/DefaultTimeline.kt | 186 ++++++++---------- .../home/room/detail/RoomDetailFragment.kt | 2 +- .../room/detail/ScrollOnNewMessageCallback.kt | 2 +- 4 files changed, 90 insertions(+), 102 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt index 0e9f13155e..5bd6f99b3a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt @@ -46,7 +46,7 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS readReceipts = readReceipts?.sortedByDescending { it.originServerTs } ?: emptyList(), - hasReadMarker = timelineEventEntity.readMarker?.eventId?.isEmpty() == false + hasReadMarker = timelineEventEntity.readMarker?.eventId?.isNotEmpty() == true ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index f22be74f70..9147b922cb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.session.room.timeline import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.crypto.CryptoService -import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.model.ReadReceipt import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.Timeline @@ -37,8 +36,6 @@ import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields import im.vector.matrix.android.internal.database.query.FilterContent import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates -import im.vector.matrix.android.internal.database.query.findIncludingEvent -import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.whereInRoom import im.vector.matrix.android.internal.task.TaskConstraints @@ -109,8 +106,8 @@ internal class DefaultTimeline( private var nextDisplayIndex: Int? = null private val builtEvents = Collections.synchronizedList(ArrayList()) private val builtEventsIdMap = Collections.synchronizedMap(HashMap()) - private val backwardsPaginationState = AtomicReference(PaginationState()) - private val forwardsPaginationState = AtomicReference(PaginationState()) + private val backwardsState = AtomicReference(State()) + private val forwardsState = AtomicReference(State()) private val timelineID = UUID.randomUUID().toString() @@ -126,43 +123,11 @@ internal class DefaultTimeline( if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL) { handleInitialLoad() } else { - // If changeSet has deletion we are having a gap, so we clear everything - if (changeSet.deletionRanges.isNotEmpty()) { - clearAllValues() - } - changeSet.insertionRanges.forEach { range -> - val (startDisplayIndex, direction) = if (range.startIndex == 0) { - Pair(filteredEvents[range.length - 1]!!.root!!.displayIndex, Timeline.Direction.FORWARDS) - } else { - Pair(filteredEvents[range.startIndex]!!.root!!.displayIndex, Timeline.Direction.BACKWARDS) - } - val state = getPaginationState(direction) - if (state.isPaginating) { - // We are getting new items from pagination - val shouldPostSnapshot = paginateInternal(startDisplayIndex, direction, state.requestedCount) - if (shouldPostSnapshot) { - postSnapshot() - } - } else { - // We are getting new items from sync - buildTimelineEvents(startDisplayIndex, direction, range.length.toLong()) - postSnapshot() - } - } - - var hasChanged = false - changeSet.changes.forEach { index -> - val eventEntity = results[index] - eventEntity?.eventId?.let { eventId -> - hasChanged = rebuildEvent(eventId) { - buildTimelineEvent(eventEntity) - } || hasChanged - } - } - if (hasChanged) postSnapshot() + handleUpdates(changeSet) } } + private val relationsListener = OrderedRealmCollectionChangeListener> { collection, changeSet -> var hasChange = false @@ -215,7 +180,6 @@ internal class DefaultTimeline( backgroundRealm.set(realm) clearUnlinkedEvents(realm) - roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst()?.also { it.sendingTimelineEvents.addChangeListener { _ -> postSnapshot() @@ -356,29 +320,29 @@ internal class DefaultTimeline( } ?: false } - private fun hasMoreInCache(direction: Timeline.Direction): Boolean { - return Realm.getInstance(realmConfiguration).use { localRealm -> - val timelineEventEntity = buildEventQuery(localRealm).findFirst(direction) - ?: return false - if (direction == Timeline.Direction.FORWARDS) { - val firstEvent = builtEvents.firstOrNull() ?: return true - firstEvent.displayIndex < timelineEventEntity.root!!.displayIndex - } else { - val lastEvent = builtEvents.lastOrNull() ?: return true - lastEvent.displayIndex > timelineEventEntity.root!!.displayIndex - } - } - } + private fun hasMoreInCache(direction: Timeline.Direction) = getState(direction).hasMoreInCache - private fun hasReachedEnd(direction: Timeline.Direction): Boolean { - return Realm.getInstance(realmConfiguration).use { localRealm -> - val currentChunk = findCurrentChunk(localRealm) ?: return false - if (direction == Timeline.Direction.FORWARDS) { - currentChunk.isLastForward - } else { - val eventEntity = buildEventQuery(localRealm).findFirst(direction) - currentChunk.isLastBackward || eventEntity?.root?.type == EventType.STATE_ROOM_CREATE - } + private fun hasReachedEnd(direction: Timeline.Direction) = getState(direction).hasReachedEnd + + private fun updateLoadingStates(results: RealmResults) { + val lastCacheEvent = results.lastOrNull() + val lastBuiltEvent = builtEvents.lastOrNull() + val firstCacheEvent = results.firstOrNull() + val firstBuiltEvent = builtEvents.firstOrNull() + val chunkEntity = getLiveChunk() + + updateState(Timeline.Direction.FORWARDS) { + it.copy( + hasMoreInCache = firstBuiltEvent == null || firstBuiltEvent.displayIndex < firstCacheEvent?.root?.displayIndex ?: Int.MIN_VALUE, + hasReachedEnd = chunkEntity?.isLastForward ?: false + ) + } + + updateState(Timeline.Direction.BACKWARDS) { + it.copy( + hasMoreInCache = lastBuiltEvent == null || lastBuiltEvent.displayIndex > lastCacheEvent?.root?.displayIndex ?: Int.MAX_VALUE, + hasReachedEnd = chunkEntity?.isLastBackward ?: false + ) } } @@ -391,16 +355,16 @@ internal class DefaultTimeline( direction: Timeline.Direction, count: Int, strict: Boolean = false): Boolean { - updatePaginationState(direction) { it.copy(requestedCount = count, isPaginating = true) } + updateState(direction) { it.copy(requestedPaginationCount = count, isPaginating = true) } val builtCount = buildTimelineEvents(startDisplayIndex, direction, count.toLong(), strict) val shouldFetchMore = builtCount < count && !hasReachedEnd(direction) if (shouldFetchMore) { val newRequestedCount = count - builtCount - updatePaginationState(direction) { it.copy(requestedCount = newRequestedCount) } + updateState(direction) { it.copy(requestedPaginationCount = newRequestedCount) } val fetchingCount = max(MIN_FETCHING_COUNT, newRequestedCount) executePaginationTask(direction, fetchingCount) } else { - updatePaginationState(direction) { it.copy(isPaginating = false, requestedCount = 0) } + updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } } return !shouldFetchMore @@ -412,7 +376,7 @@ internal class DefaultTimeline( private fun buildSendingEvents(): List { val sendingEvents = ArrayList() - if (hasReachedEnd(Timeline.Direction.FORWARDS)) { + if (hasReachedEnd(Timeline.Direction.FORWARDS) && !hasMoreInCache(Timeline.Direction.FORWARDS)) { roomEntity?.sendingTimelineEvents ?.where() ?.filterEventsWithSettings() @@ -425,20 +389,20 @@ internal class DefaultTimeline( } private fun canPaginate(direction: Timeline.Direction): Boolean { - return isReady.get() && !getPaginationState(direction).isPaginating && hasMoreToLoad(direction) + return isReady.get() && !getState(direction).isPaginating && hasMoreToLoad(direction) } - private fun getPaginationState(direction: Timeline.Direction): PaginationState { + private fun getState(direction: Timeline.Direction): State { return when (direction) { - Timeline.Direction.FORWARDS -> forwardsPaginationState.get() - Timeline.Direction.BACKWARDS -> backwardsPaginationState.get() + Timeline.Direction.FORWARDS -> forwardsState.get() + Timeline.Direction.BACKWARDS -> backwardsState.get() } } - private fun updatePaginationState(direction: Timeline.Direction, update: (PaginationState) -> PaginationState) { + private fun updateState(direction: Timeline.Direction, update: (State) -> State) { val stateReference = when (direction) { - Timeline.Direction.FORWARDS -> forwardsPaginationState - Timeline.Direction.BACKWARDS -> backwardsPaginationState + Timeline.Direction.FORWARDS -> forwardsState + Timeline.Direction.BACKWARDS -> backwardsState } val currentValue = stateReference.get() val newValue = update(currentValue) @@ -477,13 +441,51 @@ internal class DefaultTimeline( postSnapshot() } + /** + * This has to be called on TimelineThread as it access realm live results + */ + private fun handleUpdates(changeSet: OrderedCollectionChangeSet) { + // If changeSet has deletion we are having a gap, so we clear everything + if (changeSet.deletionRanges.isNotEmpty()) { + clearAllValues() + } + var postSnapshot = false + changeSet.insertionRanges.forEach { range -> + val (startDisplayIndex, direction) = if (range.startIndex == 0) { + Pair(filteredEvents[range.length - 1]!!.root!!.displayIndex, Timeline.Direction.FORWARDS) + } else { + Pair(filteredEvents[range.startIndex]!!.root!!.displayIndex, Timeline.Direction.BACKWARDS) + } + val state = getState(direction) + if (state.isPaginating) { + // We are getting new items from pagination + postSnapshot = paginateInternal(startDisplayIndex, direction, state.requestedPaginationCount) + } else { + // We are getting new items from sync + buildTimelineEvents(startDisplayIndex, direction, range.length.toLong()) + postSnapshot = true + } + } + changeSet.changes.forEach { index -> + val eventEntity = filteredEvents[index] + eventEntity?.eventId?.let { eventId -> + postSnapshot = rebuildEvent(eventId) { + buildTimelineEvent(eventEntity) + } || postSnapshot + } + } + if (postSnapshot) { + postSnapshot() + } + } + /** * This has to be called on TimelineThread as it access realm live results */ private fun executePaginationTask(direction: Timeline.Direction, limit: Int) { val token = getTokenLive(direction) if (token == null) { - updatePaginationState(direction) { it.copy(isPaginating = false, requestedCount = 0) } + updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } return } val params = PaginationTask.Params(roomId = roomId, @@ -622,15 +624,6 @@ internal class DefaultTimeline( } } - private fun findCurrentChunk(realm: Realm): ChunkEntity? { - val currentInitialEventId = initialEventId - return if (currentInitialEventId == null) { - ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) - } else { - ChunkEntity.findIncludingEvent(realm, currentInitialEventId) - } - } - private fun clearUnlinkedEvents(realm: Realm) { realm.executeTransaction { val unlinkedChunks = ChunkEntity @@ -651,6 +644,7 @@ internal class DefaultTimeline( if (isReady.get().not()) { return@post } + updateLoadingStates(filteredEvents) val snapshot = createSnapshot() val runnable = Runnable { listener?.onUpdated(snapshot) } debouncer.debounce("post_snapshot", runnable, 50) @@ -662,8 +656,8 @@ internal class DefaultTimeline( nextDisplayIndex = null builtEvents.clear() builtEventsIdMap.clear() - backwardsPaginationState.set(PaginationState()) - forwardsPaginationState.set(PaginationState()) + backwardsState.set(State()) + forwardsState.set(State()) } @@ -673,16 +667,6 @@ internal class DefaultTimeline( return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS } - private fun RealmQuery.findFirst(direction: Timeline.Direction): TimelineEventEntity? { - return if (direction == Timeline.Direction.FORWARDS) { - sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING) - } else { - sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.ASCENDING) - } - .filterEventsWithSettings() - .findFirst() - } - private fun RealmQuery.filterEventsWithSettings(): RealmQuery { if (settings.filterTypes) { `in`(TimelineEventEntityFields.ROOT.TYPE, settings.allowedTypes.toTypedArray()) @@ -693,9 +677,13 @@ internal class DefaultTimeline( return this } + private data class State( + val hasReachedEnd: Boolean = false, + val hasMoreInCache: Boolean = true, + val isPaginating: Boolean = false, + val requestedPaginationCount: Int = 0 + ) + } -private data class PaginationState( - val isPaginating: Boolean = false, - val requestedCount: Int = 0 -) + diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 72f0005d4c..a55bd8bcc0 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -532,7 +532,7 @@ class RoomDetailFragment : private fun updateJumpToBottomViewVisibility() { debouncer.debounce("jump_to_bottom_visibility", 250, Runnable { Timber.v("First visible: ${layoutManager.findFirstCompletelyVisibleItemPosition()}") - if (layoutManager.findFirstCompletelyVisibleItemPosition() != 0) { + if (layoutManager.findFirstVisibleItemPosition() != 0) { jumpToBottomView.show() } else { jumpToBottomView.hide() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnNewMessageCallback.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnNewMessageCallback.kt index 998428477b..f4cfe9eb5a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnNewMessageCallback.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnNewMessageCallback.kt @@ -26,7 +26,7 @@ class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager, override fun onInserted(position: Int, count: Int) { Timber.v("On inserted $count count at position: $position") - if (position == 0 && layoutManager.findFirstCompletelyVisibleItemPosition() == 0 && !timelineEventController.isLoadingForward()) { + if (position == 0 && layoutManager.findFirstVisibleItemPosition() == 0 && !timelineEventController.isLoadingForward()) { layoutManager.scrollToPosition(0) } } From 51568c30a60ec8c4b130743d05904fbb8fae7a4e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 24 Sep 2019 10:23:51 +0200 Subject: [PATCH 015/197] Version++ --- CHANGES.md | 21 +++++++++++++++++++++ vector/build.gradle | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 8c2372a3b1..07eab082d0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,24 @@ +Changes in RiotX 0.7.0 (2019-XX-XX) +=================================================== + +Features: + - + +Improvements: + - + +Other changes: + - + +Bugfix: + - + +Translations: + - + +Build: + - + Changes in RiotX 0.6.0 (2019-09-24) =================================================== diff --git a/vector/build.gradle b/vector/build.gradle index 697d0f36d0..16bf70aef0 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -15,7 +15,7 @@ androidExtensions { } ext.versionMajor = 0 -ext.versionMinor = 6 +ext.versionMinor = 7 ext.versionPatch = 0 static def getGitTimestamp() { From 6890f83810dba3783d4971feb1cdb112b40e37c2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 24 Sep 2019 10:47:29 +0200 Subject: [PATCH 016/197] Cleanup dead code --- .../riotx/features/home/HomeActivity.kt | 15 ----------- .../features/home/HomeActivityViewModel.kt | 25 +------------------ 2 files changed, 1 insertion(+), 39 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt index 2a43ca705a..c3636cde7a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt @@ -16,7 +16,6 @@ package im.vector.riotx.features.home -import android.app.ProgressDialog import android.content.Context import android.content.Intent import android.os.Bundle @@ -66,8 +65,6 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { @Inject lateinit var pushManager: PushersManager @Inject lateinit var notificationDrawerManager: NotificationDrawerManager - private var progress: ProgressDialog? = null - private val drawerListener = object : DrawerLayout.SimpleDrawerListener() { override fun onDrawerStateChanged(newState: Int) { hideKeyboard() @@ -93,18 +90,6 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { replaceFragment(homeDrawerFragment, R.id.homeDrawerFragmentContainer) } - homeActivityViewModel.isLoading.observe(this, Observer { - // TODO better UI - if (it) { - progress?.dismiss() - progress = ProgressDialog(this) - progress?.setMessage(getString(R.string.room_recents_create_room)) - progress?.show() - } else { - progress?.dismiss() - } - }) - navigationViewModel.navigateTo.observeEvent(this) { navigation -> when (navigation) { is Navigation.OpenDrawer -> drawerLayout.openDrawer(GravityCompat.START) diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewModel.kt index 36d5725561..f8c1eca19e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewModel.kt @@ -16,8 +16,6 @@ package im.vector.riotx.features.home -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import arrow.core.Option import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.MvRxState @@ -25,11 +23,9 @@ import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject -import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.room.model.RoomSummary -import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.rx.rx import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.features.home.group.ALL_COMMUNITIES_GROUP_ID @@ -61,10 +57,6 @@ class HomeActivityViewModel @AssistedInject constructor(@Assisted initialState: } - private val _isLoading = MutableLiveData() - val isLoading: LiveData - get() = _isLoading - init { session.addListener(this) observeRoomAndGroup() @@ -93,7 +85,7 @@ class HomeActivityViewModel @AssistedInject constructor(@Assisted initialState: .filter { !it.isDirect } .filter { selectedGroup?.groupId == ALL_COMMUNITIES_GROUP_ID - || selectedGroup?.roomIds?.contains(it.roomId) ?: true + || selectedGroup?.roomIds?.contains(it.roomId) ?: true } filteredDirectRooms + filteredGroupRooms } @@ -104,21 +96,6 @@ class HomeActivityViewModel @AssistedInject constructor(@Assisted initialState: .disposeOnClear() } - fun createRoom(createRoomParams: CreateRoomParams = CreateRoomParams()) { - _isLoading.value = true - - session.createRoom(createRoomParams, object : MatrixCallback { - override fun onSuccess(data: String) { - _isLoading.value = false - } - - override fun onFailure(failure: Throwable) { - _isLoading.value = false - super.onFailure(failure) - } - }) - } - override fun onCleared() { super.onCleared() From af433266c88a2e5ec85c812a2d818021a78d7a3a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 24 Sep 2019 11:24:03 +0200 Subject: [PATCH 017/197] Move currentDisplayMode to the ViewState --- .../riotx/features/home/HomeDetailFragment.kt | 20 +++++-------------- .../features/home/HomeDetailViewModel.kt | 12 ++++++++++- .../features/home/HomeDetailViewState.kt | 2 ++ 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt index acfac104d4..a236bec747 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt @@ -51,8 +51,6 @@ data class HomeDetailParams( ) : Parcelable -private const val CURRENT_DISPLAY_MODE = "CURRENT_DISPLAY_MODE" - private const val INDEX_CATCHUP = 0 private const val INDEX_PEOPLE = 1 private const val INDEX_ROOMS = 2 @@ -61,7 +59,6 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate { private val params: HomeDetailParams by args() private val unreadCounterBadgeViews = arrayListOf() - private lateinit var currentDisplayMode: RoomListFragment.DisplayMode private val viewModel: HomeDetailViewModel by fragmentViewModel() private lateinit var navigationViewModel: HomeNavigationViewModel @@ -80,15 +77,16 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate { override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - currentDisplayMode = savedInstanceState?.getSerializable(CURRENT_DISPLAY_MODE) as? RoomListFragment.DisplayMode - ?: RoomListFragment.DisplayMode.HOME navigationViewModel = ViewModelProviders.of(requireActivity()).get(HomeNavigationViewModel::class.java) - switchDisplayMode(currentDisplayMode) setupBottomNavigationView() setupToolbar() setupKeysBackupBanner() + + viewModel.selectSubscribe(this, HomeDetailViewState::displayMode) { displayMode -> + switchDisplayMode(displayMode) + } } private fun setupKeysBackupBanner() { @@ -126,11 +124,6 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate { } - override fun onSaveInstanceState(outState: Bundle) { - outState.putSerializable(CURRENT_DISPLAY_MODE, currentDisplayMode) - super.onSaveInstanceState(outState) - } - private fun setupToolbar() { val parentActivity = vectorBaseActivity if (parentActivity is ToolbarConfigurable) { @@ -156,10 +149,7 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate { R.id.bottom_action_rooms -> RoomListFragment.DisplayMode.ROOMS else -> RoomListFragment.DisplayMode.HOME } - if (currentDisplayMode != displayMode) { - currentDisplayMode = displayMode - switchDisplayMode(displayMode) - } + viewModel.switchDisplayMode(displayMode) true } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt index 917cafe149..be8648da35 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt @@ -24,10 +24,12 @@ import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.session.Session import im.vector.matrix.rx.rx import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.features.home.room.list.RoomListFragment import io.reactivex.schedulers.Schedulers /** - * View model used to update the home bottom bar notification counts + * View model used to update the home bottom bar notification counts, observe the sync state and + * change the selected room list view */ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: HomeDetailViewState, private val session: Session, @@ -53,6 +55,14 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho observeRoomSummaries() } + fun switchDisplayMode(displayMode: RoomListFragment.DisplayMode) = withState { state -> + if (state.displayMode != displayMode) { + setState { + copy(displayMode = displayMode) + } + } + } + // PRIVATE METHODS ***************************************************************************** private fun observeSyncState() { diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewState.kt index 970951bfba..a8f89cc566 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewState.kt @@ -18,8 +18,10 @@ package im.vector.riotx.features.home import com.airbnb.mvrx.MvRxState import im.vector.matrix.android.api.session.sync.SyncState +import im.vector.riotx.features.home.room.list.RoomListFragment data class HomeDetailViewState( + val displayMode: RoomListFragment.DisplayMode = RoomListFragment.DisplayMode.HOME, val notificationCountCatchup: Int = 0, val notificationHighlightCatchup: Boolean = false, val notificationCountPeople: Int = 0, From 9e1ded941fdf340c8a4811e4e1cb44c128da3bd0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 24 Sep 2019 12:29:37 +0200 Subject: [PATCH 018/197] Persist active tab between sessions (#503) --- CHANGES.md | 2 +- .../vector/riotx/core/di/ScreenComponent.kt | 3 + .../vector/riotx/core/di/VectorComponent.kt | 3 + .../riotx/features/home/HomeDetailFragment.kt | 6 ++ .../features/home/HomeDetailViewModel.kt | 12 ++++ .../riotx/features/ui/UiStateRepository.kt | 56 +++++++++++++++++++ 6 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/ui/UiStateRepository.kt diff --git a/CHANGES.md b/CHANGES.md index 07eab082d0..ee803b43cf 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,7 +5,7 @@ Features: - Improvements: - - + - Persist active tab between sessions (#503) Other changes: - diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index 01db4b4a01..3b18d3042e 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -65,6 +65,7 @@ import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerFragment import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment import im.vector.riotx.features.settings.* import im.vector.riotx.features.settings.push.PushGatewaysFragment +import im.vector.riotx.features.ui.UiStateRepository @Component(dependencies = [VectorComponent::class], modules = [AssistedInjectModule::class, ViewModelModule::class, HomeModule::class]) @ScreenScope @@ -80,6 +81,8 @@ interface ScreenComponent { fun navigator(): Navigator + fun uiStateRepository(): UiStateRepository + fun inject(activity: HomeActivity) fun inject(roomDetailFragment: RoomDetailFragment) diff --git a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt index a76091fb36..61c461c655 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt @@ -42,6 +42,7 @@ import im.vector.riotx.features.rageshake.BugReporter import im.vector.riotx.features.rageshake.VectorFileLogger import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler import im.vector.riotx.features.settings.VectorPreferences +import im.vector.riotx.features.ui.UiStateRepository import javax.inject.Singleton @Component(modules = [VectorModule::class]) @@ -106,6 +107,8 @@ interface VectorComponent { fun vectorFileLogger(): VectorFileLogger + fun uiStateRepository(): UiStateRepository + @Component.Factory interface Factory { fun create(@BindsInstance context: Context): VectorComponent diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt index a236bec747..112fae1b24 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt @@ -166,6 +166,12 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate { private fun switchDisplayMode(displayMode: RoomListFragment.DisplayMode) { groupToolbarTitleView.setText(displayMode.titleRes) updateSelectedFragment(displayMode) + // Update the navigation view (for when we restore the tabs) + bottomNavigationView.selectedItemId = when (displayMode) { + RoomListFragment.DisplayMode.PEOPLE -> R.id.bottom_action_people + RoomListFragment.DisplayMode.ROOMS -> R.id.bottom_action_rooms + else -> R.id.bottom_action_home + } } private fun updateSelectedFragment(displayMode: RoomListFragment.DisplayMode) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt index be8648da35..688d2b6b7b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt @@ -23,8 +23,10 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.session.Session import im.vector.matrix.rx.rx +import im.vector.riotx.core.di.HasScreenInjector import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.features.home.room.list.RoomListFragment +import im.vector.riotx.features.ui.UiStateRepository import io.reactivex.schedulers.Schedulers /** @@ -33,6 +35,7 @@ import io.reactivex.schedulers.Schedulers */ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: HomeDetailViewState, private val session: Session, + private val uiStateRepository: UiStateRepository, private val homeRoomListStore: HomeRoomListObservableStore) : VectorViewModel(initialState) { @@ -43,6 +46,13 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho companion object : MvRxViewModelFactory { + override fun initialState(viewModelContext: ViewModelContext): HomeDetailViewState? { + val uiStateRepository = (viewModelContext.activity as HasScreenInjector).injector().uiStateRepository() + return HomeDetailViewState( + displayMode = uiStateRepository.getDisplayMode() + ) + } + @JvmStatic override fun create(viewModelContext: ViewModelContext, state: HomeDetailViewState): HomeDetailViewModel? { val fragment: HomeDetailFragment = (viewModelContext as FragmentViewModelContext).fragment() @@ -60,6 +70,8 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho setState { copy(displayMode = displayMode) } + + uiStateRepository.storeDisplayMode(displayMode) } } diff --git a/vector/src/main/java/im/vector/riotx/features/ui/UiStateRepository.kt b/vector/src/main/java/im/vector/riotx/features/ui/UiStateRepository.kt new file mode 100644 index 0000000000..be8e624100 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/ui/UiStateRepository.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2019 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.riotx.features.ui + +import android.content.SharedPreferences +import androidx.core.content.edit +import im.vector.riotx.features.home.room.list.RoomListFragment +import javax.inject.Inject + + +/** + * This class is used to persist UI state across application restart + */ +class UiStateRepository @Inject constructor(private val sharedPreferences: SharedPreferences) { + + fun getDisplayMode(): RoomListFragment.DisplayMode { + return when (sharedPreferences.getInt(KEY_DISPLAY_MODE, VALUE_DISPLAY_MODE_CATCHUP)) { + VALUE_DISPLAY_MODE_PEOPLE -> RoomListFragment.DisplayMode.PEOPLE + VALUE_DISPLAY_MODE_ROOMS -> RoomListFragment.DisplayMode.ROOMS + else -> RoomListFragment.DisplayMode.HOME + } + } + + fun storeDisplayMode(displayMode: RoomListFragment.DisplayMode) { + sharedPreferences.edit { + putInt(KEY_DISPLAY_MODE, + when (displayMode) { + RoomListFragment.DisplayMode.PEOPLE -> VALUE_DISPLAY_MODE_PEOPLE + RoomListFragment.DisplayMode.ROOMS -> VALUE_DISPLAY_MODE_ROOMS + else -> VALUE_DISPLAY_MODE_CATCHUP + }) + } + } + + + companion object { + private const val KEY_DISPLAY_MODE = "UI_STATE_DISPLAY_MODE" + private const val VALUE_DISPLAY_MODE_CATCHUP = 0 + private const val VALUE_DISPLAY_MODE_PEOPLE = 1 + private const val VALUE_DISPLAY_MODE_ROOMS = 2 + } +} \ No newline at end of file From c6d01fbcf48e3828f7d7e668333190dfdcce2b4e Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 24 Sep 2019 12:57:32 +0200 Subject: [PATCH 019/197] ReadMarker: extract from ViewModel the jump to read marker visibility logic as it's easier to deal with. --- .../home/room/detail/ReadMarkerHelper.kt | 75 ++++++++++++++++ .../home/room/detail/RoomDetailFragment.kt | 67 ++++++++------ .../home/room/detail/RoomDetailViewModel.kt | 88 +++++-------------- .../home/room/detail/RoomDetailViewState.kt | 1 - .../ScrollOnHighlightedEventCallback.kt | 1 - .../timeline/TimelineEventController.kt | 6 +- 6 files changed, 140 insertions(+), 98 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt new file mode 100644 index 0000000000..c162098cff --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt @@ -0,0 +1,75 @@ +/* + + * Copyright 2019 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.riotx.features.home.room.detail + +import androidx.recyclerview.widget.LinearLayoutManager +import im.vector.riotx.core.di.ScreenScope +import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController +import timber.log.Timber +import javax.inject.Inject + +@ScreenScope +class ReadMarkerHelper @Inject constructor() { + + lateinit var timelineEventController: TimelineEventController + lateinit var layoutManager: LinearLayoutManager + var callback: Callback? = null + + private var state: RoomDetailViewState? = null + + fun updateState(state: RoomDetailViewState) { + this.state = state + checkJumpToReadMarkerVisibility() + } + + fun onTimelineScrolled() { + checkJumpToReadMarkerVisibility() + } + + private fun checkJumpToReadMarkerVisibility() { + val nonNullState = this.state ?: return + val lastVisibleItem = layoutManager.findLastVisibleItemPosition() + val readMarkerId = nonNullState.asyncRoomSummary()?.readMarkerId + if (readMarkerId == null) { + callback?.onVisibilityUpdated(false, null) + } + val positionOfReadMarker = timelineEventController.searchPositionOfEvent(readMarkerId) + Timber.v("Position of readMarker: $positionOfReadMarker") + Timber.v("Position of lastVisibleItem: $lastVisibleItem") + if (positionOfReadMarker == null) { + if (nonNullState.timeline?.isLive == true && lastVisibleItem > 0) { + callback?.onVisibilityUpdated(true, readMarkerId) + } else { + callback?.onVisibilityUpdated(false, readMarkerId) + } + } else { + if (positionOfReadMarker > lastVisibleItem) { + callback?.onVisibilityUpdated(true, readMarkerId) + } else { + callback?.onVisibilityUpdated(false, readMarkerId) + } + } + } + + + interface Callback { + fun onVisibilityUpdated(show: Boolean, readMarkerId: String?) + } + + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index a55bd8bcc0..a94e1941a1 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -227,6 +227,7 @@ class RoomDetailFragment : @Inject lateinit var errorFormatter: ErrorFormatter @Inject lateinit var eventHtmlRenderer: EventHtmlRenderer @Inject lateinit var vectorPreferences: VectorPreferences + @Inject lateinit var readMarkerHelper: ReadMarkerHelper private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback private lateinit var scrollOnHighlightedEventCallback: ScrollOnHighlightedEventCallback @@ -398,22 +399,22 @@ class RoomDetailFragment : if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { val parser = Parser.builder().build() val document = parser.parse(messageContent.formattedBody - ?: messageContent.body) + ?: messageContent.body) formattedBody = eventHtmlRenderer.render(document) } composerLayout.composerRelatedMessageContent.text = formattedBody - ?: nonFormattedBody + ?: nonFormattedBody updateComposerText(defaultContent) composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes)) avatarRenderer.render(event.senderAvatar, event.root.senderId - ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) + ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) avatarRenderer.render(event.senderAvatar, - event.root.senderId ?: "", - event.senderName, - composerLayout.composerRelatedMessageAvatar) + event.root.senderId ?: "", + event.senderName, + composerLayout.composerRelatedMessageAvatar) composerLayout.expand { //need to do it here also when not using quick reply focusComposerAndShowKeyboard() @@ -451,9 +452,9 @@ class RoomDetailFragment : REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data) REACTION_SELECT_REQUEST_CODE -> { val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID) - ?: return + ?: return val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT) - ?: return + ?: return //TODO check if already reacted with that? roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId)) } @@ -479,6 +480,13 @@ class RoomDetailFragment : it.dispatchTo(scrollOnNewMessageCallback) it.dispatchTo(scrollOnHighlightedEventCallback) } + readMarkerHelper.timelineEventController = timelineEventController + readMarkerHelper.layoutManager = layoutManager + readMarkerHelper.callback = object : ReadMarkerHelper.Callback { + override fun onVisibilityUpdated(show: Boolean, readMarkerId: String?) { + jumpToReadMarkerView.render(show, readMarkerId) + } + } recyclerView.setController(timelineEventController) recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { @@ -486,6 +494,7 @@ class RoomDetailFragment : if (recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE) { updateJumpToBottomViewVisibility() } + readMarkerHelper.onTimelineScrolled() } override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { @@ -504,26 +513,26 @@ class RoomDetailFragment : if (vectorPreferences.swipeToReplyIsEnabled()) { val swipeCallback = RoomMessageTouchHelperCallback(requireContext(), - R.drawable.ic_reply, - object : RoomMessageTouchHelperCallback.QuickReplayHandler { - override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { - (model as? AbsMessageItem)?.attributes?.informationData?.let { - val eventId = it.eventId - roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId, composerLayout.composerEditText.text.toString())) - } - } + R.drawable.ic_reply, + object : RoomMessageTouchHelperCallback.QuickReplayHandler { + override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { + (model as? AbsMessageItem)?.attributes?.informationData?.let { + val eventId = it.eventId + roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId, composerLayout.composerEditText.text.toString())) + } + } - override fun canSwipeModel(model: EpoxyModel<*>): Boolean { - return when (model) { - is MessageFileItem, - is MessageImageVideoItem, - is MessageTextItem -> { - return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED - } - else -> false - } - } - }) + override fun canSwipeModel(model: EpoxyModel<*>): Boolean { + return when (model) { + is MessageFileItem, + is MessageImageVideoItem, + is MessageTextItem -> { + return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED + } + else -> false + } + } + }) val touchHelper = ItemTouchHelper(swipeCallback) touchHelper.attachToRecyclerView(recyclerView) } @@ -698,6 +707,7 @@ class RoomDetailFragment : } private fun renderState(state: RoomDetailViewState) { + readMarkerHelper.updateState(state) renderRoomSummary(state) val summary = state.asyncRoomSummary() val inviter = state.asyncInviter() @@ -726,7 +736,6 @@ class RoomDetailFragment : composerLayout.visibility = View.GONE notificationAreaView.render(NotificationAreaView.State.Tombstone(state.tombstoneEvent)) } - jumpToReadMarkerView.render(state.showJumpToReadMarker, summary?.readMarkerId) } private fun renderRoomSummary(state: RoomDetailViewState) { @@ -1151,7 +1160,7 @@ class RoomDetailFragment : roomDetailViewModel.process(RoomDetailActions.RejectInvite) } -// JumpToReadMarkerView.Callback + // JumpToReadMarkerView.Callback override fun onJumpToReadMarkerClicked(readMarkerId: String) { roomDetailViewModel.process(RoomDetailActions.NavigateToEvent(readMarkerId, false)) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 72a09fbad6..22850a96f0 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -119,7 +119,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro observeRoomSummary() observeEventDisplayedActions() observeSummaryState() - observeJumpToReadMarkerViewVisibility() observeReadMarkerVisibility() observeDrafts() room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear() @@ -185,23 +184,23 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro copy( // Create a sendMode from a draft and retrieve the TimelineEvent sendMode = when (draft) { - is UserDraft.REGULAR -> SendMode.REGULAR(draft.text) - is UserDraft.QUOTE -> { - room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> - SendMode.QUOTE(timelineEvent, draft.text) - } - } - is UserDraft.REPLY -> { - room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> - SendMode.REPLY(timelineEvent, draft.text) - } - } - is UserDraft.EDIT -> { - room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> - SendMode.EDIT(timelineEvent, draft.text) - } - } - } ?: SendMode.REGULAR("") + is UserDraft.REGULAR -> SendMode.REGULAR(draft.text) + is UserDraft.QUOTE -> { + room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> + SendMode.QUOTE(timelineEvent, draft.text) + } + } + is UserDraft.REPLY -> { + room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> + SendMode.REPLY(timelineEvent, draft.text) + } + } + is UserDraft.EDIT -> { + room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> + SendMode.EDIT(timelineEvent, draft.text) + } + } + } ?: SendMode.REGULAR("") ) } } @@ -210,7 +209,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private fun handleTombstoneEvent(action: RoomDetailActions.HandleTombstoneEvent) { val tombstoneContent = action.event.getClearContent().toModel() - ?: return + ?: return val roomId = tombstoneContent.replacementRoom ?: "" val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN @@ -345,7 +344,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is SendMode.EDIT -> { //is original event a reply? val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel()?.relatesTo?.inReplyTo?.eventId - ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId + ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId if (inReplyTo != null) { //TODO check if same content? room.getTimeLineEvent(inReplyTo)?.let { @@ -354,13 +353,13 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } else { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val existingBody = messageContent?.body ?: "" if (existingBody != action.text) { room.editTextMessage(state.sendMode.timelineEvent.root.eventId ?: "", - messageContent?.type ?: MessageType.MSGTYPE_TEXT, - action.text, - action.autoMarkdown) + messageContent?.type ?: MessageType.MSGTYPE_TEXT, + action.text, + action.autoMarkdown) } else { Timber.w("Same message content, do not send edition") } @@ -371,7 +370,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is SendMode.QUOTE -> { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val textMsg = messageContent?.body val finalText = legacyRiotQuoteText(textMsg, action.text) @@ -702,45 +701,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro .disposeOnClear() } - private fun observeJumpToReadMarkerViewVisibility() { - Observable.combineLatest( - room.rx().liveRoomSummary() - .map { - val readMarkerId = it.readMarkerId - if (readMarkerId == null) { - Option.empty() - } else { - val readMarkerIndex = room.getTimeLineEvent(readMarkerId)?.displayIndex - ?: Int.MIN_VALUE - Option.just(readMarkerIndex) - } - } - .distinctUntilChanged(), - visibleEventsObservable.distinctUntilChanged(), - isEventVisibleObservable { it.hasReadMarker }.startWith(false).takeUntil { it }, - Function3, RoomDetailActions.TimelineEventTurnsVisible, Boolean, Boolean> { readMarkerIndex, currentVisibleEvent, isReadMarkerViewVisible -> - if (readMarkerIndex.isEmpty() || isReadMarkerViewVisible) { - false - } else { - val currentVisibleEventPosition = currentVisibleEvent.event.displayIndex - readMarkerIndex.getOrElse { Int.MIN_VALUE } < currentVisibleEventPosition - } - } - ) - .distinctUntilChanged() - .subscribe { - setState { copy(showJumpToReadMarker = it) } - } - .disposeOnClear() - } - - private fun isEventVisibleObservable(filterEvent: (TimelineEvent) -> Boolean): Observable { - return Observable.merge( - visibleEventsObservable.filter { filterEvent(it.event) }.map { true }, - invisibleEventsObservable.filter { filterEvent(it.event) }.map { false } - ) - } - private fun observeRoomSummary() { room.rx().liveRoomSummary() .execute { async -> diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt index 3a8afb1ebe..2be78506a0 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt @@ -52,7 +52,6 @@ data class RoomDetailViewState( val tombstoneEvent: Event? = null, val tombstoneEventHandling: Async = Uninitialized, val syncState: SyncState = SyncState.IDLE, - val showJumpToReadMarker: Boolean = false, val highlightedEventId: String? = null, val hideReadMarker: Boolean = false ) : MvRxState { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt index 92e38c112b..08add3f0c7 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt @@ -56,7 +56,6 @@ class ScrollOnHighlightedEventCallback(private val layoutManager: LinearLayoutMa // Do not scroll it item is already visible if (positionToScroll !in firstVisibleItem..lastVisibleItem) { Timber.v("Scroll to $positionToScroll") - // Note: Offset will be from the bottom, since the layoutManager is reversed layoutManager.scrollToPosition(positionToScroll) } scheduledEventId.set(null) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index 701e412b1d..fdc37a7f35 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -158,7 +158,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec synchronized(modelCache) { for (i in 0 until modelCache.size) { if (modelCache[i]?.eventId == viewState.highlightedEventId - || modelCache[i]?.eventId == eventIdToHighlight) { + || modelCache[i]?.eventId == eventIdToHighlight) { modelCache[i] = null } } @@ -229,8 +229,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec // Should be build if not cached or if cached but contains mergedHeader or formattedDay // We then are sure we always have items up to date. if (modelCache[position] == null - || modelCache[position]?.mergedHeaderModel != null - || modelCache[position]?.formattedDayModel != null) { + || modelCache[position]?.mergedHeaderModel != null + || modelCache[position]?.formattedDayModel != null) { modelCache[position] = buildItemModels(position, currentSnapshot) } } From 1c9cf7a81072fa374457d0706af64a2d6918ec40 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 24 Sep 2019 13:40:03 +0200 Subject: [PATCH 020/197] Dagger code cleanup --- .../android/internal/crypto/CryptoModule.kt | 18 +++++++++--------- .../android/internal/session/SessionModule.kt | 2 +- .../internal/session/filter/FilterModule.kt | 2 +- .../internal/session/pushers/GetPushersTask.kt | 4 ++-- .../internal/session/pushers/PushersModule.kt | 2 +- .../internal/session/room/RoomModule.kt | 2 +- .../im/vector/riotx/core/di/VectorComponent.kt | 2 +- .../im/vector/riotx/core/di/VectorModule.kt | 2 +- 8 files changed, 17 insertions(+), 17 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt index 742e3ff21a..26647457a1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt @@ -117,13 +117,13 @@ internal abstract class CryptoModule { abstract fun bindGetDevicesTask(getDevicesTask: DefaultGetDevicesTask): GetDevicesTask @Binds - abstract fun bindSetDeviceNameTask(getDevicesTask: DefaultSetDeviceNameTask): SetDeviceNameTask + abstract fun bindSetDeviceNameTask(setDeviceNameTask: DefaultSetDeviceNameTask): SetDeviceNameTask @Binds - abstract fun bindUploadKeysTask(getDevicesTask: DefaultUploadKeysTask): UploadKeysTask + abstract fun bindUploadKeysTask(uploadKeysTask: DefaultUploadKeysTask): UploadKeysTask @Binds - abstract fun bindDownloadKeysForUsersTask(downloadKeysForUsers: DefaultDownloadKeysForUsers): DownloadKeysForUsersTask + abstract fun bindDownloadKeysForUsersTask(downloadKeysForUsersTask: DefaultDownloadKeysForUsers): DownloadKeysForUsersTask @Binds abstract fun bindCreateKeysBackupVersionTask(createKeysBackupVersionTask: DefaultCreateKeysBackupVersionTask): CreateKeysBackupVersionTask @@ -135,10 +135,10 @@ internal abstract class CryptoModule { abstract fun bindDeleteRoomSessionDataTask(deleteRoomSessionDataTask: DefaultDeleteRoomSessionDataTask): DeleteRoomSessionDataTask @Binds - abstract fun bindDeleteRoomSessionsDataTask(deleteRoomSessionDataTask: DefaultDeleteRoomSessionsDataTask): DeleteRoomSessionsDataTask + abstract fun bindDeleteRoomSessionsDataTask(deleteRoomSessionsDataTask: DefaultDeleteRoomSessionsDataTask): DeleteRoomSessionsDataTask @Binds - abstract fun bindDeleteSessionsDataTask(deleteRoomSessionDataTask: DefaultDeleteSessionsDataTask): DeleteSessionsDataTask + abstract fun bindDeleteSessionsDataTask(deleteSessionsDataTask: DefaultDeleteSessionsDataTask): DeleteSessionsDataTask @Binds abstract fun bindGetKeysBackupLastVersionTask(getKeysBackupLastVersionTask: DefaultGetKeysBackupLastVersionTask): GetKeysBackupLastVersionTask @@ -150,19 +150,19 @@ internal abstract class CryptoModule { abstract fun bindGetRoomSessionDataTask(getRoomSessionDataTask: DefaultGetRoomSessionDataTask): GetRoomSessionDataTask @Binds - abstract fun bindGetRoomSessionsDataTask(getRoomSessionDataTask: DefaultGetRoomSessionsDataTask): GetRoomSessionsDataTask + abstract fun bindGetRoomSessionsDataTask(getRoomSessionsDataTask: DefaultGetRoomSessionsDataTask): GetRoomSessionsDataTask @Binds - abstract fun bindGetSessionsDataTask(getRoomSessionDataTask: DefaultGetSessionsDataTask): GetSessionsDataTask + abstract fun bindGetSessionsDataTask(getSessionsDataTask: DefaultGetSessionsDataTask): GetSessionsDataTask @Binds abstract fun bindStoreRoomSessionDataTask(storeRoomSessionDataTask: DefaultStoreRoomSessionDataTask): StoreRoomSessionDataTask @Binds - abstract fun bindStoreRoomSessionsDataTask(storeRoomSessionDataTask: DefaultStoreRoomSessionsDataTask): StoreRoomSessionsDataTask + abstract fun bindStoreRoomSessionsDataTask(storeRoomSessionsDataTask: DefaultStoreRoomSessionsDataTask): StoreRoomSessionsDataTask @Binds - abstract fun bindStoreSessionsDataTask(storeRoomSessionDataTask: DefaultStoreSessionsDataTask): StoreSessionsDataTask + abstract fun bindStoreSessionsDataTask(storeSessionsDataTask: DefaultStoreSessionsDataTask): StoreSessionsDataTask @Binds abstract fun bindUpdateKeysBackupVersionTask(updateKeysBackupVersionTask: DefaultUpdateKeysBackupVersionTask): UpdateKeysBackupVersionTask diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index a08c7e4ab7..db4997ca89 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -162,7 +162,7 @@ internal abstract class SessionModule { @Binds @IntoSet - abstract fun bindEventRelationsAggregationUpdater(groupSummaryUpdater: EventRelationsAggregationUpdater): LiveEntityObserver + abstract fun bindEventRelationsAggregationUpdater(eventRelationsAggregationUpdater: EventRelationsAggregationUpdater): LiveEntityObserver @Binds @IntoSet diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterModule.kt index 745a92b3f9..0fc125a172 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterModule.kt @@ -43,7 +43,7 @@ internal abstract class FilterModule { abstract fun bindFilterService(filterService: DefaultFilterService): FilterService @Binds - abstract fun bindSaveFilterTask(saveFilterTask_Factory: DefaultSaveFilterTask): SaveFilterTask + abstract fun bindSaveFilterTask(saveFilterTask: DefaultSaveFilterTask): SaveFilterTask } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/GetPushersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/GetPushersTask.kt index 8fd1a5b3be..ffaedad652 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/GetPushersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/GetPushersTask.kt @@ -26,8 +26,8 @@ import javax.inject.Inject internal interface GetPushersTask : Task -internal class DefaultGetPusherTask @Inject constructor(private val pushersAPI: PushersAPI, - private val monarchy: Monarchy) : GetPushersTask { +internal class DefaultGetPushersTask @Inject constructor(private val pushersAPI: PushersAPI, + private val monarchy: Monarchy) : GetPushersTask { override suspend fun execute(params: Unit) { val response = executeRequest { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/PushersModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/PushersModule.kt index 784a140b19..ad4e3eee7a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/PushersModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/PushersModule.kt @@ -54,7 +54,7 @@ internal abstract class PushersModule { abstract fun bindConditionResolver(conditionResolver: DefaultConditionResolver): ConditionResolver @Binds - abstract fun bindGetPushersTask(getPusherTask: DefaultGetPusherTask): GetPushersTask + abstract fun bindGetPushersTask(getPushersTask: DefaultGetPushersTask): GetPushersTask @Binds abstract fun bindGetPushRulesTask(getPushRulesTask: DefaultGetPushRulesTask): GetPushRulesTask diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt index 572e03d0d6..95edf3dbaa 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt @@ -127,5 +127,5 @@ internal abstract class RoomModule { abstract fun bindFileService(fileService: DefaultFileService): FileService @Binds - abstract fun bindFetchEditHistoryTask(editHistoryTask: DefaultFetchEditHistoryTask): FetchEditHistoryTask + abstract fun bindFetchEditHistoryTask(fetchEditHistoryTask: DefaultFetchEditHistoryTask): FetchEditHistoryTask } diff --git a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt index 61c461c655..040decbcc2 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt @@ -49,7 +49,7 @@ import javax.inject.Singleton @Singleton interface VectorComponent { - fun inject(vectorApplication: NotificationBroadcastReceiver) + fun inject(notificationBroadcastReceiver: NotificationBroadcastReceiver) fun inject(vectorApplication: VectorApplication) diff --git a/vector/src/main/java/im/vector/riotx/core/di/VectorModule.kt b/vector/src/main/java/im/vector/riotx/core/di/VectorModule.kt index 251fc0b1e9..f70771ef3e 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/VectorModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/VectorModule.kt @@ -62,7 +62,7 @@ abstract class VectorModule { @Provides @JvmStatic - fun providesAuthenticator(matrix: Matrix): Authenticator{ + fun providesAuthenticator(matrix: Matrix): Authenticator { return matrix.authenticator() } } From 0d807505077a8bdc63921c2427b3e0bc7be80da4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 24 Sep 2019 13:43:50 +0200 Subject: [PATCH 021/197] Create interface for UiStateRepository and an implementation with SharedPrefs --- .../im/vector/riotx/core/di/VectorModule.kt | 4 ++ .../ui/SharedPreferencesUiStateRepository.kt | 56 +++++++++++++++++++ .../riotx/features/ui/UiStateRepository.kt | 34 ++--------- 3 files changed, 64 insertions(+), 30 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/ui/SharedPreferencesUiStateRepository.kt diff --git a/vector/src/main/java/im/vector/riotx/core/di/VectorModule.kt b/vector/src/main/java/im/vector/riotx/core/di/VectorModule.kt index f70771ef3e..e4532bada0 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/VectorModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/VectorModule.kt @@ -28,6 +28,8 @@ import im.vector.matrix.android.api.auth.Authenticator import im.vector.matrix.android.api.session.Session import im.vector.riotx.features.navigation.DefaultNavigator import im.vector.riotx.features.navigation.Navigator +import im.vector.riotx.features.ui.SharedPreferencesUiStateRepository +import im.vector.riotx.features.ui.UiStateRepository @Module abstract class VectorModule { @@ -70,5 +72,7 @@ abstract class VectorModule { @Binds abstract fun bindNavigator(navigator: DefaultNavigator): Navigator + @Binds + abstract fun bindUiStateRepository(uiStateRepository: SharedPreferencesUiStateRepository): UiStateRepository } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/ui/SharedPreferencesUiStateRepository.kt b/vector/src/main/java/im/vector/riotx/features/ui/SharedPreferencesUiStateRepository.kt new file mode 100644 index 0000000000..3ca01c21c2 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/ui/SharedPreferencesUiStateRepository.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2019 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.riotx.features.ui + +import android.content.SharedPreferences +import androidx.core.content.edit +import im.vector.riotx.features.home.room.list.RoomListFragment +import javax.inject.Inject + + +/** + * This class is used to persist UI state across application restart + */ +class SharedPreferencesUiStateRepository @Inject constructor(private val sharedPreferences: SharedPreferences) : UiStateRepository { + + override fun getDisplayMode(): RoomListFragment.DisplayMode { + return when (sharedPreferences.getInt(KEY_DISPLAY_MODE, VALUE_DISPLAY_MODE_CATCHUP)) { + VALUE_DISPLAY_MODE_PEOPLE -> RoomListFragment.DisplayMode.PEOPLE + VALUE_DISPLAY_MODE_ROOMS -> RoomListFragment.DisplayMode.ROOMS + else -> RoomListFragment.DisplayMode.HOME + } + } + + override fun storeDisplayMode(displayMode: RoomListFragment.DisplayMode) { + sharedPreferences.edit { + putInt(KEY_DISPLAY_MODE, + when (displayMode) { + RoomListFragment.DisplayMode.PEOPLE -> VALUE_DISPLAY_MODE_PEOPLE + RoomListFragment.DisplayMode.ROOMS -> VALUE_DISPLAY_MODE_ROOMS + else -> VALUE_DISPLAY_MODE_CATCHUP + }) + } + } + + + companion object { + private const val KEY_DISPLAY_MODE = "UI_STATE_DISPLAY_MODE" + private const val VALUE_DISPLAY_MODE_CATCHUP = 0 + private const val VALUE_DISPLAY_MODE_PEOPLE = 1 + private const val VALUE_DISPLAY_MODE_ROOMS = 2 + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/ui/UiStateRepository.kt b/vector/src/main/java/im/vector/riotx/features/ui/UiStateRepository.kt index be8e624100..15f3f3efa2 100644 --- a/vector/src/main/java/im/vector/riotx/features/ui/UiStateRepository.kt +++ b/vector/src/main/java/im/vector/riotx/features/ui/UiStateRepository.kt @@ -16,41 +16,15 @@ package im.vector.riotx.features.ui -import android.content.SharedPreferences -import androidx.core.content.edit import im.vector.riotx.features.home.room.list.RoomListFragment -import javax.inject.Inject /** - * This class is used to persist UI state across application restart + * This interface is used to persist UI state across application restart */ -class UiStateRepository @Inject constructor(private val sharedPreferences: SharedPreferences) { +interface UiStateRepository { - fun getDisplayMode(): RoomListFragment.DisplayMode { - return when (sharedPreferences.getInt(KEY_DISPLAY_MODE, VALUE_DISPLAY_MODE_CATCHUP)) { - VALUE_DISPLAY_MODE_PEOPLE -> RoomListFragment.DisplayMode.PEOPLE - VALUE_DISPLAY_MODE_ROOMS -> RoomListFragment.DisplayMode.ROOMS - else -> RoomListFragment.DisplayMode.HOME - } - } + fun getDisplayMode(): RoomListFragment.DisplayMode - fun storeDisplayMode(displayMode: RoomListFragment.DisplayMode) { - sharedPreferences.edit { - putInt(KEY_DISPLAY_MODE, - when (displayMode) { - RoomListFragment.DisplayMode.PEOPLE -> VALUE_DISPLAY_MODE_PEOPLE - RoomListFragment.DisplayMode.ROOMS -> VALUE_DISPLAY_MODE_ROOMS - else -> VALUE_DISPLAY_MODE_CATCHUP - }) - } - } - - - companion object { - private const val KEY_DISPLAY_MODE = "UI_STATE_DISPLAY_MODE" - private const val VALUE_DISPLAY_MODE_CATCHUP = 0 - private const val VALUE_DISPLAY_MODE_PEOPLE = 1 - private const val VALUE_DISPLAY_MODE_ROOMS = 2 - } + fun storeDisplayMode(displayMode: RoomListFragment.DisplayMode) } \ No newline at end of file From f6373221de6fd03b807e2e26f2b3f91d087164c9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 24 Sep 2019 16:05:08 +0200 Subject: [PATCH 022/197] Dagger cleanup --- vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt index 040decbcc2..10b3730022 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt @@ -65,7 +65,7 @@ interface VectorComponent { fun resources(): Resources - fun dimensionUtils(): DimensionConverter + fun dimensionConverter(): DimensionConverter fun vectorConfiguration(): VectorConfiguration From 63b43de4b8c5fb9f3829c8c9340c6fa965c13ae9 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 24 Sep 2019 22:52:43 +0200 Subject: [PATCH 023/197] Read marker: final refact [WIP] --- .../riotx/core/ui/views/ReadMarkerView.kt | 4 +- .../home/room/detail/ReadMarkerHelper.kt | 48 ++++++++++++++---- .../home/room/detail/RoomDetailFragment.kt | 50 ++++++------------- .../home/room/detail/RoomDetailViewModel.kt | 45 ++++++++--------- .../home/room/detail/RoomDetailViewState.kt | 2 +- .../timeline/TimelineEventController.kt | 27 +++++++--- .../timeline/TimelineLayoutManagerHolder.kt | 29 +++++++++++ .../timeline/factory/DefaultItemFactory.kt | 4 +- .../timeline/factory/EncryptedItemFactory.kt | 4 +- .../factory/MergedHeaderItemFactory.kt | 11 ++-- .../timeline/factory/MessageItemFactory.kt | 6 +-- .../timeline/factory/NoticeItemFactory.kt | 4 +- .../timeline/factory/TimelineItemFactory.kt | 14 +++--- .../helper/MessageInformationDataFactory.kt | 6 +-- .../detail/timeline/item/AbsMessageItem.kt | 4 +- .../detail/timeline/item/MergedHeaderItem.kt | 4 +- .../room/detail/timeline/item/NoticeItem.kt | 5 +- 17 files changed, 152 insertions(+), 115 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineLayoutManagerHolder.kt diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt index 55665ca27f..19dad458a5 100644 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt @@ -37,7 +37,7 @@ class ReadMarkerView @JvmOverloads constructor( ) : View(context, attrs, defStyleAttr) { interface Callback { - fun onReadMarkerLongBound() + fun onReadMarkerLongBound(isDisplayed: Boolean) } private var eventId: String? = null @@ -57,7 +57,7 @@ class ReadMarkerView @JvmOverloads constructor( if (hasReadMarker) { callbackDispatcherJob = GlobalScope.launch(Dispatchers.Main) { delay(DELAY_IN_MS) - callback?.onReadMarkerLongBound() + callback?.onReadMarkerLongBound(displayReadMarker) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt index c162098cff..85ad6201d3 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt @@ -30,10 +30,25 @@ class ReadMarkerHelper @Inject constructor() { lateinit var layoutManager: LinearLayoutManager var callback: Callback? = null + private var onReadMarkerLongDisplayed = false + private var readMarkerVisible: Boolean = true private var state: RoomDetailViewState? = null - fun updateState(state: RoomDetailViewState) { - this.state = state + fun readMarkerVisible(): Boolean { + return readMarkerVisible + } + + fun onResume() { + onReadMarkerLongDisplayed = false + } + + fun onReadMarkerLongDisplayed() { + onReadMarkerLongDisplayed = true + } + + fun updateWith(newState: RoomDetailViewState) { + state = newState + checkReadMarkerVisibility() checkJumpToReadMarkerVisibility() } @@ -41,34 +56,47 @@ class ReadMarkerHelper @Inject constructor() { checkJumpToReadMarkerVisibility() } + private fun checkReadMarkerVisibility() { + val nonNullState = this.state ?: return + val firstVisibleItem = layoutManager.findFirstVisibleItemPosition() + val lastVisibleItem = layoutManager.findLastVisibleItemPosition() + readMarkerVisible = if (!onReadMarkerLongDisplayed) { + true + } else { + if (nonNullState.timeline?.isLive == false) { + true + } else { + !(firstVisibleItem == 0 && lastVisibleItem > 0) + } + } + } + private fun checkJumpToReadMarkerVisibility() { val nonNullState = this.state ?: return val lastVisibleItem = layoutManager.findLastVisibleItemPosition() val readMarkerId = nonNullState.asyncRoomSummary()?.readMarkerId if (readMarkerId == null) { - callback?.onVisibilityUpdated(false, null) + callback?.onJumpToReadMarkerVisibilityUpdate(false, null) } val positionOfReadMarker = timelineEventController.searchPositionOfEvent(readMarkerId) - Timber.v("Position of readMarker: $positionOfReadMarker") - Timber.v("Position of lastVisibleItem: $lastVisibleItem") if (positionOfReadMarker == null) { if (nonNullState.timeline?.isLive == true && lastVisibleItem > 0) { - callback?.onVisibilityUpdated(true, readMarkerId) + callback?.onJumpToReadMarkerVisibilityUpdate(true, readMarkerId) } else { - callback?.onVisibilityUpdated(false, readMarkerId) + callback?.onJumpToReadMarkerVisibilityUpdate(false, readMarkerId) } } else { if (positionOfReadMarker > lastVisibleItem) { - callback?.onVisibilityUpdated(true, readMarkerId) + callback?.onJumpToReadMarkerVisibilityUpdate(true, readMarkerId) } else { - callback?.onVisibilityUpdated(false, readMarkerId) + callback?.onJumpToReadMarkerVisibilityUpdate(false, readMarkerId) } } } interface Callback { - fun onVisibilityUpdated(show: Boolean, readMarkerId: String?) + fun onJumpToReadMarkerVisibilityUpdate(show: Boolean, readMarkerId: String?) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index a94e1941a1..aadfbb9fcb 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -28,12 +28,7 @@ import android.os.Parcelable import android.text.Editable import android.text.Spannable import android.text.TextUtils -import android.view.HapticFeedbackConstants -import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuItem -import android.view.View -import android.view.Window +import android.view.* import android.view.inputmethod.InputMethodManager import android.widget.TextView import android.widget.Toast @@ -51,13 +46,7 @@ import androidx.recyclerview.widget.RecyclerView import butterknife.BindView import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyVisibilityTracker -import com.airbnb.mvrx.Async -import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.Success -import com.airbnb.mvrx.args -import com.airbnb.mvrx.fragmentViewModel -import com.airbnb.mvrx.withState +import com.airbnb.mvrx.* import com.github.piasy.biv.BigImageViewer import com.github.piasy.biv.loader.ImageLoader import com.google.android.material.snackbar.Snackbar @@ -71,13 +60,7 @@ import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.model.Membership -import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent -import im.vector.matrix.android.api.session.room.model.message.MessageContent -import im.vector.matrix.android.api.session.room.model.message.MessageFileContent -import im.vector.matrix.android.api.session.room.model.message.MessageImageContent -import im.vector.matrix.android.api.session.room.model.message.MessageTextContent -import im.vector.matrix.android.api.session.room.model.message.MessageType -import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent +import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent @@ -124,17 +107,8 @@ import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController -import im.vector.riotx.features.home.room.detail.timeline.action.ActionsHandler -import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet -import im.vector.riotx.features.home.room.detail.timeline.action.SimpleAction -import im.vector.riotx.features.home.room.detail.timeline.action.ViewEditHistoryBottomSheet -import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionBottomSheet -import im.vector.riotx.features.home.room.detail.timeline.item.AbsMessageItem -import im.vector.riotx.features.home.room.detail.timeline.item.MessageFileItem -import im.vector.riotx.features.home.room.detail.timeline.item.MessageImageVideoItem -import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData -import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem -import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData +import im.vector.riotx.features.home.room.detail.timeline.action.* +import im.vector.riotx.features.home.room.detail.timeline.item.* import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.html.PillImageSpan import im.vector.riotx.features.invite.VectorInviteView @@ -432,8 +406,8 @@ class RoomDetailFragment : } override fun onResume() { + readMarkerHelper.onResume() super.onResume() - notificationDrawerManager.setCurrentRoom(roomDetailArgs.roomId) } @@ -483,7 +457,7 @@ class RoomDetailFragment : readMarkerHelper.timelineEventController = timelineEventController readMarkerHelper.layoutManager = layoutManager readMarkerHelper.callback = object : ReadMarkerHelper.Callback { - override fun onVisibilityUpdated(show: Boolean, readMarkerId: String?) { + override fun onJumpToReadMarkerVisibilityUpdate(show: Boolean, readMarkerId: String?) { jumpToReadMarkerView.render(show, readMarkerId) } } @@ -707,13 +681,13 @@ class RoomDetailFragment : } private fun renderState(state: RoomDetailViewState) { - readMarkerHelper.updateState(state) + readMarkerHelper.updateWith(state) renderRoomSummary(state) val summary = state.asyncRoomSummary() val inviter = state.asyncInviter() if (summary?.membership == Membership.JOIN) { scrollOnHighlightedEventCallback.timeline = state.timeline - timelineEventController.update(state) + timelineEventController.update(state, readMarkerHelper.readMarkerVisible()) inviteView.visibility = View.GONE val uid = session.myUserId val meMember = session.getRoom(state.roomId)?.getRoomMember(uid) @@ -974,7 +948,10 @@ class RoomDetailFragment : .show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS") } - override fun onReadMarkerLongDisplayed() = withState(roomDetailViewModel) { state -> + override fun onReadMarkerLongBound(isDisplayed: Boolean) { + if (isDisplayed) { + readMarkerHelper.onReadMarkerLongDisplayed() + } val firstVisibleItem = layoutManager.findFirstVisibleItemPosition() val nextReadMarkerId = timelineEventController.searchEventIdAtPosition(firstVisibleItem) if (nextReadMarkerId != null) { @@ -982,6 +959,7 @@ class RoomDetailFragment : } } + // AutocompleteUserPresenter.Callback override fun onQueryUsers(query: CharSequence?) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 22850a96f0..72c6d67a7d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -21,8 +21,6 @@ import android.text.TextUtils import androidx.annotation.IdRes import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import arrow.core.Option -import arrow.core.getOrElse import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success @@ -43,13 +41,11 @@ import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.getFileUrl -import im.vector.matrix.android.api.session.room.model.relation.ReactionContent import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent import im.vector.matrix.android.api.session.room.send.UserDraft -import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineSettings -import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent +import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.matrix.rx.rx @@ -62,11 +58,11 @@ import im.vector.riotx.core.utils.LiveEvent import im.vector.riotx.core.utils.subscribeLogError import im.vector.riotx.features.command.CommandParser import im.vector.riotx.features.command.ParsedCommand +import im.vector.riotx.features.home.room.detail.timeline.TimelineLayoutManagerHolder import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents import im.vector.riotx.features.settings.VectorPreferences import io.reactivex.Observable import io.reactivex.functions.BiFunction -import io.reactivex.functions.Function3 import io.reactivex.rxkotlin.subscribeBy import org.commonmark.parser.Parser import org.commonmark.renderer.html.HtmlRenderer @@ -76,6 +72,7 @@ import java.util.concurrent.TimeUnit class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: RoomDetailViewState, + private val timelineLayoutManagerHolder: TimelineLayoutManagerHolder, private val userPreferencesProvider: UserPreferencesProvider, private val vectorPreferences: VectorPreferences, private val session: Session @@ -119,8 +116,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro observeRoomSummary() observeEventDisplayedActions() observeSummaryState() - observeReadMarkerVisibility() observeDrafts() + observeReadMarkerVisibility() + observeOwnState() room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear() timeline.start() setState { copy(timeline = this@RoomDetailViewModel.timeline) } @@ -711,23 +709,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } - private fun observeReadMarkerVisibility() { - Observable - .combineLatest( - room.rx().liveReadMarker(), - room.rx().liveReadReceipt(), - BiFunction, Optional, Boolean> { readMarker, readReceipt -> - readMarker.getOrNull() == readReceipt.getOrNull() - } - ) - .startWith(false) - .subscribe { - setState { copy(hideReadMarker = it) } - } - .disposeOnClear() - } - - private fun observeSummaryState() { asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary -> if (summary.membership == Membership.INVITE) { @@ -743,6 +724,22 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } + private fun observeReadMarkerVisibility() { + Observable + .combineLatest( + room.rx().liveReadMarker(), + room.rx().liveReadReceipt(), + BiFunction, Optional, Boolean> { readMarker, readReceipt -> + readMarker.getOrNull() != readReceipt.getOrNull() + } + ) + .subscribe { + setState { copy(readMarkerVisible = it) } + } + .disposeOnClear() + } + + override fun onCleared() { timeline.dispose() super.onCleared() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt index 2be78506a0..2609aed2e3 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt @@ -53,7 +53,7 @@ data class RoomDetailViewState( val tombstoneEventHandling: Async = Uninitialized, val syncState: SyncState = SyncState.IDLE, val highlightedEventId: String? = null, - val hideReadMarker: Boolean = false + val readMarkerVisible: Boolean = false ) : MvRxState { constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index fdc37a7f35..525fb6cd6a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -34,7 +34,10 @@ import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.features.home.room.detail.RoomDetailViewState import im.vector.riotx.features.home.room.detail.timeline.factory.MergedHeaderItemFactory import im.vector.riotx.features.home.room.detail.timeline.factory.TimelineItemFactory -import im.vector.riotx.features.home.room.detail.timeline.helper.* +import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback +import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener +import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider +import im.vector.riotx.features.home.room.detail.timeline.helper.nextOrNull import im.vector.riotx.features.home.room.detail.timeline.item.* import im.vector.riotx.features.media.ImageContentRenderer import im.vector.riotx.features.media.VideoContentRenderer @@ -79,7 +82,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec interface ReadReceiptsCallback { fun onReadReceiptsClicked(readReceipts: List) - fun onReadMarkerLongDisplayed() + fun onReadMarkerLongBound(isDisplayed: Boolean) } interface UrlClickCallback { @@ -141,7 +144,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec requestModelBuild() } - fun update(viewState: RoomDetailViewState) { + fun update(viewState: RoomDetailViewState, readMarkerVisible: Boolean) { if (timeline != viewState.timeline) { timeline = viewState.timeline timeline?.listener = this @@ -166,8 +169,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec eventIdToHighlight = viewState.highlightedEventId requestModelBuild = true } - if (hideReadMarker != viewState.hideReadMarker) { - hideReadMarker = viewState.hideReadMarker + if (this.readMarkerVisible != readMarkerVisible) { + this.readMarkerVisible = readMarkerVisible requestModelBuild = true } if (requestModelBuild) { @@ -175,7 +178,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } } - private var hideReadMarker: Boolean = false + private var readMarkerVisible: Boolean = false private var eventIdToHighlight: String? = null override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { @@ -255,11 +258,19 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec val date = event.root.localDateTime() val nextDate = nextEvent?.root?.localDateTime() val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate() - val eventModel = timelineItemFactory.create(event, nextEvent, eventIdToHighlight, hideReadMarker, callback).also { + val eventModel = timelineItemFactory.create(event, nextEvent, eventIdToHighlight, readMarkerVisible, callback).also { it.id(event.localId) it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) } - val mergedHeaderModel = mergedHeaderItemFactory.create(event, nextEvent, items, addDaySeparator, currentPosition, eventIdToHighlight, callback) { + val mergedHeaderModel = mergedHeaderItemFactory.create(event, + nextEvent = nextEvent, + items = items, + addDaySeparator = addDaySeparator, + readMarkerVisible = readMarkerVisible, + currentPosition = currentPosition, + eventIdToHighlight = eventIdToHighlight, + callback = callback + ) { requestModelBuild() } val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineLayoutManagerHolder.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineLayoutManagerHolder.kt new file mode 100644 index 0000000000..429515798a --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineLayoutManagerHolder.kt @@ -0,0 +1,29 @@ +/* + + * Copyright 2019 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.riotx.features.home.room.detail.timeline + +import androidx.recyclerview.widget.LinearLayoutManager +import im.vector.riotx.core.di.ScreenScope +import javax.inject.Inject + +@ScreenScope +class TimelineLayoutManagerHolder @Inject constructor() { + + lateinit var layoutManager: LinearLayoutManager + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt index a387f3f496..959079bf8b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt @@ -31,7 +31,7 @@ class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: Ava fun create(event: TimelineEvent, highlight: Boolean, - hideReadMarker: Boolean, + readMarkerVisible: Boolean, callback: TimelineEventController.Callback?, exception: Exception? = null): DefaultItem? { val text = if (exception == null) { @@ -40,7 +40,7 @@ class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: Ava "an exception occurred when rendering the event ${event.root.eventId}" } - val informationData = informationDataFactory.create(event, null, hideReadMarker) + val informationData = informationDataFactory.create(event, null, readMarkerVisible) return DefaultItem_() .leftGuideline(avatarSizeProvider.leftGuideline) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt index 663762850a..e67507d7bb 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt @@ -41,7 +41,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat fun create(event: TimelineEvent, nextEvent: TimelineEvent?, highlight: Boolean, - hideReadMarker: Boolean, + readMarkerVisible: Boolean, callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? { event.root.eventId ?: return null @@ -65,7 +65,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat // TODO This is not correct format for error, change it - val informationData = messageInformationDataFactory.create(event, nextEvent, hideReadMarker) + val informationData = messageInformationDataFactory.create(event, nextEvent, readMarkerVisible) val attributes = attributesFactory.create(null, informationData, callback) return MessageTextItem_() .leftGuideline(avatarSizeProvider.leftGuideline) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index 80b3aa261b..ddf93410a1 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -18,15 +18,9 @@ package im.vector.riotx.features.home.room.detail.timeline.factory import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotx.core.di.ActiveSessionHolder -import im.vector.riotx.core.extensions.displayReadMarker import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController -import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider -import im.vector.riotx.features.home.room.detail.timeline.helper.MergedTimelineEventVisibilityStateChangedListener -import im.vector.riotx.features.home.room.detail.timeline.helper.canBeMerged -import im.vector.riotx.features.home.room.detail.timeline.helper.prevSameTypeEvents -import im.vector.riotx.features.home.room.detail.timeline.helper.senderAvatar -import im.vector.riotx.features.home.room.detail.timeline.helper.senderName +import im.vector.riotx.features.home.room.detail.timeline.helper.* import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem_ import javax.inject.Inject @@ -42,6 +36,7 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act nextEvent: TimelineEvent?, items: List, addDaySeparator: Boolean, + readMarkerVisible: Boolean, currentPosition: Int, eventIdToHighlight: String?, callback: TimelineEventController.Callback?, @@ -67,7 +62,7 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act if (readMarkerId == null && mergedEvent.hasReadMarker) { readMarkerId = mergedEvent.root.eventId } - if (!showReadMarker && mergedEvent.displayReadMarker(sessionHolder.getActiveSession().myUserId)) { + if (!showReadMarker && mergedEvent.hasReadMarker && readMarkerVisible) { showReadMarker = true } val senderAvatar = mergedEvent.senderAvatar() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 2cf5a60c44..747ae483c9 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -76,12 +76,12 @@ class MessageItemFactory @Inject constructor( fun create(event: TimelineEvent, nextEvent: TimelineEvent?, highlight: Boolean, - hideReadMarker: Boolean, + readMarkerVisible: Boolean, callback: TimelineEventController.Callback? ): VectorEpoxyModel<*>? { event.root.eventId ?: return null - val informationData = messageInformationDataFactory.create(event, nextEvent, hideReadMarker) + val informationData = messageInformationDataFactory.create(event, nextEvent, readMarkerVisible) if (event.root.isRedacted()) { //message is redacted @@ -98,7 +98,7 @@ class MessageItemFactory @Inject constructor( || event.isEncrypted() && event.root.content.toModel()?.relatesTo?.type == RelationType.REPLACE ) { // This is an edit event, we should it when debugging as a notice event - return noticeItemFactory.create(event, highlight, hideReadMarker, callback) + return noticeItemFactory.create(event, highlight, readMarkerVisible, callback) } val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt index f251a70905..ff7af61fbe 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt @@ -34,11 +34,11 @@ class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEv fun create(event: TimelineEvent, highlight: Boolean, - hideReadMarker: Boolean, + readMarkerVisible: Boolean, callback: TimelineEventController.Callback?): NoticeItem? { val formattedText = eventFormatter.format(event) ?: return null - val informationData = informationDataFactory.create(event, null, hideReadMarker) + val informationData = informationDataFactory.create(event, null, readMarkerVisible) val attributes = NoticeItem.Attributes( avatarRenderer = avatarRenderer, informationData = informationData, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 18254120af..eda00fff95 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -33,14 +33,14 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me fun create(event: TimelineEvent, nextEvent: TimelineEvent?, eventIdToHighlight: String?, - hideReadMarker: Boolean, + readMarkerVisible: Boolean, callback: TimelineEventController.Callback?): VectorEpoxyModel<*> { val highlight = event.root.eventId == eventIdToHighlight val computedModel = try { when (event.root.getClearType()) { - EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, hideReadMarker, callback) + EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, readMarkerVisible, callback) // State and call EventType.STATE_ROOM_TOMBSTONE, EventType.STATE_ROOM_NAME, @@ -52,22 +52,22 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.CALL_ANSWER, EventType.REACTION, EventType.REDACTION, - EventType.ENCRYPTION -> noticeItemFactory.create(event, highlight, hideReadMarker, callback) + EventType.ENCRYPTION -> noticeItemFactory.create(event, highlight, readMarkerVisible, callback) // State room create EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback) // Crypto EventType.ENCRYPTED -> { if (event.root.isRedacted()) { // Redacted event, let the MessageItemFactory handle it - messageItemFactory.create(event, nextEvent, highlight, hideReadMarker, callback) + messageItemFactory.create(event, nextEvent, highlight, readMarkerVisible, callback) } else { - encryptedItemFactory.create(event, nextEvent, highlight, hideReadMarker, callback) + encryptedItemFactory.create(event, nextEvent, highlight, readMarkerVisible, callback) } } // Unhandled event types (yet) EventType.STATE_ROOM_THIRD_PARTY_INVITE, - EventType.STICKER -> defaultItemFactory.create(event, highlight, hideReadMarker, callback) + EventType.STICKER -> defaultItemFactory.create(event, highlight, readMarkerVisible, callback) else -> { Timber.v("Type ${event.root.getClearType()} not handled") null @@ -75,7 +75,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me } } catch (e: Exception) { Timber.e(e, "failed to create message item") - defaultItemFactory.create(event, highlight, hideReadMarker, callback, e) + defaultItemFactory.create(event, highlight, readMarkerVisible, callback, e) } return (computedModel ?: EmptyItem_()) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index 453f7e4cd9..8448ddc059 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -24,10 +24,8 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.core.resources.ColorProvider -import im.vector.riotx.core.utils.isSingleEmoji import im.vector.riotx.features.home.getColorFromUserId import im.vector.riotx.core.date.VectorDateFormatter -import im.vector.riotx.core.extensions.displayReadMarker import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.home.room.detail.timeline.item.ReactionInfoData import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData @@ -41,7 +39,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses private val dateFormatter: VectorDateFormatter, private val colorProvider: ColorProvider) { - fun create(event: TimelineEvent, nextEvent: TimelineEvent?, hideReadMarker: Boolean): MessageInformationData { + fun create(event: TimelineEvent, nextEvent: TimelineEvent?, readMarkerVisible: Boolean): MessageInformationData { // Non nullability has been tested before val eventId = event.root.eventId!! @@ -65,7 +63,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId ?: "")) } - val displayReadMarker = !hideReadMarker && event.displayReadMarker(session.myUserId) + val displayReadMarker = readMarkerVisible && event.hasReadMarker return MessageInformationData( eventId = eventId, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt index 408a997efd..5bf8ba6e06 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -57,8 +57,8 @@ abstract class AbsMessageItem : BaseEventItem() { private val _readMarkerCallback = object : ReadMarkerView.Callback { - override fun onReadMarkerLongBound() { - attributes.readReceiptsCallback?.onReadMarkerLongDisplayed() + override fun onReadMarkerLongBound(isDisplayed: Boolean) { + attributes.readReceiptsCallback?.onReadMarkerLongBound(isDisplayed) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt index de105b2261..da19a88133 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt @@ -41,8 +41,8 @@ abstract class MergedHeaderItem : BaseEventItem() { private val _readMarkerCallback = object : ReadMarkerView.Callback { - override fun onReadMarkerLongBound() { - attributes.readReceiptsCallback?.onReadMarkerLongDisplayed() + override fun onReadMarkerLongBound(isDisplayed: Boolean) { + attributes.readReceiptsCallback?.onReadMarkerLongBound(isDisplayed) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt index 89270ce026..559b02aa61 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt @@ -38,8 +38,9 @@ abstract class NoticeItem : BaseEventItem() { }) private val _readMarkerCallback = object : ReadMarkerView.Callback { - override fun onReadMarkerLongBound() { - attributes.readReceiptsCallback?.onReadMarkerLongDisplayed() + + override fun onReadMarkerLongBound(isDisplayed: Boolean) { + attributes.readReceiptsCallback?.onReadMarkerLongBound(isDisplayed) } } From b24a37226263ecf10d0450b13814483ab69966eb Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 25 Sep 2019 10:50:13 +0200 Subject: [PATCH 024/197] Show "Clear message queue" option (in debug mode) --- .../home/room/detail/RoomDetailViewModel.kt | 20 ++++++++----------- vector/src/main/res/menu/menu_timeline.xml | 2 +- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 80f333a76e..ac56114319 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -48,6 +48,7 @@ import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.matrix.rx.rx +import im.vector.riotx.BuildConfig import im.vector.riotx.R import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.intent.getFilenameFromUri @@ -232,18 +233,13 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro get() = _downloadedFileEvent - fun isMenuItemVisible(@IdRes itemId: Int): Boolean { - if (itemId == R.id.clear_message_queue) { - //For now always disable, woker cancellation is not working properly - return false//timeline.pendingEventCount() > 0 - } - if (itemId == R.id.resend_all) { - return timeline.failedToDeliverEventCount() > 0 - } - if (itemId == R.id.clear_all) { - return timeline.failedToDeliverEventCount() > 0 - } - return false + fun isMenuItemVisible(@IdRes itemId: Int) = when (itemId) { + R.id.clear_message_queue -> + /* For now always disable on production, worker cancellation is not working properly */ + timeline.pendingEventCount() > 0 && BuildConfig.DEBUG + R.id.resend_all -> timeline.failedToDeliverEventCount() > 0 + R.id.clear_all -> timeline.failedToDeliverEventCount() > 0 + else -> false } // PRIVATE METHODS ***************************************************************************** diff --git a/vector/src/main/res/menu/menu_timeline.xml b/vector/src/main/res/menu/menu_timeline.xml index 824735406f..7eea0e2582 100644 --- a/vector/src/main/res/menu/menu_timeline.xml +++ b/vector/src/main/res/menu/menu_timeline.xml @@ -22,7 +22,7 @@ From 9b91b6ea87effe986e7539592661b18948514848 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 25 Sep 2019 10:56:18 +0200 Subject: [PATCH 025/197] Create Extension to convert a Response to a Failure --- .../matrix/android/internal/network/Request.kt | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt index ede9e823bf..3420717199 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt @@ -23,9 +23,9 @@ import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.internal.di.MoshiProvider import kotlinx.coroutines.CancellationException -import okhttp3.ResponseBody import org.greenrobot.eventbus.EventBus import retrofit2.Call +import retrofit2.Response import timber.log.Timber import java.io.IOException @@ -43,7 +43,7 @@ internal class Request { response.body() ?: throw IllegalStateException("The request returned a null body") } else { - throw manageFailure(response.errorBody(), response.code()) + throw response.toFailure() } } catch (exception: Throwable) { throw when (exception) { @@ -56,10 +56,8 @@ internal class Request { } } - private fun manageFailure(errorBody: ResponseBody?, httpCode: Int): Throwable { - if (errorBody == null) { - return RuntimeException("Error body should not be null") - } + private fun Response.toFailure(): Failure { + val errorBody = errorBody() ?: return Failure.Unknown(RuntimeException("errorBody() should not be null")) val errorBodyStr = errorBody.string() @@ -74,13 +72,13 @@ internal class Request { EventBus.getDefault().post(ConsentNotGivenError(matrixError.consentUri)) } - return Failure.ServerError(matrixError, httpCode) + return Failure.ServerError(matrixError, code()) } } catch (ex: JsonDataException) { // This is not a MatrixError Timber.w("The error returned by the server is not a MatrixError") } - return Failure.OtherServerError(errorBodyStr, httpCode) + return Failure.OtherServerError(errorBodyStr, code()) } } \ No newline at end of file From ae8bceacba1f1b6076e3fc7fb1cd7e0da7ece3b1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 25 Sep 2019 10:58:55 +0200 Subject: [PATCH 026/197] Create Extension to convert a Response to a Failure -> expose to other object --- .../android/internal/network/Request.kt | 35 ------------ .../internal/network/RetrofitExtensions.kt | 54 ++++++++++++++++++- 2 files changed, 52 insertions(+), 37 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt index 3420717199..a333a02c67 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt @@ -16,24 +16,15 @@ package im.vector.matrix.android.internal.network -import com.squareup.moshi.JsonDataException -import com.squareup.moshi.Moshi -import im.vector.matrix.android.api.failure.ConsentNotGivenError import im.vector.matrix.android.api.failure.Failure -import im.vector.matrix.android.api.failure.MatrixError -import im.vector.matrix.android.internal.di.MoshiProvider import kotlinx.coroutines.CancellationException -import org.greenrobot.eventbus.EventBus import retrofit2.Call -import retrofit2.Response -import timber.log.Timber import java.io.IOException internal suspend inline fun executeRequest(block: Request.() -> Unit) = Request().apply(block).execute() internal class Request { - private val moshi: Moshi = MoshiProvider.providesMoshi() lateinit var apiCall: Call suspend fun execute(): DATA { @@ -55,30 +46,4 @@ internal class Request { } } } - - private fun Response.toFailure(): Failure { - val errorBody = errorBody() ?: return Failure.Unknown(RuntimeException("errorBody() should not be null")) - - val errorBodyStr = errorBody.string() - - val matrixErrorAdapter = moshi.adapter(MatrixError::class.java) - - try { - val matrixError = matrixErrorAdapter.fromJson(errorBodyStr) - - if (matrixError != null) { - if (matrixError.code == MatrixError.M_CONSENT_NOT_GIVEN && !matrixError.consentUri.isNullOrBlank()) { - // Also send this error to the bus, for a global management - EventBus.getDefault().post(ConsentNotGivenError(matrixError.consentUri)) - } - - return Failure.ServerError(matrixError, code()) - } - } catch (ex: JsonDataException) { - // This is not a MatrixError - Timber.w("The error returned by the server is not a MatrixError") - } - - return Failure.OtherServerError(errorBodyStr, code()) - } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt index 824d74b30e..64fcb08bac 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt @@ -18,14 +18,22 @@ package im.vector.matrix.android.internal.network +import com.squareup.moshi.JsonDataException +import im.vector.matrix.android.api.failure.ConsentNotGivenError +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.failure.MatrixError +import im.vector.matrix.android.internal.di.MoshiProvider import kotlinx.coroutines.suspendCancellableCoroutine +import okhttp3.ResponseBody +import org.greenrobot.eventbus.EventBus import retrofit2.Call import retrofit2.Callback import retrofit2.Response +import timber.log.Timber import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException -suspend fun Call.awaitResponse(): Response { +internal suspend fun Call.awaitResponse(): Response { return suspendCancellableCoroutine { continuation -> continuation.invokeOnCancellation { cancel() @@ -40,4 +48,46 @@ suspend fun Call.awaitResponse(): Response { } }) } -} \ No newline at end of file +} + +/** + * Convert a retrofit Response to a Failure, and eventually parse errorBody to convert it to a MatrixError + */ +internal fun Response.toFailure(): Failure { + return toFailure(errorBody(), code()) +} + +/** + * Convert a okhttp3 Response to a Failure, and eventually parse errorBody to convert it to a MatrixError + */ +internal fun okhttp3.Response.toFailure(): Failure { + return toFailure(body(), code()) +} + +private fun toFailure(errorBody: ResponseBody?, httpCode: Int): Failure { + if (errorBody == null) { + return Failure.Unknown(RuntimeException("errorBody should not be null")) + } + + val errorBodyStr = errorBody.string() + + val matrixErrorAdapter = MoshiProvider.providesMoshi().adapter(MatrixError::class.java) + + try { + val matrixError = matrixErrorAdapter.fromJson(errorBodyStr) + + if (matrixError != null) { + if (matrixError.code == MatrixError.M_CONSENT_NOT_GIVEN && !matrixError.consentUri.isNullOrBlank()) { + // Also send this error to the bus, for a global management + EventBus.getDefault().post(ConsentNotGivenError(matrixError.consentUri)) + } + + return Failure.ServerError(matrixError, httpCode) + } + } catch (ex: JsonDataException) { + // This is not a MatrixError + Timber.w("The error returned by the server is not a MatrixError") + } + + return Failure.OtherServerError(errorBodyStr, httpCode) +} From 4c04014e4d998429812289bbc6ecf5f4bf37bc92 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 25 Sep 2019 11:26:49 +0200 Subject: [PATCH 027/197] Do not log big data request (ex: file upload) --- .../interceptors/CurlLoggingInterceptor.kt | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/matrix-sdk-android/src/debug/java/im/vector/matrix/android/internal/network/interceptors/CurlLoggingInterceptor.kt b/matrix-sdk-android/src/debug/java/im/vector/matrix/android/internal/network/interceptors/CurlLoggingInterceptor.kt index 3d499be3c1..5863edd154 100644 --- a/matrix-sdk-android/src/debug/java/im/vector/matrix/android/internal/network/interceptors/CurlLoggingInterceptor.kt +++ b/matrix-sdk-android/src/debug/java/im/vector/matrix/android/internal/network/interceptors/CurlLoggingInterceptor.kt @@ -22,6 +22,7 @@ import okhttp3.Interceptor import okhttp3.Response import okhttp3.logging.HttpLoggingInterceptor import okio.Buffer +import timber.log.Timber import java.io.IOException import java.nio.charset.Charset import javax.inject.Inject @@ -58,15 +59,21 @@ internal class CurlLoggingInterceptor @Inject constructor(private val logger: Ht val requestBody = request.body() if (requestBody != null) { - val buffer = Buffer() - requestBody.writeTo(buffer) - var charset: Charset? = UTF8 - val contentType = requestBody.contentType() - if (contentType != null) { - charset = contentType.charset(UTF8) + if (requestBody.contentLength() > 100_000) { + Timber.w("Unable to log curl command data, size is too big (${requestBody.contentLength()})") + // Ensure the curl command will failed + curlCmd += "DATA IS TOO BIG" + } else { + val buffer = Buffer() + requestBody.writeTo(buffer) + var charset: Charset? = UTF8 + val contentType = requestBody.contentType() + if (contentType != null) { + charset = contentType.charset(UTF8) + } + // try to keep to a single line and use a subshell to preserve any line breaks + curlCmd += " --data $'" + buffer.readString(charset!!).replace("\n", "\\n") + "'" } - // try to keep to a single line and use a subshell to preserve any line breaks - curlCmd += " --data $'" + buffer.readString(charset!!).replace("\n", "\\n") + "'" } val headers = request.headers() From f3039601bf135c27b5b74016a3bdd4252a53bb5e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 25 Sep 2019 11:27:23 +0200 Subject: [PATCH 028/197] throw Failure instead of meaning less IOException --- .../matrix/android/internal/session/content/FileUploader.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt index 2ec17248d1..15d75ceeb6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt @@ -22,6 +22,7 @@ import com.squareup.moshi.Moshi import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.internal.di.Authenticated import im.vector.matrix.android.internal.network.ProgressRequestBody +import im.vector.matrix.android.internal.network.toFailure import okhttp3.* import java.io.File import java.io.IOException @@ -74,7 +75,7 @@ internal class FileUploader @Inject constructor(@Authenticated return Try { okHttpClient.newCall(request).execute().use { response -> if (!response.isSuccessful) { - throw IOException() + throw response.toFailure() } else { response.body()?.source()?.let { responseAdapter.fromJson(it) From f077cc846763689dc3b61331b954589be639928e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 25 Sep 2019 14:09:26 +0200 Subject: [PATCH 029/197] Stop sending media in an infinite loop in case of error (part of #587) Not sure how this commit fix it, but the issue is not observed anymore with it --- .../attachments/MXEncryptedAttachments.kt | 19 +---- .../internal/network/RetrofitExtensions.kt | 19 +++++ .../internal/session/content/FileUploader.kt | 43 +++++----- .../session/content/UploadContentWorker.kt | 79 +++++++++---------- 4 files changed, 77 insertions(+), 83 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt index c699325562..95ff11d595 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.internal.crypto.attachments import android.util.Base64 -import arrow.core.Try import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileKey import timber.log.Timber @@ -50,7 +49,7 @@ object MXEncryptedAttachments { * @param mimetype the mime type * @return the encryption file info */ - fun encryptAttachment(attachmentStream: InputStream, mimetype: String): Try { + fun encryptAttachment(attachmentStream: InputStream, mimetype: String): EncryptionResult { val t0 = System.currentTimeMillis() val secureRandom = SecureRandom() @@ -70,7 +69,7 @@ object MXEncryptedAttachments { val outStream = ByteArrayOutputStream() - try { + outStream.use { val encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM) val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM) val ivParameterSpec = IvParameterSpec(initVectorBytes) @@ -114,19 +113,7 @@ object MXEncryptedAttachments { ) Timber.v("Encrypt in ${System.currentTimeMillis() - t0} ms") - return Try.just(result) - } catch (oom: OutOfMemoryError) { - Timber.e(oom, "## encryptAttachment failed") - return Try.Failure(oom) - } catch (e: Exception) { - Timber.e(e, "## encryptAttachment failed") - return Try.Failure(e) - } finally { - try { - outStream.close() - } catch (e: Exception) { - Timber.e(e, "## encryptAttachment() : fail to close outStream") - } + return result } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt index 64fcb08bac..2bdcd9a2fb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt @@ -30,6 +30,7 @@ import retrofit2.Call import retrofit2.Callback import retrofit2.Response import timber.log.Timber +import java.io.IOException import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException @@ -50,6 +51,24 @@ internal suspend fun Call.awaitResponse(): Response { } } +internal suspend fun okhttp3.Call.awaitResponse(): okhttp3.Response { + return suspendCancellableCoroutine { continuation -> + continuation.invokeOnCancellation { + cancel() + } + + enqueue(object : okhttp3.Callback { + override fun onResponse(call: okhttp3.Call, response: okhttp3.Response) { + continuation.resume(response) + } + + override fun onFailure(call: okhttp3.Call, e: IOException) { + continuation.resumeWithException(e) + } + }) + } +} + /** * Convert a retrofit Response to a Failure, and eventually parse errorBody to convert it to a MatrixError */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt index 15d75ceeb6..2f99199736 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt @@ -16,12 +16,11 @@ package im.vector.matrix.android.internal.session.content -import arrow.core.Try -import arrow.core.Try.Companion.raise import com.squareup.moshi.Moshi import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.internal.di.Authenticated import im.vector.matrix.android.internal.network.ProgressRequestBody +import im.vector.matrix.android.internal.network.awaitResponse import im.vector.matrix.android.internal.network.toFailure import okhttp3.* import java.io.File @@ -38,28 +37,26 @@ internal class FileUploader @Inject constructor(@Authenticated private val responseAdapter = moshi.adapter(ContentUploadResponse::class.java) - fun uploadFile(file: File, - filename: String?, - mimeType: String, - progressListener: ProgressRequestBody.Listener? = null): Try { - + suspend fun uploadFile(file: File, + filename: String?, + mimeType: String, + progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse { val uploadBody = RequestBody.create(MediaType.parse(mimeType), file) return upload(uploadBody, filename, progressListener) } - fun uploadByteArray(byteArray: ByteArray, - filename: String?, - mimeType: String, - progressListener: ProgressRequestBody.Listener? = null): Try { - + suspend fun uploadByteArray(byteArray: ByteArray, + filename: String?, + mimeType: String, + progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse { val uploadBody = RequestBody.create(MediaType.parse(mimeType), byteArray) return upload(uploadBody, filename, progressListener) } - private fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): Try { - val urlBuilder = HttpUrl.parse(uploadUrl)?.newBuilder() ?: return raise(RuntimeException()) + private suspend fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): ContentUploadResponse { + val urlBuilder = HttpUrl.parse(uploadUrl)?.newBuilder() ?: throw RuntimeException() val httpUrl = urlBuilder .addQueryParameter("filename", filename) @@ -72,19 +69,15 @@ internal class FileUploader @Inject constructor(@Authenticated .post(requestBody) .build() - return Try { - okHttpClient.newCall(request).execute().use { response -> - if (!response.isSuccessful) { - throw response.toFailure() - } else { - response.body()?.source()?.let { - responseAdapter.fromJson(it) - } - ?: throw IOException() + return okHttpClient.newCall(request).awaitResponse().use { response -> + if (!response.isSuccessful) { + throw response.toFailure() + } else { + response.body()?.source()?.let { + responseAdapter.fromJson(it) } + ?: throw IOException() } } - } - } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt index b015670daa..2d9509b43d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt @@ -93,32 +93,28 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : } } - val contentUploadResponse = if (params.isRoomEncrypted) { - Timber.v("Encrypt thumbnail") - contentUploadStateTracker.setEncryptingThumbnail(eventId) - MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType) - .flatMap { encryptionResult -> - uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo + try { + val contentUploadResponse = if (params.isRoomEncrypted) { + Timber.v("Encrypt thumbnail") + contentUploadStateTracker.setEncryptingThumbnail(eventId) + val encryptionResult = MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType) + uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo + fileUploader.uploadByteArray(encryptionResult.encryptedByteArray, + "thumb_${attachment.name}", + "application/octet-stream", + thumbnailProgressListener) + } else { + fileUploader.uploadByteArray(thumbnailData.bytes, + "thumb_${attachment.name}", + thumbnailData.mimeType, + thumbnailProgressListener) + } - fileUploader - .uploadByteArray(encryptionResult.encryptedByteArray, - "thumb_${attachment.name}", - "application/octet-stream", - thumbnailProgressListener) - } - } else { - fileUploader - .uploadByteArray(thumbnailData.bytes, - "thumb_${attachment.name}", - thumbnailData.mimeType, - thumbnailProgressListener) + uploadedThumbnailUrl = contentUploadResponse.contentUri + } catch (t: Throwable) { + Timber.e(t) + return handleFailure(params, t) } - - contentUploadResponse - .fold( - { Timber.e(it) }, - { uploadedThumbnailUrl = it.contentUri } - ) } val progressListener = object : ProgressRequestBody.Listener { @@ -133,27 +129,26 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null - val contentUploadResponse = if (params.isRoomEncrypted) { - Timber.v("Encrypt file") - contentUploadStateTracker.setEncrypting(eventId) + return try { + val contentUploadResponse = if (params.isRoomEncrypted) { + Timber.v("Encrypt file") + contentUploadStateTracker.setEncrypting(eventId) - MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.mimeType) - .flatMap { encryptionResult -> - uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo + val encryptionResult = MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.mimeType) + uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo - fileUploader - .uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener) - } - } else { - fileUploader - .uploadFile(attachmentFile, attachment.name, attachment.mimeType, progressListener) + fileUploader + .uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener) + } else { + fileUploader + .uploadFile(attachmentFile, attachment.name, attachment.mimeType, progressListener) + } + + handleSuccess(params, contentUploadResponse.contentUri, uploadedFileEncryptedFileInfo, uploadedThumbnailUrl, uploadedThumbnailEncryptedFileInfo) + } catch (t: Throwable) { + Timber.e(t) + handleFailure(params, t) } - - return contentUploadResponse - .fold( - { handleFailure(params, it) }, - { handleSuccess(params, it.contentUri, uploadedFileEncryptedFileInfo, uploadedThumbnailUrl, uploadedThumbnailEncryptedFileInfo) } - ) } private fun handleFailure(params: Params, failure: Throwable): Result { From 17cba1a43223bebeaae7595509cc958aa5c23f5a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 25 Sep 2019 14:39:33 +0200 Subject: [PATCH 030/197] Display progress in the timeline when uploading file --- .../im/vector/riotx/core/utils/FileUtils.kt | 2 ++ .../timeline/factory/MessageItemFactory.kt | 5 +++++ .../helper/ContentUploadStateTrackerBinder.kt | 9 ++++----- .../detail/timeline/item/MessageFileItem.kt | 18 ++++++++++++++++++ .../timeline/item/MessageImageVideoItem.kt | 7 +++++-- .../features/media/ImageContentRenderer.kt | 6 ++---- .../layout/item_timeline_event_file_stub.xml | 2 +- 7 files changed, 37 insertions(+), 12 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/core/utils/FileUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/FileUtils.kt index 4b2d0682d2..5812c395e5 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/FileUtils.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/FileUtils.kt @@ -24,6 +24,8 @@ import java.io.File // Implementation should return true in case of success typealias ActionOnFile = (file: File) -> Boolean +internal fun String?.isLocalFile() = this != null && File(this).exists() + /* ========================================================================================== * Delete * ========================================================================================== */ diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 4819db4075..0dfa44563c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -42,6 +42,7 @@ import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.core.utils.DimensionConverter import im.vector.riotx.core.utils.containsOnlyEmojis +import im.vector.riotx.core.utils.isLocalFile import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder @@ -117,6 +118,8 @@ class MessageItemFactory @Inject constructor( .avatarRenderer(avatarRenderer) .colorProvider(colorProvider) .dimensionConverter(dimensionConverter) + .izLocalFile(messageContent.getFileUrl().isLocalFile()) + .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) .informationData(informationData) .highlighted(highlight) .avatarCallback(callback) @@ -147,6 +150,8 @@ class MessageItemFactory @Inject constructor( .avatarRenderer(avatarRenderer) .colorProvider(colorProvider) .dimensionConverter(dimensionConverter) + .izLocalFile(messageContent.getFileUrl().isLocalFile()) + .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) .informationData(informationData) .highlighted(highlight) .avatarCallback(callback) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt index ca79666747..a7db7c826e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt @@ -27,7 +27,6 @@ import im.vector.matrix.android.api.session.room.send.SendState import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.resources.ColorProvider -import im.vector.riotx.features.media.ImageContentRenderer import im.vector.riotx.features.ui.getMessageTextColor import javax.inject.Inject @@ -37,12 +36,12 @@ class ContentUploadStateTrackerBinder @Inject constructor(private val activeSess private val updateListeners = mutableMapOf() fun bind(eventId: String, - mediaData: ImageContentRenderer.Data, + isLocalFile: Boolean, progressLayout: ViewGroup) { activeSessionHolder.getActiveSession().also { session -> val uploadStateTracker = session.contentUploadProgressTracker() - val updateListener = ContentMediaProgressUpdater(progressLayout, mediaData, colorProvider) + val updateListener = ContentMediaProgressUpdater(progressLayout, isLocalFile, colorProvider) updateListeners[eventId] = updateListener uploadStateTracker.track(eventId, updateListener) } @@ -60,7 +59,7 @@ class ContentUploadStateTrackerBinder @Inject constructor(private val activeSess } private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup, - private val mediaData: ImageContentRenderer.Data, + private val isLocalFile: Boolean, private val colorProvider: ColorProvider) : ContentUploadStateTracker.UpdateListener { override fun onUpdate(state: ContentUploadStateTracker.State) { @@ -76,7 +75,7 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup, } private fun handleIdle(state: ContentUploadStateTracker.State.Idle) { - if (mediaData.isLocalFile()) { + if (isLocalFile) { progressLayout.isVisible = true val progressBar = progressLayout.findViewById(R.id.mediaProgressBar) val progressTextView = progressLayout.findViewById(R.id.mediaProgressTextView) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageFileItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageFileItem.kt index 45e57b59db..56d6a33bc7 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageFileItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageFileItem.kt @@ -22,9 +22,11 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.annotation.DrawableRes +import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R +import im.vector.riotx.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder @EpoxyModelClass(layout = R.layout.item_timeline_event_base) abstract class MessageFileItem : AbsMessageItem() { @@ -36,19 +38,35 @@ abstract class MessageFileItem : AbsMessageItem() { var iconRes: Int = 0 @EpoxyAttribute var clickListener: View.OnClickListener? = null + @EpoxyAttribute + var izLocalFile = false + @EpoxyAttribute + lateinit var contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder override fun bind(holder: Holder) { super.bind(holder) renderSendState(holder.fileLayout, holder.filenameView) + if (!informationData.sendState.hasFailed()) { + contentUploadStateTrackerBinder.bind(informationData.eventId, izLocalFile, holder.progressLayout) + } else { + holder.progressLayout.isVisible = false + } holder.filenameView.text = filename holder.fileImageView.setImageResource(iconRes) holder.filenameView.setOnClickListener(clickListener) holder.filenameView.paintFlags = (holder.filenameView.paintFlags or Paint.UNDERLINE_TEXT_FLAG) } + override fun unbind(holder: Holder) { + super.unbind(holder) + + contentUploadStateTrackerBinder.unbind(informationData.eventId) + } + override fun getViewType() = STUB_ID class Holder : AbsMessageItem.Holder(STUB_ID) { + val progressLayout by bind(R.id.messageFileUploadProgressLayout) val fileLayout by bind(R.id.messageFileLayout) val fileImageView by bind(R.id.messageFileImageView) val filenameView by bind(R.id.messageFilenameView) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt index 6f713b17fe..50e263267a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt @@ -20,6 +20,7 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageView import androidx.core.view.ViewCompat +import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R @@ -44,11 +45,13 @@ abstract class MessageImageVideoItem : AbsMessageItem Date: Wed, 25 Sep 2019 14:44:34 +0200 Subject: [PATCH 031/197] Human readable error --- .../helper/ContentUploadStateTrackerBinder.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt index a7db7c826e..09b2e86a83 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt @@ -26,12 +26,14 @@ import im.vector.matrix.android.api.session.content.ContentUploadStateTracker import im.vector.matrix.android.api.session.room.send.SendState import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder +import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.features.ui.getMessageTextColor import javax.inject.Inject class ContentUploadStateTrackerBinder @Inject constructor(private val activeSessionHolder: ActiveSessionHolder, - private val colorProvider: ColorProvider) { + private val colorProvider: ColorProvider, + private val errorFormatter: ErrorFormatter) { private val updateListeners = mutableMapOf() @@ -41,7 +43,7 @@ class ContentUploadStateTrackerBinder @Inject constructor(private val activeSess activeSessionHolder.getActiveSession().also { session -> val uploadStateTracker = session.contentUploadProgressTracker() - val updateListener = ContentMediaProgressUpdater(progressLayout, isLocalFile, colorProvider) + val updateListener = ContentMediaProgressUpdater(progressLayout, isLocalFile, colorProvider, errorFormatter) updateListeners[eventId] = updateListener uploadStateTracker.track(eventId, updateListener) } @@ -60,7 +62,8 @@ class ContentUploadStateTrackerBinder @Inject constructor(private val activeSess private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup, private val isLocalFile: Boolean, - private val colorProvider: ColorProvider) : ContentUploadStateTracker.UpdateListener { + private val colorProvider: ColorProvider, + private val errorFormatter: ErrorFormatter) : ContentUploadStateTracker.UpdateListener { override fun onUpdate(state: ContentUploadStateTracker.State) { when (state) { @@ -133,7 +136,7 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup, val progressBar = progressLayout.findViewById(R.id.mediaProgressBar) val progressTextView = progressLayout.findViewById(R.id.mediaProgressTextView) progressBar?.isVisible = false - progressTextView?.text = state.throwable.localizedMessage + progressTextView?.text = errorFormatter.toHumanReadable(state.throwable) progressTextView?.setTextColor(colorProvider.getMessageTextColor(SendState.UNDELIVERED)) } From 643a2baabf082dbcf8301d68b3b5c7a3e1e22407 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 25 Sep 2019 15:03:16 +0200 Subject: [PATCH 032/197] Set click and long click listener even if information data are not displayed --- .../home/room/detail/timeline/item/AbsMessageItem.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt index 8cc181bd37..fa132be365 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -111,8 +111,6 @@ abstract class AbsMessageItem : BaseEventItem() { holder.timeView.text = informationData.time holder.memberNameView.text = informationData.memberName avatarRenderer.render(informationData.avatarUrl, informationData.senderId, informationData.memberName?.toString(), holder.avatarImageView) - holder.view.setOnClickListener(cellClickListener) - holder.view.setOnLongClickListener(longClickListener) holder.avatarImageView.setOnLongClickListener(longClickListener) holder.memberNameView.setOnLongClickListener(longClickListener) } else { @@ -121,12 +119,13 @@ abstract class AbsMessageItem : BaseEventItem() { holder.avatarImageView.visibility = View.GONE holder.memberNameView.visibility = View.GONE holder.timeView.visibility = View.GONE - holder.view.setOnClickListener(null) - holder.view.setOnLongClickListener(null) holder.avatarImageView.setOnLongClickListener(null) holder.memberNameView.setOnLongClickListener(null) } + holder.view.setOnClickListener(cellClickListener) + holder.view.setOnLongClickListener(longClickListener) + holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener) if (!shouldShowReactionAtBottom() || informationData.orderedReactionList.isNullOrEmpty()) { From 1b66d1f746374992f0e30a3226a001f88313c899 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 25 Sep 2019 15:25:26 +0200 Subject: [PATCH 033/197] Fix bad rendering of file item if the filename is long --- .../layout/item_timeline_event_file_stub.xml | 86 ++++++++++--------- 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/vector/src/main/res/layout/item_timeline_event_file_stub.xml b/vector/src/main/res/layout/item_timeline_event_file_stub.xml index 729509b859..259e22b466 100644 --- a/vector/src/main/res/layout/item_timeline_event_file_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_file_stub.xml @@ -2,53 +2,55 @@ + android:layout_height="wrap_content" + android:paddingTop="8dp" + android:paddingBottom="8dp"> - + + + + + + + app:layout_constraintStart_toEndOf="@+id/messageFileImageView" + app:layout_constraintTop_toTopOf="parent" + tools:text="A filename here" /> - - - - - - - - - + + From a0b1ef3216522f8a468b0e7db47cb20a9ba0e76f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 25 Sep 2019 16:59:50 +0200 Subject: [PATCH 034/197] Do not upload file to big for the homeserver (#587) Also create a HomeServerCapabilitiesService which provide configuration of the homeserver. Data are retrieved every 8 hours (as RiotWeb?) --- CHANGES.md | 3 +- .../matrix/android/api/session/Session.kt | 2 + .../homeserver/HomeServerCapabilities.kt | 28 +++++++ .../HomeServerCapabilitiesService.kt | 29 ++++++++ .../mapper/HomeServerCapabilitiesMapper.kt | 38 ++++++++++ .../model/HomeServerCapabilitiesEntity.kt | 29 ++++++++ .../database/model/SessionRealmModule.kt | 3 +- .../query/HomeServerCapabilitiesQueries.kt | 37 ++++++++++ .../internal/network/NetworkConstants.kt | 5 ++ .../internal/session/DefaultSession.kt | 7 +- .../internal/session/SessionComponent.kt | 2 + .../android/internal/session/SessionModule.kt | 5 ++ .../session/homeserver/CapabilitiesAPI.kt | 31 ++++++++ .../DefaultGetHomeServerCapabilitiesTask.kt | 74 +++++++++++++++++++ .../DefaultHomeServerCapabilitiesService.kt | 37 ++++++++++ .../homeserver/GetUploadCapabilitiesResult.kt | 30 ++++++++ .../HomeServerCapabilitiesModule.kt | 41 ++++++++++ .../android/internal/session/sync/SyncTask.kt | 7 +- .../home/room/detail/FileTooBigError.kt | 23 ++++++ .../home/room/detail/RoomDetailFragment.kt | 17 +++++ .../home/room/detail/RoomDetailViewModel.kt | 20 ++++- vector/src/main/res/values/strings_riotX.xml | 2 + 22 files changed, 463 insertions(+), 7 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilities.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilitiesService.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/HomeServerCapabilitiesMapper.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/HomeServerCapabilitiesEntity.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/HomeServerCapabilitiesQueries.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/CapabilitiesAPI.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/GetUploadCapabilitiesResult.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeServerCapabilitiesModule.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/FileTooBigError.kt diff --git a/CHANGES.md b/CHANGES.md index 53ed7748de..396419ea66 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,12 +6,13 @@ Features: Improvements: - Persist active tab between sessions (#503) + - Do not upload file to big for the homeserver (#587) Other changes: - Bugfix: - - + - Fix issue on upload error in loop (#587) Translations: - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt index 53dc8e77a0..31e96bb3b8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt @@ -26,6 +26,7 @@ import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.group.GroupService +import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService import im.vector.matrix.android.api.session.pushers.PushersService import im.vector.matrix.android.api.session.room.RoomDirectoryService import im.vector.matrix.android.api.session.room.RoomService @@ -52,6 +53,7 @@ interface Session : PushRuleService, PushersService, InitialSyncProgressService, + HomeServerCapabilitiesService, SecureStorageService { /** diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilities.kt new file mode 100644 index 0000000000..215516a6c1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilities.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 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.matrix.android.api.session.homeserver + +data class HomeServerCapabilities( + /** + * Max size of file which can be uploaded to the homeserver in bytes. [MAX_UPLOAD_FILE_SIZE_UNKNOWN] if unknown or not retrieved yet + */ + val maxUploadFileSize: Long +) { + companion object { + const val MAX_UPLOAD_FILE_SIZE_UNKNOWN = -1L + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilitiesService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilitiesService.kt new file mode 100644 index 0000000000..f7107e9d47 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilitiesService.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 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.matrix.android.api.session.homeserver + +/** + * This interface defines a method to sign out. It's implemented at the session level. + */ +interface HomeServerCapabilitiesService { + + /** + * Get the HomeServer capabilities + */ + fun getHomeServerCapabilities(): HomeServerCapabilities + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/HomeServerCapabilitiesMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/HomeServerCapabilitiesMapper.kt new file mode 100644 index 0000000000..23ab7b64be --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/HomeServerCapabilitiesMapper.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2019 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.matrix.android.internal.database.mapper + +import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities +import im.vector.matrix.android.internal.database.model.HomeServerCapabilitiesEntity + +/** + * HomeServerCapabilitiesEntity <-> HomeSeverCapabilities + */ +internal object HomeServerCapabilitiesMapper { + + fun map(entity: HomeServerCapabilitiesEntity): HomeServerCapabilities { + return HomeServerCapabilities( + entity.maxUploadFileSize + ) + } + + fun map(domain: HomeServerCapabilities): HomeServerCapabilitiesEntity { + return HomeServerCapabilitiesEntity( + domain.maxUploadFileSize + ) + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/HomeServerCapabilitiesEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/HomeServerCapabilitiesEntity.kt new file mode 100644 index 0000000000..2bed0305c7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/HomeServerCapabilitiesEntity.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 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.matrix.android.internal.database.model + +import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities +import io.realm.RealmObject + +internal open class HomeServerCapabilitiesEntity( + var maxUploadFileSize: Long = HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN, + var lastUpdatedTimestamp: Long = 0L +) : RealmObject() { + + companion object + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt index 680e2eac7d..ffe20d9efe 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt @@ -45,6 +45,7 @@ import io.realm.annotations.RealmModule PusherDataEntity::class, ReadReceiptsSummaryEntity::class, UserDraftsEntity::class, - DraftEntity::class + DraftEntity::class, + HomeServerCapabilitiesEntity::class ]) internal class SessionRealmModule diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/HomeServerCapabilitiesQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/HomeServerCapabilitiesQueries.kt new file mode 100644 index 0000000000..64cd6e4770 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/HomeServerCapabilitiesQueries.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 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.matrix.android.internal.database.query + +import im.vector.matrix.android.internal.database.model.HomeServerCapabilitiesEntity +import io.realm.Realm +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +/** + * Get the current HomeServerCapabilitiesEntity, create one if it does not exist + */ +internal fun HomeServerCapabilitiesEntity.Companion.getOrCreate(realm: Realm): HomeServerCapabilitiesEntity { + var homeServerCapabilitiesEntity = realm.where().findFirst() + if (homeServerCapabilitiesEntity == null) { + realm.executeTransaction { + realm.createObject() + } + homeServerCapabilitiesEntity = realm.where().findFirst()!! + } + + return homeServerCapabilitiesEntity +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt index cbd4d0c674..02ac778fcc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt @@ -22,4 +22,9 @@ internal object NetworkConstants { const val URI_API_PREFIX_PATH_R0 = "$URI_API_PREFIX_PATH/r0/" const val URI_API_PREFIX_PATH_UNSTABLE = "$URI_API_PREFIX_PATH/unstable/" + + // Media + private const val URI_API_MEDIA_PREFIX_PATH = "_matrix/media" + const val URI_API_MEDIA_PREFIX_PATH_R0 = "$URI_API_MEDIA_PREFIX_PATH/r0/" + } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index 02addaceab..319cce491b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -32,6 +32,7 @@ import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.group.GroupService +import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService import im.vector.matrix.android.api.session.pushers.PushersService import im.vector.matrix.android.api.session.room.RoomDirectoryService import im.vector.matrix.android.api.session.room.RoomService @@ -68,7 +69,8 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se private val syncThreadProvider: Provider, private val contentUrlResolver: ContentUrlResolver, private val contentUploadProgressTracker: ContentUploadStateTracker, - private val initialSyncProgressService: Lazy) + private val initialSyncProgressService: Lazy, + private val homeServerCapabilitiesService: Lazy) : Session, RoomService by roomService.get(), RoomDirectoryService by roomDirectoryService.get(), @@ -81,7 +83,8 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se PushersService by pushersService.get(), FileService by fileService.get(), InitialSyncProgressService by initialSyncProgressService.get(), - SecureStorageService by secureStorageService.get() { + SecureStorageService by secureStorageService.get(), + HomeServerCapabilitiesService by homeServerCapabilitiesService.get() { private var isOpen = false diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt index c8745fc356..b2ed02ff3e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt @@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.session.content.UploadContentWorker import im.vector.matrix.android.internal.session.filter.FilterModule import im.vector.matrix.android.internal.session.group.GetGroupDataWorker import im.vector.matrix.android.internal.session.group.GroupModule +import im.vector.matrix.android.internal.session.homeserver.HomeServerCapabilitiesModule import im.vector.matrix.android.internal.session.pushers.AddHttpPusherWorker import im.vector.matrix.android.internal.session.pushers.PushersModule import im.vector.matrix.android.internal.session.room.RoomModule @@ -51,6 +52,7 @@ import im.vector.matrix.android.internal.task.TaskExecutor SessionModule::class, RoomModule::class, SyncModule::class, + HomeServerCapabilitiesModule::class, SignOutModule::class, GroupModule::class, UserModule::class, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index db4997ca89..7b655dd939 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -27,6 +27,7 @@ import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.session.InitialSyncProgressService import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService import im.vector.matrix.android.api.session.securestorage.SecureStorageService import im.vector.matrix.android.internal.database.LiveEntityObserver import im.vector.matrix.android.internal.database.RealmKeysUtils @@ -36,6 +37,7 @@ import im.vector.matrix.android.internal.network.AccessTokenInterceptor import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater +import im.vector.matrix.android.internal.session.homeserver.DefaultHomeServerCapabilitiesService import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater import im.vector.matrix.android.internal.session.room.create.RoomCreateEventLiveObserver import im.vector.matrix.android.internal.session.room.prune.EventsPruner @@ -178,4 +180,7 @@ internal abstract class SessionModule { @Binds abstract fun bindSecureStorageService(secureStorageService: DefaultSecureStorageService): SecureStorageService + @Binds + abstract fun bindHomeServerCapabilitiesService(homeServerCapabilitiesService: DefaultHomeServerCapabilitiesService): HomeServerCapabilitiesService + } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/CapabilitiesAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/CapabilitiesAPI.kt new file mode 100644 index 0000000000..69972a1f57 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/CapabilitiesAPI.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 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.matrix.android.internal.session.homeserver + +import im.vector.matrix.android.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.GET + +internal interface CapabilitiesAPI { + + /** + * Request the upload capabilities + */ + @GET(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "config") + fun getUploadCapabilities(): Call + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt new file mode 100644 index 0000000000..d3cb945adb --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2019 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.matrix.android.internal.session.homeserver + +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities +import im.vector.matrix.android.internal.database.model.HomeServerCapabilitiesEntity +import im.vector.matrix.android.internal.database.query.getOrCreate +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task +import java.util.* +import javax.inject.Inject + +internal interface GetHomeServerCapabilitiesTask : Task + +internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( + private val capabilitiesAPI: CapabilitiesAPI, + private val monarchy: Monarchy +) : GetHomeServerCapabilitiesTask { + + + override suspend fun execute(params: Unit) { + var doRequest = false + monarchy.doWithRealm { realm -> + val homeServerCapabilitiesEntity = HomeServerCapabilitiesEntity.getOrCreate(realm) + + doRequest = homeServerCapabilitiesEntity.lastUpdatedTimestamp + MIN_DELAY_BETWEEN_TWO_REQUEST_MILLIS < Date().time + } + + if (!doRequest) { + return + } + + val uploadCapabilities = executeRequest { + apiCall = capabilitiesAPI.getUploadCapabilities() + } + + // TODO Add other call here (get version, etc.) + + insertInDb(uploadCapabilities) + } + + + private fun insertInDb(getUploadCapabilitiesResult: GetUploadCapabilitiesResult) { + monarchy + .writeAsync { realm -> + val homeServerCapabilitiesEntity = HomeServerCapabilitiesEntity.getOrCreate(realm) + + homeServerCapabilitiesEntity.maxUploadFileSize = getUploadCapabilitiesResult.maxUploadSize + ?: HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN + + homeServerCapabilitiesEntity.lastUpdatedTimestamp = Date().time + } + } + + companion object { + // 8 hours like on Riot Web + private const val MIN_DELAY_BETWEEN_TWO_REQUEST_MILLIS = 8 * 60 * 60 * 1000 + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt new file mode 100644 index 0000000000..6f6416ba4a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 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.matrix.android.internal.session.homeserver + +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities +import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService +import im.vector.matrix.android.internal.database.mapper.HomeServerCapabilitiesMapper +import im.vector.matrix.android.internal.database.model.HomeServerCapabilitiesEntity +import im.vector.matrix.android.internal.database.query.getOrCreate +import javax.inject.Inject + +internal class DefaultHomeServerCapabilitiesService @Inject constructor(private val monarchy: Monarchy) : HomeServerCapabilitiesService { + + override fun getHomeServerCapabilities(): HomeServerCapabilities { + var entity: HomeServerCapabilitiesEntity? = null + monarchy.doWithRealm { realm -> + entity = HomeServerCapabilitiesEntity.getOrCreate(realm) + } + + return entity?.let { HomeServerCapabilitiesMapper.map(it) } ?: HomeServerCapabilities(HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN) + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/GetUploadCapabilitiesResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/GetUploadCapabilitiesResult.kt new file mode 100644 index 0000000000..8e410cc834 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/GetUploadCapabilitiesResult.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 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.matrix.android.internal.session.homeserver + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class GetUploadCapabilitiesResult( + /** + * The maximum size an upload can be in bytes. Clients SHOULD use this as a guide when uploading content. + * If not listed or null, the size limit should be treated as unknown. + */ + @Json(name = "m.upload.size") + val maxUploadSize: Long? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeServerCapabilitiesModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeServerCapabilitiesModule.kt new file mode 100644 index 0000000000..71b3ee63b8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeServerCapabilitiesModule.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2019 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.matrix.android.internal.session.homeserver + +import dagger.Binds +import dagger.Module +import dagger.Provides +import im.vector.matrix.android.internal.session.SessionScope +import retrofit2.Retrofit + +@Module +internal abstract class HomeServerCapabilitiesModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesCapabilitiesAPI(retrofit: Retrofit): CapabilitiesAPI { + return retrofit.create(CapabilitiesAPI::class.java) + } + } + + @Binds + abstract fun bindGetHomeServerCapabilitiesTask(getHomeServerCapabilitiesTask: DefaultGetHomeServerCapabilitiesTask): GetHomeServerCapabilitiesTask + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt index 28d4d5fc48..f9cbd05d26 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.session.sync -import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.R import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.MatrixError @@ -25,6 +24,7 @@ import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService import im.vector.matrix.android.internal.session.filter.FilterRepository +import im.vector.matrix.android.internal.session.homeserver.GetHomeServerCapabilitiesTask import im.vector.matrix.android.internal.session.sync.model.SyncResponse import im.vector.matrix.android.internal.task.Task import javax.inject.Inject @@ -42,11 +42,14 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI, private val sessionParamsStore: SessionParamsStore, private val initialSyncProgressService: DefaultInitialSyncProgressService, private val syncTokenStore: SyncTokenStore, - private val monarchy: Monarchy + private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask ) : SyncTask { override suspend fun execute(params: SyncTask.Params) { + // Maybe refresh the home server capabilities data we know + getHomeServerCapabilitiesTask.execute(Unit) + val requestParams = HashMap() var timeout = 0L val token = syncTokenStore.getLastToken() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/FileTooBigError.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/FileTooBigError.kt new file mode 100644 index 0000000000..0f9bfebb47 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/FileTooBigError.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2019 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.riotx.features.home.room.detail + +data class FileTooBigError( + val filename: String, + val fileSizeInBytes: Long, + val homeServerLimitInBytes: Long +) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 7bc5cf7016..7934e0ccae 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -28,6 +28,7 @@ import android.os.Parcelable import android.text.Editable import android.text.Spannable import android.text.TextUtils +import android.text.format.Formatter import android.view.* import android.view.inputmethod.InputMethodManager import android.widget.TextView @@ -227,6 +228,10 @@ class RoomDetailFragment : scrollOnHighlightedEventCallback.scheduleScrollTo(it) } + roomDetailViewModel.fileTooBigEvent.observeEvent(this) { + displayFileTooBigWarning(it) + } + roomDetailViewModel.selectSubscribe(this, RoomDetailViewState::tombstoneEventHandling, uniqueOnly("tombstoneEventHandling")) { renderTombstoneEventHandling(it) } @@ -254,6 +259,18 @@ class RoomDetailFragment : } } + private fun displayFileTooBigWarning(error: FileTooBigError) { + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.dialog_title_error) + .setMessage(getString(R.string.error_file_too_big, + error.filename, + Formatter.formatFileSize(requireContext(), error.homeServerLimitInBytes), + Formatter.formatFileSize(requireContext(), error.fileSizeInBytes) + )) + .setPositiveButton(R.string.ok, null) + .show() + } + private fun setupNotificationView() { notificationAreaView.delegate = object : NotificationAreaView.Delegate { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index ac56114319..d31a25a8d9 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -37,6 +37,7 @@ import im.vector.matrix.android.api.session.events.model.isImageMessage import im.vector.matrix.android.api.session.events.model.isTextMessage import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.file.FileService +import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageType @@ -228,6 +229,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro val navigateToEvent: LiveData> get() = _navigateToEvent + private val _fileTooBigEvent = MutableLiveData>() + val fileTooBigEvent: LiveData> + get() = _fileTooBigEvent + private val _downloadedFileEvent = MutableLiveData>() val downloadedFileEvent: LiveData> get() = _downloadedFileEvent @@ -466,7 +471,20 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro type = ContentAttachmentData.Type.values()[it.mediaType] ) } - room.sendMedias(attachments) + + val homeServerCapabilities = session.getHomeServerCapabilities() + + val maxUploadFileSize = homeServerCapabilities.maxUploadFileSize + + if (maxUploadFileSize == HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN) { + // Unknown limitation + room.sendMedias(attachments) + } else { + attachments.find { it.size > maxUploadFileSize } + ?.let { + _fileTooBigEvent.postValue(LiveEvent(FileTooBigError(it.name ?: it.path, it.size, maxUploadFileSize))) + } ?: run { room.sendMedias(attachments) } + } } private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) { diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 6b46d359be..7d3312cf24 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -22,4 +22,6 @@ Create a new room Close keys backup banner + The file %1$s is too large to upload. The file size limit is %2$s but this file is %3$s. + \ No newline at end of file From 60f6b3ef02a6e2b62f031aa6cff161ff50f2c941 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 25 Sep 2019 17:08:58 +0200 Subject: [PATCH 035/197] Auto review --- CHANGES.md | 2 +- .../api/session/homeserver/HomeServerCapabilitiesService.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 396419ea66..dc4c743bc4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,7 +6,7 @@ Features: Improvements: - Persist active tab between sessions (#503) - - Do not upload file to big for the homeserver (#587) + - Do not upload file too big for the homeserver (#587) Other changes: - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilitiesService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilitiesService.kt index f7107e9d47..8e23c21068 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilitiesService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilitiesService.kt @@ -17,7 +17,7 @@ package im.vector.matrix.android.api.session.homeserver /** - * This interface defines a method to sign out. It's implemented at the session level. + * This interface defines a method to retrieve the homeserver capabilities. */ interface HomeServerCapabilitiesService { From 4a80df082ceb85b14964509b5d4f8173ffe14570 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 25 Sep 2019 19:14:12 +0200 Subject: [PATCH 036/197] Timeline: refact [WIP] --- .../EventAnnotationsSummaryEntityQuery.kt | 8 +- .../query/ReadReceiptsSummaryEntityQueries.kt | 9 +- .../query/TimelineEventEntityQueries.kt | 8 +- .../room/EventRelationsAggregationTask.kt | 10 +-- .../session/room/read/SetReadMarkersTask.kt | 21 +++-- .../session/room/send/DefaultSendService.kt | 2 +- .../session/room/timeline/DefaultTimeline.kt | 9 +- .../room/timeline/DefaultTimelineService.kt | 6 +- .../room/timeline/TimelineHiddenReadMarker.kt | 89 ++++++++++++------- .../session/sync/RoomFullyReadHandler.kt | 15 ++-- .../core/ui/views/JumpToReadMarkerView.kt | 31 +++++-- .../riotx/core/ui/views/ReadMarkerView.kt | 1 - .../home/room/detail/ReadMarkerHelper.kt | 25 +++--- .../home/room/detail/RoomDetailFragment.kt | 73 ++++++++------- .../home/room/detail/RoomDetailViewModel.kt | 78 +++++++--------- .../home/room/detail/RoomDetailViewState.kt | 3 +- .../timeline/TimelineEventController.kt | 57 +++++------- .../timeline/TimelineLayoutManagerHolder.kt | 29 ------ .../factory/MergedHeaderItemFactory.kt | 3 +- .../detail/timeline/item/AbsMessageItem.kt | 7 +- .../detail/timeline/item/BaseEventItem.kt | 3 + .../room/detail/timeline/item/DefaultItem.kt | 4 + .../detail/timeline/item/MergedHeaderItem.kt | 10 ++- .../room/detail/timeline/item/NoticeItem.kt | 7 +- .../main/res/layout/fragment_room_detail.xml | 4 +- 25 files changed, 266 insertions(+), 246 deletions(-) delete mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineLayoutManagerHolder.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventAnnotationsSummaryEntityQuery.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventAnnotationsSummaryEntityQuery.kt index f1179ebeb4..0f454b0af7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventAnnotationsSummaryEntityQuery.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventAnnotationsSummaryEntityQuery.kt @@ -38,10 +38,12 @@ internal fun EventAnnotationsSummaryEntity.Companion.whereInRoom(realm: Realm, r } -internal fun EventAnnotationsSummaryEntity.Companion.create(realm: Realm, eventId: String): EventAnnotationsSummaryEntity { - val obj = realm.createObject(EventAnnotationsSummaryEntity::class.java, eventId) +internal fun EventAnnotationsSummaryEntity.Companion.create(realm: Realm, roomId: String, eventId: String): EventAnnotationsSummaryEntity { + val obj = realm.createObject(EventAnnotationsSummaryEntity::class.java, eventId).apply { + this.roomId = roomId + } //Denormalization - TimelineEventEntity.where(realm, eventId = eventId).findFirst()?.let { + TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId).findFirst()?.let { it.annotations = obj } return obj diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadReceiptsSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadReceiptsSummaryEntityQueries.kt index 0c3d7d8eb1..1773297727 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadReceiptsSummaryEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadReceiptsSummaryEntityQueries.kt @@ -27,10 +27,7 @@ internal fun ReadReceiptsSummaryEntity.Companion.where(realm: Realm, eventId: St .equalTo(ReadReceiptsSummaryEntityFields.EVENT_ID, eventId) } -internal fun ReadReceiptsSummaryEntity.Companion.whereInRoom(realm: Realm, roomId: String?): RealmQuery { - val query = realm.where() - if (roomId != null) { - query.equalTo(ReadReceiptsSummaryEntityFields.ROOM_ID, roomId) - } - return query +internal fun ReadReceiptsSummaryEntity.Companion.whereInRoom(realm: Realm, roomId: String): RealmQuery { + return realm.where() + .equalTo(ReadReceiptsSummaryEntityFields.ROOM_ID, roomId) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt index 182e58a3b5..8b9beca11e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt @@ -22,13 +22,15 @@ import im.vector.matrix.android.internal.database.model.EventEntity.LinkFilterMo import io.realm.* import io.realm.kotlin.where -internal fun TimelineEventEntity.Companion.where(realm: Realm, eventId: String): RealmQuery { +internal fun TimelineEventEntity.Companion.where(realm: Realm, roomId: String, eventId: String): RealmQuery { return realm.where() + .equalTo(TimelineEventEntityFields.ROOM_ID, roomId) .equalTo(TimelineEventEntityFields.EVENT_ID, eventId) } -internal fun TimelineEventEntity.Companion.where(realm: Realm, eventIds: List): RealmQuery { +internal fun TimelineEventEntity.Companion.where(realm: Realm, roomId: String, eventIds: List): RealmQuery { return realm.where() + .equalTo(TimelineEventEntityFields.ROOM_ID, roomId) .`in`(TimelineEventEntityFields.EVENT_ID, eventIds.toTypedArray()) } @@ -121,6 +123,6 @@ internal fun TimelineEventEntity.Companion.findAllInRoomWithSendStates(realm: Re val sendStatesStr = sendStates.map { it.name }.toTypedArray() return realm.where() .equalTo(TimelineEventEntityFields.ROOM_ID, roomId) - .`in`(TimelineEventEntityFields.ROOT.SEND_STATE_STR,sendStatesStr) + .`in`(TimelineEventEntityFields.ROOT.SEND_STATE_STR, sendStatesStr) .findAll() } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt index 786ba168ac..3743eef211 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt @@ -85,7 +85,7 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor( EventAnnotationsSummaryEntity.where(realm, event.eventId ?: "").findFirst()?.let { - TimelineEventEntity.where(realm, eventId = event.eventId + TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findFirst()?.let { tet -> tet.annotations = it } @@ -167,8 +167,7 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor( var existing = EventAnnotationsSummaryEntity.where(realm, targetEventId).findFirst() if (existing == null) { Timber.v("###REPLACE creating new relation summary for $targetEventId") - existing = EventAnnotationsSummaryEntity.create(realm, targetEventId) - existing.roomId = roomId + existing = EventAnnotationsSummaryEntity.create(realm, roomId, targetEventId) } //we have it @@ -233,8 +232,7 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor( val eventId = event.eventId ?: "" val existing = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() if (existing == null) { - val eventSummary = EventAnnotationsSummaryEntity.create(realm, eventId) - eventSummary.roomId = roomId + val eventSummary = EventAnnotationsSummaryEntity.create(realm, roomId, eventId) val sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java) sum.key = it.key sum.firstTimestamp = event.originServerTs ?: 0 //TODO how to maintain order? @@ -261,7 +259,7 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor( val reactionEventId = event.eventId Timber.v("Reaction $reactionEventId relates to $relatedEventID") val eventSummary = EventAnnotationsSummaryEntity.where(realm, relatedEventID).findFirst() - ?: EventAnnotationsSummaryEntity.create(realm, relatedEventID).apply { this.roomId = roomId } + ?: EventAnnotationsSummaryEntity.create(realm, roomId, relatedEventID).apply { this.roomId = roomId } var sum = eventSummary.reactionsSummary.find { it.key == reaction } val txId = event.unsignedData?.transactionId diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt index 6a875b0563..2748a49930 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt @@ -17,9 +17,7 @@ package im.vector.matrix.android.internal.session.room.read import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ReadMarkerEntity -import im.vector.matrix.android.internal.database.model.ReadReceiptEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.query.* @@ -83,7 +81,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI } if (readReceiptEventId != null - && !isEventRead(monarchy, userId, params.roomId, readReceiptEventId)) { + && !isEventRead(monarchy, userId, params.roomId, readReceiptEventId)) { if (LocalEchoEventFactory.isLocalEchoId(readReceiptEventId)) { Timber.w("Can't set read receipt for local event $readReceiptEventId") } else { @@ -112,7 +110,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI val isLatestReceived = TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId == readReceiptId if (isLatestReceived) { val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() - ?: return@awaitTransaction + ?: return@awaitTransaction roomSummary.notificationCount = 0 roomSummary.highlightCount = 0 roomSummary.hasUnreadMessages = false @@ -121,14 +119,15 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI } } - private fun isReadMarkerMoreRecent(roomId: String, fullyReadEventId: String): Boolean { + private fun isReadMarkerMoreRecent(roomId: String, newReadMarkerId: String): Boolean { return Realm.getInstance(monarchy.realmConfiguration).use { realm -> - val readMarkerEntity = ReadMarkerEntity.where(realm, roomId = roomId).findFirst() - val readMarkerEvent = readMarkerEntity?.timelineEvent?.firstOrNull() - val eventToCheck = TimelineEventEntity.where(realm, eventId = fullyReadEventId).findFirst() - val readReceiptIndex = readMarkerEvent?.root?.displayIndex ?: Int.MAX_VALUE - val eventToCheckIndex = eventToCheck?.root?.displayIndex ?: Int.MIN_VALUE - eventToCheckIndex > readReceiptIndex + val currentReadMarkerId = ReadMarkerEntity.where(realm, roomId = roomId).findFirst()?.eventId + ?: return true + val readMarkerEvent = TimelineEventEntity.where(realm, roomId = roomId, eventId = currentReadMarkerId).findFirst() + val newReadMarkerEvent = TimelineEventEntity.where(realm, roomId = roomId, eventId = newReadMarkerId).findFirst() + val currentReadMarkerIndex = readMarkerEvent?.root?.displayIndex ?: Int.MAX_VALUE + val newReadMarkerIndex = newReadMarkerEvent?.root?.displayIndex ?: Int.MIN_VALUE + newReadMarkerIndex > currentReadMarkerIndex } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt index a342d3fe72..8b5b84c297 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt @@ -158,7 +158,7 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private override fun deleteFailedEcho(localEcho: TimelineEvent) { monarchy.writeAsync { realm -> - TimelineEventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.let { + TimelineEventEntity.where(realm, roomId = roomId, eventId = localEcho.root.eventId ?: "").findFirst()?.let { it.deleteFromRealm() } EventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.let { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index 9147b922cb..d95408bfb1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.api.util.CancelableBag +import im.vector.matrix.android.internal.database.helper.deleteOnCascade import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.ChunkEntity @@ -625,12 +626,14 @@ internal class DefaultTimeline( } private fun clearUnlinkedEvents(realm: Realm) { - realm.executeTransaction { + realm.executeTransaction { localRealm -> val unlinkedChunks = ChunkEntity - .where(it, roomId = roomId) + .where(localRealm, roomId = roomId) .equalTo("${ChunkEntityFields.TIMELINE_EVENTS.ROOT}.${EventEntityFields.IS_UNLINKED}", true) .findAll() - unlinkedChunks.deleteAllFromRealm() + unlinkedChunks.forEach { + it.deleteOnCascade() + } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt index 0ded458a20..4c4b08ce4f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt @@ -60,14 +60,14 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv timelineEventMapper, settings, TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings), - TimelineHiddenReadMarker(roomId) + TimelineHiddenReadMarker(roomId, settings) ) } override fun getTimeLineEvent(eventId: String): TimelineEvent? { return monarchy .fetchCopyMap({ - TimelineEventEntity.where(it, eventId = eventId).findFirst() + TimelineEventEntity.where(it, roomId = roomId, eventId = eventId).findFirst() }, { entity, realm -> timelineEventMapper.map(entity) }) @@ -75,7 +75,7 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv override fun getTimeLineEventLive(eventId: String): LiveData { val liveData = RealmLiveData(monarchy.realmConfiguration) { - TimelineEventEntity.where(it, eventId = eventId) + TimelineEventEntity.where(it, roomId = roomId, eventId = eventId) } return Transformations.map(liveData) { events -> events.firstOrNull()?.let { timelineEventMapper.map(it) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt index 7ae6cbcfe1..eebb98ca19 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt @@ -18,12 +18,16 @@ package im.vector.matrix.android.internal.session.room.timeline +import im.vector.matrix.android.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.internal.database.model.ReadMarkerEntity +import im.vector.matrix.android.internal.database.model.ReadMarkerEntityFields import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields +import im.vector.matrix.android.internal.database.query.FilterContent import im.vector.matrix.android.internal.database.query.where +import io.realm.OrderedRealmCollectionChangeListener import io.realm.Realm -import io.realm.RealmObjectChangeListener +import io.realm.RealmQuery import io.realm.RealmResults /** @@ -31,7 +35,8 @@ import io.realm.RealmResults * When an hidden event has read marker, we want to transfer it on the first older displayed event. * It has to be used in [DefaultTimeline] and we should call the [start] and [dispose] methods to properly handle realm subscription. */ -internal class TimelineHiddenReadMarker constructor(private val roomId: String) { +internal class TimelineHiddenReadMarker constructor(private val roomId: String, + private val settings: TimelineSettings) { interface Delegate { fun rebuildEvent(eventId: String, hasReadMarker: Boolean): Boolean @@ -39,39 +44,42 @@ internal class TimelineHiddenReadMarker constructor(private val roomId: String) } private var previousDisplayedEventId: String? = null - private var readMarkerEntity: ReadMarkerEntity? = null + private var hiddenReadMarker: RealmResults? = null private lateinit var liveEvents: RealmResults private lateinit var delegate: Delegate - private val readMarkerListener = RealmObjectChangeListener { readMarker, _ -> - if (!readMarker.isLoaded || !readMarker.isValid) { - return@RealmObjectChangeListener + private val readMarkerListener = OrderedRealmCollectionChangeListener> { readMarkers, changeSet -> + if (!readMarkers.isLoaded || !readMarkers.isValid) { + return@OrderedRealmCollectionChangeListener } var hasChange = false - previousDisplayedEventId?.also { - hasChange = delegate.rebuildEvent(it, false) - previousDisplayedEventId = null - } - val isEventHidden = liveEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, readMarker.eventId).findFirst() == null - if (isEventHidden) { - val hiddenEvent = readMarker.timelineEvent?.firstOrNull() - ?: return@RealmObjectChangeListener - val displayIndex = hiddenEvent.root?.displayIndex - if (displayIndex != null) { - // Then we are looking for the first displayable event after the hidden one - val firstDisplayedEvent = liveEvents.where() - .lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex) - .findFirst() - - // If we find one, we should rebuild this one with marker - if (firstDisplayedEvent != null) { - previousDisplayedEventId = firstDisplayedEvent.eventId - hasChange = delegate.rebuildEvent(firstDisplayedEvent.eventId, true) - } + if (changeSet.deletions.isNotEmpty()) { + previousDisplayedEventId?.also { + hasChange = delegate.rebuildEvent(it, false) + previousDisplayedEventId = null } } - if (hasChange) delegate.onReadMarkerUpdated() + val readMarker = readMarkers.firstOrNull() ?: return@OrderedRealmCollectionChangeListener + val hiddenEvent = readMarker.timelineEvent?.firstOrNull() + ?: return@OrderedRealmCollectionChangeListener + + val displayIndex = hiddenEvent.root?.displayIndex + if (displayIndex != null) { + // Then we are looking for the first displayable event after the hidden one + val firstDisplayedEvent = liveEvents.where() + .lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex) + .findFirst() + + // If we find one, we should rebuild this one with marker + if (firstDisplayedEvent != null) { + previousDisplayedEventId = firstDisplayedEvent.eventId + hasChange = delegate.rebuildEvent(firstDisplayedEvent.eventId, true) + } + } + if (hasChange) { + delegate.onReadMarkerUpdated() + } } @@ -83,8 +91,10 @@ internal class TimelineHiddenReadMarker constructor(private val roomId: String) this.delegate = delegate // We are looking for read receipts set on hidden events. // We only accept those with a timelineEvent (so coming from pagination/sync). - readMarkerEntity = ReadMarkerEntity.where(realm, roomId = roomId) - .findFirstAsync() + hiddenReadMarker = ReadMarkerEntity.where(realm, roomId = roomId) + .isNotEmpty(ReadMarkerEntityFields.TIMELINE_EVENT) + .filterReceiptsWithSettings() + .findAllAsync() .also { it.addChangeListener(readMarkerListener) } } @@ -93,7 +103,26 @@ internal class TimelineHiddenReadMarker constructor(private val roomId: String) * Dispose the realm query subscription. Has to be called on an HandlerThread */ fun dispose() { - this.readMarkerEntity?.removeAllChangeListeners() + this.hiddenReadMarker?.removeAllChangeListeners() } + /** + * We are looking for readMarker related to filtered events. So, it's the opposite of [DefaultTimeline.filterEventsWithSettings] method. + */ + private fun RealmQuery.filterReceiptsWithSettings(): RealmQuery { + beginGroup() + if (settings.filterTypes) { + not().`in`("${ReadMarkerEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.TYPE}", settings.allowedTypes.toTypedArray()) + } + if (settings.filterTypes && settings.filterEdits) { + or() + } + if (settings.filterEdits) { + like("${ReadMarkerEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", FilterContent.EDIT_TYPE) + } + endGroup() + return this + } + + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt index 99fbc5750d..fdbaa2ab1b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt @@ -16,10 +16,12 @@ package im.vector.matrix.android.internal.session.sync +import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.session.room.read.FullyReadContent import im.vector.matrix.android.internal.database.model.ReadMarkerEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity +import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields import im.vector.matrix.android.internal.database.query.getOrCreate import im.vector.matrix.android.internal.database.query.where import io.realm.Realm @@ -37,15 +39,14 @@ internal class RoomFullyReadHandler @Inject constructor() { RoomSummaryEntity.getOrCreate(realm, roomId).apply { readMarkerId = content.eventId } - val readMarkerEntity = ReadMarkerEntity.getOrCreate(realm, roomId) - // Remove the old marker if any - if (readMarkerEntity.eventId.isNotEmpty()) { - val oldReadMarkerEvent = TimelineEventEntity.where(realm, eventId = readMarkerEntity.eventId).findFirst() - oldReadMarkerEvent?.readMarker = null + // Remove the old markers if any + val oldReadMarkerEvents = TimelineEventEntity.where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.BOTH).isNotNull(TimelineEventEntityFields.READ_MARKER.`$`).findAll() + oldReadMarkerEvents.forEach { it.readMarker = null } + val readMarkerEntity = ReadMarkerEntity.getOrCreate(realm, roomId).apply { + this.eventId = content.eventId } - readMarkerEntity.eventId = content.eventId // Attach to timelineEvent if known - val timelineEventEntity = TimelineEventEntity.where(realm, eventId = content.eventId).findFirst() + val timelineEventEntity = TimelineEventEntity.where(realm, roomId = roomId, eventId = content.eventId).findFirst() timelineEventEntity?.readMarker = readMarkerEntity } diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt index 398d525217..ed81f82e72 100644 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt @@ -21,16 +21,21 @@ package im.vector.riotx.core.ui.views import android.content.Context import android.util.AttributeSet import android.view.View +import android.view.ViewGroup import android.widget.LinearLayout import android.widget.RelativeLayout import androidx.core.content.ContextCompat import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams import butterknife.ButterKnife +import com.airbnb.epoxy.VisibilityState +import com.google.android.material.internal.ViewUtils.dpToPx import im.vector.riotx.R import im.vector.riotx.features.themes.ThemeUtils import kotlinx.android.synthetic.main.view_jump_to_read_marker.view.* import me.gujun.android.span.span import me.saket.bettermovementmethod.BetterLinkMovementMethod +import timber.log.Timber class JumpToReadMarkerView @JvmOverloads constructor( context: Context, @@ -49,26 +54,34 @@ class JumpToReadMarkerView @JvmOverloads constructor( setupView() } + private var readMarkerId: String? = null + private fun setupView() { - LinearLayout.inflate(context, R.layout.view_jump_to_read_marker, this) + inflate(context, R.layout.view_jump_to_read_marker, this) setBackgroundColor(ContextCompat.getColor(context, R.color.notification_accent_color)) jumpToReadMarkerLabelView.movementMethod = BetterLinkMovementMethod.getInstance() isClickable = true + jumpToReadMarkerLabelView.text = span(resources.getString(R.string.room_jump_to_first_unread)) { + textDecorationLine = "underline" + onClick = { + readMarkerId?.also { + callback?.onJumpToReadMarkerClicked(it) + } + } + } closeJumpToReadMarkerView.setOnClickListener { - visibility = View.GONE + visibility = View.INVISIBLE callback?.onClearReadMarkerClicked() } } fun render(show: Boolean, readMarkerId: String?) { - isVisible = show - if (readMarkerId != null) { - jumpToReadMarkerLabelView.text = span(resources.getString(R.string.room_jump_to_first_unread)) { - textDecorationLine = "underline" - onClick = { callback?.onJumpToReadMarkerClicked(readMarkerId) } - } + this.readMarkerId = readMarkerId + visibility = if(show){ + View.VISIBLE + }else { + View.INVISIBLE } - } diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt index 19dad458a5..9e9a147719 100644 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt @@ -45,7 +45,6 @@ class ReadMarkerView @JvmOverloads constructor( private var callbackDispatcherJob: Job? = null fun bindView(eventId: String?, hasReadMarker: Boolean, displayReadMarker: Boolean, readMarkerCallback: Callback) { - Timber.v("Bind event $eventId - hasReadMarker: $hasReadMarker - displayReadMarker: $displayReadMarker") this.eventId = eventId this.callback = readMarkerCallback if (displayReadMarker) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt index 85ad6201d3..7364c254a8 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt @@ -19,6 +19,7 @@ package im.vector.riotx.features.home.room.detail import androidx.recyclerview.widget.LinearLayoutManager import im.vector.riotx.core.di.ScreenScope +import im.vector.riotx.core.utils.createBackgroundHandler import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import timber.log.Timber import javax.inject.Inject @@ -31,6 +32,7 @@ class ReadMarkerHelper @Inject constructor() { var callback: Callback? = null private var onReadMarkerLongDisplayed = false + private var jumpToReadMarkerVisible = false private var readMarkerVisible: Boolean = true private var state: RoomDetailViewState? = null @@ -75,23 +77,20 @@ class ReadMarkerHelper @Inject constructor() { val nonNullState = this.state ?: return val lastVisibleItem = layoutManager.findLastVisibleItemPosition() val readMarkerId = nonNullState.asyncRoomSummary()?.readMarkerId - if (readMarkerId == null) { - callback?.onJumpToReadMarkerVisibilityUpdate(false, null) - } - val positionOfReadMarker = timelineEventController.searchPositionOfEvent(readMarkerId) - if (positionOfReadMarker == null) { - if (nonNullState.timeline?.isLive == true && lastVisibleItem > 0) { - callback?.onJumpToReadMarkerVisibilityUpdate(true, readMarkerId) - } else { - callback?.onJumpToReadMarkerVisibilityUpdate(false, readMarkerId) - } + val newJumpToReadMarkerVisible = if (readMarkerId == null) { + false } else { - if (positionOfReadMarker > lastVisibleItem) { - callback?.onJumpToReadMarkerVisibilityUpdate(true, readMarkerId) + val positionOfReadMarker = timelineEventController.searchPositionOfEvent(readMarkerId) + if (positionOfReadMarker == null) { + nonNullState.timeline?.isLive == true && lastVisibleItem > 0 } else { - callback?.onJumpToReadMarkerVisibilityUpdate(false, readMarkerId) + positionOfReadMarker > lastVisibleItem } } + if (newJumpToReadMarkerVisible != jumpToReadMarkerVisible) { + jumpToReadMarkerVisible = newJumpToReadMarkerVisible + callback?.onJumpToReadMarkerVisibilityUpdate(jumpToReadMarkerVisible, readMarkerId) + } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index aadfbb9fcb..f490bf66ab 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -157,7 +157,7 @@ class RoomDetailFragment : } } - /** + /**x * Sanitize the display name. * * @param displayName the display name to sanitize @@ -373,22 +373,22 @@ class RoomDetailFragment : if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { val parser = Parser.builder().build() val document = parser.parse(messageContent.formattedBody - ?: messageContent.body) + ?: messageContent.body) formattedBody = eventHtmlRenderer.render(document) } composerLayout.composerRelatedMessageContent.text = formattedBody - ?: nonFormattedBody + ?: nonFormattedBody updateComposerText(defaultContent) composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes)) avatarRenderer.render(event.senderAvatar, event.root.senderId - ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) + ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) avatarRenderer.render(event.senderAvatar, - event.root.senderId ?: "", - event.senderName, - composerLayout.composerRelatedMessageAvatar) + event.root.senderId ?: "", + event.senderName, + composerLayout.composerRelatedMessageAvatar) composerLayout.expand { //need to do it here also when not using quick reply focusComposerAndShowKeyboard() @@ -426,9 +426,9 @@ class RoomDetailFragment : REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data) REACTION_SELECT_REQUEST_CODE -> { val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID) - ?: return + ?: return val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT) - ?: return + ?: return //TODO check if already reacted with that? roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId)) } @@ -487,26 +487,26 @@ class RoomDetailFragment : if (vectorPreferences.swipeToReplyIsEnabled()) { val swipeCallback = RoomMessageTouchHelperCallback(requireContext(), - R.drawable.ic_reply, - object : RoomMessageTouchHelperCallback.QuickReplayHandler { - override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { - (model as? AbsMessageItem)?.attributes?.informationData?.let { - val eventId = it.eventId - roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId, composerLayout.composerEditText.text.toString())) - } - } + R.drawable.ic_reply, + object : RoomMessageTouchHelperCallback.QuickReplayHandler { + override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { + (model as? AbsMessageItem)?.attributes?.informationData?.let { + val eventId = it.eventId + roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId, composerLayout.composerEditText.text.toString())) + } + } - override fun canSwipeModel(model: EpoxyModel<*>): Boolean { - return when (model) { - is MessageFileItem, - is MessageImageVideoItem, - is MessageTextItem -> { - return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED - } - else -> false - } - } - }) + override fun canSwipeModel(model: EpoxyModel<*>): Boolean { + return when (model) { + is MessageFileItem, + is MessageImageVideoItem, + is MessageTextItem -> { + return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED + } + else -> false + } + } + }) val touchHelper = ItemTouchHelper(swipeCallback) touchHelper.attachToRecyclerView(recyclerView) } @@ -948,12 +948,19 @@ class RoomDetailFragment : .show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS") } - override fun onReadMarkerLongBound(isDisplayed: Boolean) { - if (isDisplayed) { - readMarkerHelper.onReadMarkerLongDisplayed() + override fun onReadMarkerLongBound(readMarkerId: String, isDisplayed: Boolean) { + readMarkerHelper.onReadMarkerLongDisplayed() + val readMarkerIndex = timelineEventController.searchPositionOfEvent(readMarkerId) ?: return + val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition() + if (readMarkerIndex > lastVisibleItemPosition) { + return + } + val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() + val firstVisibleItem = timelineEventController.adapter.getModelAtPosition(firstVisibleItemPosition) + val nextReadMarkerId = when (firstVisibleItem) { + is BaseEventItem -> firstVisibleItem.getEventId() + else -> null } - val firstVisibleItem = layoutManager.findFirstVisibleItemPosition() - val nextReadMarkerId = timelineEventController.searchEventIdAtPosition(firstVisibleItem) if (nextReadMarkerId != null) { roomDetailViewModel.process(RoomDetailActions.SetReadMarkerAction(nextReadMarkerId)) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 72c6d67a7d..a863337cdd 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -58,7 +58,6 @@ import im.vector.riotx.core.utils.LiveEvent import im.vector.riotx.core.utils.subscribeLogError import im.vector.riotx.features.command.CommandParser import im.vector.riotx.features.command.ParsedCommand -import im.vector.riotx.features.home.room.detail.timeline.TimelineLayoutManagerHolder import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents import im.vector.riotx.features.settings.VectorPreferences import io.reactivex.Observable @@ -72,7 +71,6 @@ import java.util.concurrent.TimeUnit class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: RoomDetailViewState, - private val timelineLayoutManagerHolder: TimelineLayoutManagerHolder, private val userPreferencesProvider: UserPreferencesProvider, private val vectorPreferences: VectorPreferences, private val session: Session @@ -117,8 +115,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro observeEventDisplayedActions() observeSummaryState() observeDrafts() - observeReadMarkerVisibility() - observeOwnState() room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear() timeline.start() setState { copy(timeline = this@RoomDetailViewModel.timeline) } @@ -182,23 +178,23 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro copy( // Create a sendMode from a draft and retrieve the TimelineEvent sendMode = when (draft) { - is UserDraft.REGULAR -> SendMode.REGULAR(draft.text) - is UserDraft.QUOTE -> { - room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> - SendMode.QUOTE(timelineEvent, draft.text) - } - } - is UserDraft.REPLY -> { - room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> - SendMode.REPLY(timelineEvent, draft.text) - } - } - is UserDraft.EDIT -> { - room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> - SendMode.EDIT(timelineEvent, draft.text) - } - } - } ?: SendMode.REGULAR("") + is UserDraft.REGULAR -> SendMode.REGULAR(draft.text) + is UserDraft.QUOTE -> { + room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> + SendMode.QUOTE(timelineEvent, draft.text) + } + } + is UserDraft.REPLY -> { + room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> + SendMode.REPLY(timelineEvent, draft.text) + } + } + is UserDraft.EDIT -> { + room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> + SendMode.EDIT(timelineEvent, draft.text) + } + } + } ?: SendMode.REGULAR("") ) } } @@ -207,7 +203,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private fun handleTombstoneEvent(action: RoomDetailActions.HandleTombstoneEvent) { val tombstoneContent = action.event.getClearContent().toModel() - ?: return + ?: return val roomId = tombstoneContent.replacementRoom ?: "" val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN @@ -342,7 +338,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is SendMode.EDIT -> { //is original event a reply? val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel()?.relatesTo?.inReplyTo?.eventId - ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId + ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId if (inReplyTo != null) { //TODO check if same content? room.getTimeLineEvent(inReplyTo)?.let { @@ -351,13 +347,13 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } else { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val existingBody = messageContent?.body ?: "" if (existingBody != action.text) { room.editTextMessage(state.sendMode.timelineEvent.root.eventId ?: "", - messageContent?.type ?: MessageType.MSGTYPE_TEXT, - action.text, - action.autoMarkdown) + messageContent?.type ?: MessageType.MSGTYPE_TEXT, + action.text, + action.autoMarkdown) } else { Timber.w("Same message content, do not send edition") } @@ -368,7 +364,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is SendMode.QUOTE -> { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val textMsg = messageContent?.body val finalText = legacyRiotQuoteText(textMsg, action.text) @@ -681,7 +677,15 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } private fun handleSetReadMarkerAction(action: RoomDetailActions.SetReadMarkerAction) = withState { state -> - room.setReadMarker(action.eventId, callback = object : MatrixCallback {}) + var readMarkerId = action.eventId + val indexOfEvent = timeline.getIndexOfEvent(readMarkerId) + // force to set the read marker on the next event + if (indexOfEvent != null) { + timeline.getTimelineEventAtIndex(indexOfEvent - 1)?.root?.eventId?.also { eventIdOfNext -> + readMarkerId = eventIdOfNext + } + } + room.setReadMarker(readMarkerId, callback = object : MatrixCallback {}) } private fun handleMarkAllAsRead() { @@ -724,22 +728,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } - private fun observeReadMarkerVisibility() { - Observable - .combineLatest( - room.rx().liveReadMarker(), - room.rx().liveReadReceipt(), - BiFunction, Optional, Boolean> { readMarker, readReceipt -> - readMarker.getOrNull() != readReceipt.getOrNull() - } - ) - .subscribe { - setState { copy(readMarkerVisible = it) } - } - .disposeOnClear() - } - - override fun onCleared() { timeline.dispose() super.onCleared() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt index 2609aed2e3..7549ffbb23 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt @@ -52,8 +52,7 @@ data class RoomDetailViewState( val tombstoneEvent: Event? = null, val tombstoneEventHandling: Async = Uninitialized, val syncState: SyncState = SyncState.IDLE, - val highlightedEventId: String? = null, - val readMarkerVisible: Boolean = false + val highlightedEventId: String? = null ) : MvRxState { constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index 525fb6cd6a..3dda4b333c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -82,7 +82,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec interface ReadReceiptsCallback { fun onReadReceiptsClicked(readReceipts: List) - fun onReadMarkerLongBound(isDisplayed: Boolean) + fun onReadMarkerLongBound(readMarkerId: String, isDisplayed: Boolean) } interface UrlClickCallback { @@ -161,7 +161,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec synchronized(modelCache) { for (i in 0 until modelCache.size) { if (modelCache[i]?.eventId == viewState.highlightedEventId - || modelCache[i]?.eventId == eventIdToHighlight) { + || modelCache[i]?.eventId == eventIdToHighlight) { modelCache[i] = null } } @@ -232,8 +232,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec // Should be build if not cached or if cached but contains mergedHeader or formattedDay // We then are sure we always have items up to date. if (modelCache[position] == null - || modelCache[position]?.mergedHeaderModel != null - || modelCache[position]?.formattedDayModel != null) { + || modelCache[position]?.mergedHeaderModel != null + || modelCache[position]?.formattedDayModel != null) { modelCache[position] = buildItemModels(position, currentSnapshot) } } @@ -258,18 +258,24 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec val date = event.root.localDateTime() val nextDate = nextEvent?.root?.localDateTime() val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate() - val eventModel = timelineItemFactory.create(event, nextEvent, eventIdToHighlight, readMarkerVisible, callback).also { + // Don't show read marker if it's on first item + val showReadMarker = if (currentPosition == 0 && event.hasReadMarker) { + false + } else { + readMarkerVisible + } + val eventModel = timelineItemFactory.create(event, nextEvent, eventIdToHighlight, showReadMarker, callback).also { it.id(event.localId) it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) } val mergedHeaderModel = mergedHeaderItemFactory.create(event, - nextEvent = nextEvent, - items = items, - addDaySeparator = addDaySeparator, - readMarkerVisible = readMarkerVisible, - currentPosition = currentPosition, - eventIdToHighlight = eventIdToHighlight, - callback = callback + nextEvent = nextEvent, + items = items, + addDaySeparator = addDaySeparator, + readMarkerVisible = readMarkerVisible, + currentPosition = currentPosition, + eventIdToHighlight = eventIdToHighlight, + callback = callback ) { requestModelBuild() } @@ -317,40 +323,23 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec realPosition++ } for (i in 0 until modelCache.size) { - val itemCache = modelCache[i] - if (itemCache?.eventId == eventId) { + val itemCache = modelCache[i] ?: continue + if (itemCache.eventId == eventId) { return realPosition } - if (itemCache?.eventModel != null) { + if (itemCache.eventModel != null && !mergedHeaderItemFactory.isCollapsed(itemCache.localId)) { realPosition++ } - if (itemCache?.mergedHeaderModel != null) { + if (itemCache.mergedHeaderModel != null) { realPosition++ } - if (itemCache?.formattedDayModel != null) { + if (itemCache.formattedDayModel != null) { realPosition++ } } return null } - fun searchEventIdAtPosition(position: Int): String? = synchronized(modelCache) { - var offsetValue = 0 - for (i in 0 until position) { - val itemCache = modelCache[i] - if (itemCache?.eventModel == null) { - offsetValue-- - } - if (itemCache?.mergedHeaderModel != null) { - offsetValue++ - } - if (itemCache?.formattedDayModel != null) { - offsetValue++ - } - } - return modelCache.getOrNull(position - offsetValue)?.eventId - } - fun isLoadingForward() = showingForwardLoader private data class CacheItemData( diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineLayoutManagerHolder.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineLayoutManagerHolder.kt deleted file mode 100644 index 429515798a..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineLayoutManagerHolder.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - - * Copyright 2019 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.riotx.features.home.room.detail.timeline - -import androidx.recyclerview.widget.LinearLayoutManager -import im.vector.riotx.core.di.ScreenScope -import javax.inject.Inject - -@ScreenScope -class TimelineLayoutManagerHolder @Inject constructor() { - - lateinit var layoutManager: LinearLayoutManager - -} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index ddf93410a1..b5e5f50e03 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -71,7 +71,8 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act userId = mergedEvent.root.senderId ?: "", avatarUrl = senderAvatar, memberName = senderName ?: "", - eventId = mergedEvent.localId + localId = mergedEvent.localId, + eventId = mergedEvent.root.eventId ?: "" ) mergedData.add(data) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt index 5bf8ba6e06..c7d75998a2 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -29,6 +29,7 @@ import androidx.core.view.children import androidx.core.view.isGone import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.VisibilityState import im.vector.matrix.android.api.session.room.send.SendState import im.vector.riotx.R import im.vector.riotx.core.resources.ColorProvider @@ -58,7 +59,7 @@ abstract class AbsMessageItem : BaseEventItem() { private val _readMarkerCallback = object : ReadMarkerView.Callback { override fun onReadMarkerLongBound(isDisplayed: Boolean) { - attributes.readReceiptsCallback?.onReadMarkerLongBound(isDisplayed) + attributes.readReceiptsCallback?.onReadMarkerLongBound(attributes.informationData.eventId, isDisplayed) } } @@ -157,6 +158,10 @@ abstract class AbsMessageItem : BaseEventItem() { return true } + override fun getEventId(): String? { + return attributes.informationData.eventId + } + protected open fun renderSendState(root: View, textView: TextView?, failureIndicator: ImageView? = null) { root.isClickable = attributes.informationData.sendState.isSent() val state = if (attributes.informationData.hasPendingEdits) SendState.UNSENT else attributes.informationData.sendState diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt index 5b0b64fea7..7727b07cd8 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt @@ -44,10 +44,13 @@ abstract class BaseEventItem : VectorEpoxyModel override fun bind(holder: H) { super.bind(holder) + holder holder.leftGuideline.setGuidelineBegin(leftGuideline) holder.checkableBackground.isChecked = highlighted } + abstract fun getEventId(): String? + abstract class BaseHolder(@IdRes val stubId: Int) : VectorEpoxyHolder() { val leftGuideline by bind(R.id.messageStartGuideline) val checkableBackground by bind(R.id.messageSelectedBackground) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/DefaultItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/DefaultItem.kt index a5ffb9a2ae..0e2d87512b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/DefaultItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/DefaultItem.kt @@ -55,6 +55,10 @@ abstract class DefaultItem : BaseEventItem() { holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener) } + override fun getEventId(): String? { + return informationData.eventId + } + override fun getViewType() = STUB_ID class Holder : BaseHolder(STUB_ID) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt index da19a88133..727a585d71 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt @@ -42,7 +42,7 @@ abstract class MergedHeaderItem : BaseEventItem() { private val _readMarkerCallback = object : ReadMarkerView.Callback { override fun onReadMarkerLongBound(isDisplayed: Boolean) { - attributes.readReceiptsCallback?.onReadMarkerLongBound(isDisplayed) + attributes.readReceiptsCallback?.onReadMarkerLongBound(attributes.readMarkerId ?: "", isDisplayed) } } @@ -89,8 +89,14 @@ abstract class MergedHeaderItem : BaseEventItem() { super.unbind(holder) } + + override fun getEventId(): String? { + return attributes.mergeData.firstOrNull()?.eventId + } + data class Data( - val eventId: Long, + val localId: Long, + val eventId: String, val userId: String, val memberName: String, val avatarUrl: String? diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt index 559b02aa61..c398970e8e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt @@ -40,7 +40,7 @@ abstract class NoticeItem : BaseEventItem() { private val _readMarkerCallback = object : ReadMarkerView.Callback { override fun onReadMarkerLongBound(isDisplayed: Boolean) { - attributes.readReceiptsCallback?.onReadMarkerLongBound(isDisplayed) + attributes.readReceiptsCallback?.onReadMarkerLongBound(attributes.informationData.eventId, isDisplayed) } } @@ -69,6 +69,11 @@ abstract class NoticeItem : BaseEventItem() { super.unbind(holder) } + + override fun getEventId(): String? { + return attributes.informationData.eventId + } + override fun getViewType() = STUB_ID class Holder : BaseHolder(STUB_ID) { diff --git a/vector/src/main/res/layout/fragment_room_detail.xml b/vector/src/main/res/layout/fragment_room_detail.xml index f95afbd647..a9385f4eeb 100644 --- a/vector/src/main/res/layout/fragment_room_detail.xml +++ b/vector/src/main/res/layout/fragment_room_detail.xml @@ -124,10 +124,10 @@ android:id="@+id/jumpToReadMarkerView" android:layout_width="0dp" android:layout_height="wrap_content" - android:visibility="gone" + android:visibility="invisible" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/syncProgressBarWrap" /> + app:layout_constraintTop_toBottomOf="@id/syncStateView" /> Date: Thu, 26 Sep 2019 10:08:44 +0200 Subject: [PATCH 037/197] Update after Dominaezzz's review --- .../DefaultGetHomeServerCapabilitiesTask.kt | 3 ++- .../DefaultHomeServerCapabilitiesService.kt | 11 +++++++++-- .../features/home/room/detail/RoomDetailFragment.kt | 4 ++-- .../features/home/room/detail/RoomDetailViewModel.kt | 8 ++++---- vector/src/main/res/values/strings_riotX.xml | 2 +- 5 files changed, 18 insertions(+), 10 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt index d3cb945adb..defcfbaa81 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt @@ -22,6 +22,7 @@ import im.vector.matrix.android.internal.database.model.HomeServerCapabilitiesEn import im.vector.matrix.android.internal.database.query.getOrCreate import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task +import im.vector.matrix.android.internal.util.awaitTransaction import java.util.* import javax.inject.Inject @@ -35,7 +36,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( override suspend fun execute(params: Unit) { var doRequest = false - monarchy.doWithRealm { realm -> + monarchy.awaitTransaction { realm -> val homeServerCapabilitiesEntity = HomeServerCapabilitiesEntity.getOrCreate(realm) doRequest = homeServerCapabilitiesEntity.lastUpdatedTimestamp + MIN_DELAY_BETWEEN_TWO_REQUEST_MILLIS < Date().time diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt index 6f6416ba4a..0af849af2e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt @@ -32,6 +32,13 @@ internal class DefaultHomeServerCapabilitiesService @Inject constructor(private entity = HomeServerCapabilitiesEntity.getOrCreate(realm) } - return entity?.let { HomeServerCapabilitiesMapper.map(it) } ?: HomeServerCapabilities(HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN) + return with(entity) { + if (this != null) { + HomeServerCapabilitiesMapper.map(this) + } else { + // Should not happen + HomeServerCapabilities(HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN) + } + } } -} \ No newline at end of file +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 7934e0ccae..7c7b90cd4b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -264,8 +264,8 @@ class RoomDetailFragment : .setTitle(R.string.dialog_title_error) .setMessage(getString(R.string.error_file_too_big, error.filename, - Formatter.formatFileSize(requireContext(), error.homeServerLimitInBytes), - Formatter.formatFileSize(requireContext(), error.fileSizeInBytes) + Formatter.formatFileSize(requireContext(), error.fileSizeInBytes), + Formatter.formatFileSize(requireContext(), error.homeServerLimitInBytes) )) .setPositiveButton(R.string.ok, null) .show() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index d31a25a8d9..f1d2431351 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -480,10 +480,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro // Unknown limitation room.sendMedias(attachments) } else { - attachments.find { it.size > maxUploadFileSize } - ?.let { - _fileTooBigEvent.postValue(LiveEvent(FileTooBigError(it.name ?: it.path, it.size, maxUploadFileSize))) - } ?: run { room.sendMedias(attachments) } + when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) { + null -> room.sendMedias(attachments) + else -> _fileTooBigEvent.postValue(LiveEvent(FileTooBigError(tooBigFile.name ?: tooBigFile.path, tooBigFile.size, maxUploadFileSize))) + } } } diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 7d3312cf24..a48376e085 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -22,6 +22,6 @@ Create a new room Close keys backup banner - The file %1$s is too large to upload. The file size limit is %2$s but this file is %3$s. + "The file '%1$s' (%2$s) is too large to upload. The limit is %3$s." \ No newline at end of file From f02f16d9c55d7bf037abddcbed54ede96ea698c2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 26 Sep 2019 10:41:52 +0200 Subject: [PATCH 038/197] Use IEC units instead of SI units for file sizes --- tools/check/forbidden_strings_in_code.txt | 8 ++++- .../im/vector/riotx/core/utils/TextUtils.kt | 27 +++++++++++++++++ .../home/room/detail/RoomDetailFragment.kt | 29 ++++++++----------- .../helper/ContentUploadStateTrackerBinder.kt | 6 ++-- .../settings/VectorSettingsGeneralFragment.kt | 14 ++++----- 5 files changed, 56 insertions(+), 28 deletions(-) diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index 7686cb0b7c..7e00295a48 100644 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -149,4 +149,10 @@ android\.app\.AlertDialog new Gson\(\) ### Use matrixOneTimeWorkRequestBuilder -import androidx.work.OneTimeWorkRequestBuilder===1 \ No newline at end of file +import androidx.work.OneTimeWorkRequestBuilder===1 + +### Use TextUtils.formatFileSize +Formatter\.formatFileSize===1 + +### Use TextUtils.formatFileSize with short format param to true +Formatter\.formatShortFileSize===1 diff --git a/vector/src/main/java/im/vector/riotx/core/utils/TextUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/TextUtils.kt index 9d980e7f98..a7fa2cb350 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/TextUtils.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/TextUtils.kt @@ -16,6 +16,9 @@ package im.vector.riotx.core.utils +import android.content.Context +import android.os.Build +import android.text.format.Formatter import java.util.* object TextUtils { @@ -42,4 +45,28 @@ object TextUtils { return value.toString() } } + + /** + * Since Android O, the system considers that 1ko = 1000 bytes instead of 1024 bytes. We want to avoid that for the moment. + */ + fun formatFileSize(context: Context, sizeBytes: Long, useShortFormat: Boolean = false): String { + val normalizedSize = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) { + sizeBytes + } else { + // First convert the size + when { + sizeBytes < 1024 -> sizeBytes + sizeBytes < 1024 * 1024 -> sizeBytes * 1000 / 1024 + sizeBytes < 1024 * 1024 * 1024 -> sizeBytes * 1000 / 1024 * 1000 / 1024 + else -> sizeBytes * 1000 / 1024 * 1000 / 1024 * 1000 / 1024 + } + } + + return if (useShortFormat) { + Formatter.formatShortFileSize(context, normalizedSize) + } else { + Formatter.formatFileSize(context, normalizedSize) + + } + } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 7c7b90cd4b..ad9201c628 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -27,8 +27,6 @@ import android.os.Bundle import android.os.Parcelable import android.text.Editable import android.text.Spannable -import android.text.TextUtils -import android.text.format.Formatter import android.view.* import android.view.inputmethod.InputMethodManager import android.widget.TextView @@ -149,18 +147,15 @@ class RoomDetailFragment : * @param displayName the display name to sanitize * @return the sanitized display name */ - fun sanitizeDisplayname(displayName: String): String? { - // sanity checks - if (!TextUtils.isEmpty(displayName)) { - val ircPattern = " (IRC)" - - if (displayName.endsWith(ircPattern)) { - return displayName.substring(0, displayName.length - ircPattern.length) - } + private fun sanitizeDisplayName(displayName: String): String { + if (displayName.endsWith(ircPattern)) { + return displayName.substring(0, displayName.length - ircPattern.length) } return displayName } + + private const val ircPattern = " (IRC)" } private val roomDetailArgs: RoomDetailArgs by args() @@ -264,8 +259,8 @@ class RoomDetailFragment : .setTitle(R.string.dialog_title_error) .setMessage(getString(R.string.error_file_too_big, error.filename, - Formatter.formatFileSize(requireContext(), error.fileSizeInBytes), - Formatter.formatFileSize(requireContext(), error.homeServerLimitInBytes) + TextUtils.formatFileSize(requireContext(), error.fileSizeInBytes), + TextUtils.formatFileSize(requireContext(), error.homeServerLimitInBytes) )) .setPositiveButton(R.string.ok, null) .show() @@ -987,23 +982,23 @@ class RoomDetailFragment : // var vibrate = false val myDisplayName = session.getUser(session.myUserId)?.displayName - if (TextUtils.equals(myDisplayName, text)) { + if (myDisplayName == text) { // current user - if (TextUtils.isEmpty(composerLayout.composerEditText.text)) { + if (composerLayout.composerEditText.text.isBlank()) { composerLayout.composerEditText.append(Command.EMOTE.command + " ") composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length) // vibrate = true } } else { // another user - if (TextUtils.isEmpty(composerLayout.composerEditText.text)) { + if (composerLayout.composerEditText.text.isBlank()) { // Ensure displayName will not be interpreted as a Slash command if (text.startsWith("/")) { composerLayout.composerEditText.append("\\") } - composerLayout.composerEditText.append(sanitizeDisplayname(text)!! + ": ") + composerLayout.composerEditText.append(sanitizeDisplayName(text) + ": ") } else { - composerLayout.composerEditText.text.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayname(text)!! + " ") + composerLayout.composerEditText.text.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayName(text) + " ") } // vibrate = true diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt index 09b2e86a83..96cb7f0d8e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt @@ -16,7 +16,6 @@ package im.vector.riotx.features.home.room.detail.timeline.helper -import android.text.format.Formatter import android.view.View import android.view.ViewGroup import android.widget.ProgressBar @@ -28,6 +27,7 @@ import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.core.utils.TextUtils import im.vector.riotx.features.ui.getMessageTextColor import javax.inject.Inject @@ -126,8 +126,8 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup, progressBar?.isIndeterminate = false progressBar?.progress = percent.toInt() progressTextView?.text = progressLayout.context.getString(resId, - Formatter.formatShortFileSize(progressLayout.context, current), - Formatter.formatShortFileSize(progressLayout.context, total)) + TextUtils.formatFileSize(progressLayout.context, current, true), + TextUtils.formatFileSize(progressLayout.context, total, true)) progressTextView?.setTextColor(colorProvider.getMessageTextColor(SendState.SENDING)) } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt index 2bec8cf103..e51feb2363 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt @@ -20,7 +20,7 @@ import android.app.Activity import android.content.Context import android.content.Intent import android.text.Editable -import android.text.TextUtils +import android.util.Patterns import android.view.View import android.view.ViewGroup import android.view.inputmethod.InputMethodManager @@ -171,7 +171,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { MXSession.getApplicationSizeCaches(activity, object : SimpleApiCallback() { override fun onSuccess(size: Long) { if (null != activity) { - it.summary = android.text.format.Formatter.formatFileSize(activity, size) + it.summary = TextUtils.formatFileSize(activity, size) } } }) @@ -189,7 +189,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { val size = getSizeOfFiles(requireContext(), File(requireContext().cacheDir, DiskCache.Factory.DEFAULT_DISK_CACHE_DIR)) - it.summary = android.text.format.Formatter.formatFileSize(activity, size.toLong()) + it.summary = TextUtils.formatFileSize(requireContext(), size.toLong()) it.onPreferenceClickListener = Preference.OnPreferenceClickListener { GlobalScope.launch(Dispatchers.Main) { @@ -208,7 +208,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { File(requireContext().cacheDir, DiskCache.Factory.DEFAULT_DISK_CACHE_DIR)) } - it.summary = android.text.format.Formatter.formatFileSize(activity, newSize.toLong()) + it.summary = TextUtils.formatFileSize(requireContext(), newSize.toLong()) hideLoadingView() } @@ -534,7 +534,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { private fun addEmail(email: String) { // check first if the email syntax is valid // if email is null , then also its invalid email - if (TextUtils.isEmpty(email) || !android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches()) { + if (email.isBlank() || !Patterns.EMAIL_ADDRESS.matcher(email).matches()) { activity?.toast(R.string.auth_invalid_email) return } @@ -719,9 +719,9 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { val newPwd = newPasswordText.text.toString().trim() val newConfirmPwd = confirmNewPasswordText.text.toString().trim() - updateButton.isEnabled = oldPwd.isNotEmpty() && newPwd.isNotEmpty() && TextUtils.equals(newPwd, newConfirmPwd) + updateButton.isEnabled = oldPwd.isNotEmpty() && newPwd.isNotEmpty() && newPwd == newConfirmPwd - if (newPwd.isNotEmpty() && newConfirmPwd.isNotEmpty() && !TextUtils.equals(newPwd, newConfirmPwd)) { + if (newPwd.isNotEmpty() && newConfirmPwd.isNotEmpty() && newPwd != newConfirmPwd) { confirmNewPasswordTil.error = getString(R.string.passwords_do_not_match) } } From 810a97c63999c1f37ff10d28cde6d6c71de71f34 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 26 Sep 2019 11:14:13 +0200 Subject: [PATCH 039/197] Import string from Android-SDK (#355) --- .../src/main/res/values-en-rUS/strings.xml | 11 +++++++++++ tools/import_from_riot.sh | 1 + 2 files changed, 12 insertions(+) create mode 100644 matrix-sdk-android/src/main/res/values-en-rUS/strings.xml diff --git a/matrix-sdk-android/src/main/res/values-en-rUS/strings.xml b/matrix-sdk-android/src/main/res/values-en-rUS/strings.xml new file mode 100644 index 0000000000..09e9dfc111 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-en-rUS/strings.xml @@ -0,0 +1,11 @@ + + + + + + Wrench + Airplane + + + + diff --git a/tools/import_from_riot.sh b/tools/import_from_riot.sh index cdc4275095..4bc19ca86c 100755 --- a/tools/import_from_riot.sh +++ b/tools/import_from_riot.sh @@ -32,6 +32,7 @@ cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-da/strings.xml ./mat cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-de/strings.xml ./matrix-sdk-android/src/main/res/values-de/strings.xml cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-el/strings.xml ./matrix-sdk-android/src/main/res/values-el/strings.xml cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-eo/strings.xml ./matrix-sdk-android/src/main/res/values-eo/strings.xml +cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-en-rUS/strings.xml ./matrix-sdk-android/src/main/res/values-en-rUS/strings.xml cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-es/strings.xml ./matrix-sdk-android/src/main/res/values-es/strings.xml cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-es-rMX/strings.xml ./matrix-sdk-android/src/main/res/values-es-rMX/strings.xml cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-eu/strings.xml ./matrix-sdk-android/src/main/res/values-eu/strings.xml From 0ea878af8afdc608a897d0e97711578b015d80fb Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 26 Sep 2019 11:55:16 +0200 Subject: [PATCH 040/197] Timeline: fix some more issues --- .../session/room/timeline/DefaultTimeline.kt | 12 ++-- .../room/timeline/TimelineHiddenReadMarker.kt | 18 ++++-- .../timeline/TimelineHiddenReadReceipts.kt | 25 +++++--- .../session/sync/RoomFullyReadHandler.kt | 10 ++- .../home/room/detail/ReadMarkerHelper.kt | 4 +- .../home/room/detail/RoomDetailFragment.kt | 5 +- .../ScrollOnHighlightedEventCallback.kt | 6 +- .../timeline/TimelineEventController.kt | 64 +++++++++---------- .../detail/timeline/item/AbsMessageItem.kt | 5 +- .../detail/timeline/item/BaseEventItem.kt | 7 +- .../room/detail/timeline/item/DefaultItem.kt | 2 +- .../detail/timeline/item/MergedHeaderItem.kt | 4 +- .../room/detail/timeline/item/NoticeItem.kt | 4 +- 13 files changed, 93 insertions(+), 73 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index d95408bfb1..52ef816e99 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -198,9 +198,9 @@ internal class DefaultTimeline( .also { it.addChangeListener(relationsListener) } if (settings.buildReadReceipts) { - hiddenReadReceipts.start(realm, filteredEvents, this) + hiddenReadReceipts.start(realm, filteredEvents, nonFilteredEvents, this) } - hiddenReadMarker.start(realm, filteredEvents, this) + hiddenReadMarker.start(realm, filteredEvents, nonFilteredEvents, this) isReady.set(true) } } @@ -490,9 +490,9 @@ internal class DefaultTimeline( return } val params = PaginationTask.Params(roomId = roomId, - from = token, - direction = direction.toPaginationDirection(), - limit = limit) + from = token, + direction = direction.toPaginationDirection(), + limit = limit) Timber.v("Should fetch $limit items $direction") cancelableBag += paginationTask @@ -563,7 +563,7 @@ internal class DefaultTimeline( val timelineEvent = buildTimelineEvent(eventEntity) if (timelineEvent.isEncrypted() - && timelineEvent.root.mxDecryptionResult == null) { + && timelineEvent.root.mxDecryptionResult == null) { timelineEvent.root.eventId?.let { eventDecryptor.requestDecryption(it) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt index eebb98ca19..03d79c2e00 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt @@ -46,7 +46,8 @@ internal class TimelineHiddenReadMarker constructor(private val roomId: String, private var previousDisplayedEventId: String? = null private var hiddenReadMarker: RealmResults? = null - private lateinit var liveEvents: RealmResults + private lateinit var filteredEvents: RealmResults + private lateinit var nonFilteredEvents: RealmResults private lateinit var delegate: Delegate private val readMarkerListener = OrderedRealmCollectionChangeListener> { readMarkers, changeSet -> @@ -62,12 +63,13 @@ internal class TimelineHiddenReadMarker constructor(private val roomId: String, } val readMarker = readMarkers.firstOrNull() ?: return@OrderedRealmCollectionChangeListener val hiddenEvent = readMarker.timelineEvent?.firstOrNull() - ?: return@OrderedRealmCollectionChangeListener + ?: return@OrderedRealmCollectionChangeListener + val isLoaded = nonFilteredEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, hiddenEvent.eventId).findFirst() != null val displayIndex = hiddenEvent.root?.displayIndex - if (displayIndex != null) { + if (isLoaded && displayIndex != null) { // Then we are looking for the first displayable event after the hidden one - val firstDisplayedEvent = liveEvents.where() + val firstDisplayedEvent = filteredEvents.where() .lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex) .findFirst() @@ -86,8 +88,12 @@ internal class TimelineHiddenReadMarker constructor(private val roomId: String, /** * Start the realm query subscription. Has to be called on an HandlerThread */ - fun start(realm: Realm, liveEvents: RealmResults, delegate: Delegate) { - this.liveEvents = liveEvents + fun start(realm: Realm, + filteredEvents: RealmResults, + nonFilteredEvents: RealmResults, + delegate: Delegate) { + this.filteredEvents = filteredEvents + this.nonFilteredEvents = nonFilteredEvents this.delegate = delegate // We are looking for read receipts set on hidden events. // We only accept those with a timelineEvent (so coming from pagination/sync). diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt index f932e6f3c0..0c538f794e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt @@ -49,7 +49,8 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu private val correctedReadReceiptsByEvent = HashMap>() private lateinit var hiddenReadReceipts: RealmResults - private lateinit var liveEvents: RealmResults + private lateinit var nonFilteredEvents: RealmResults + private lateinit var filteredEvents: RealmResults private lateinit var delegate: Delegate private val hiddenReadReceiptsListener = OrderedRealmCollectionChangeListener> { collection, changeSet -> @@ -60,7 +61,7 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu // Deletion here means we don't have any readReceipts for the given hidden events changeSet.deletions.forEach { val eventId = correctedReadReceiptsEventByIndex.get(it, "") - val timelineEvent = liveEvents.where() + val timelineEvent = filteredEvents.where() .equalTo(TimelineEventEntityFields.EVENT_ID, eventId) .findFirst() @@ -70,12 +71,14 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu } correctedReadReceiptsEventByIndex.clear() correctedReadReceiptsByEvent.clear() - hiddenReadReceipts.forEachIndexed { index, summary -> - val timelineEvent = summary?.timelineEvent?.firstOrNull() - val displayIndex = timelineEvent?.root?.displayIndex - if (displayIndex != null) { + for (index in 0 until hiddenReadReceipts.size) { + val summary = hiddenReadReceipts[index] ?: continue + val timelineEvent = summary.timelineEvent?.firstOrNull() ?: continue + val isLoaded = nonFilteredEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, timelineEvent.eventId).findFirst() != null + val displayIndex = timelineEvent.root?.displayIndex + if (isLoaded && displayIndex != null) { // Then we are looking for the first displayable event after the hidden one - val firstDisplayedEvent = liveEvents.where() + val firstDisplayedEvent = filteredEvents.where() .lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex) .findFirst() @@ -106,8 +109,12 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu /** * Start the realm query subscription. Has to be called on an HandlerThread */ - fun start(realm: Realm, liveEvents: RealmResults, delegate: Delegate) { - this.liveEvents = liveEvents + fun start(realm: Realm, + filteredEvents: RealmResults, + nonFilteredEvents: RealmResults, + delegate: Delegate) { + this.filteredEvents = filteredEvents + this.nonFilteredEvents = nonFilteredEvents this.delegate = delegate // We are looking for read receipts set on hidden events. // We only accept those with a timelineEvent (so coming from pagination/sync). diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt index fdbaa2ab1b..c052cf7146 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt @@ -40,14 +40,18 @@ internal class RoomFullyReadHandler @Inject constructor() { readMarkerId = content.eventId } // Remove the old markers if any - val oldReadMarkerEvents = TimelineEventEntity.where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.BOTH).isNotNull(TimelineEventEntityFields.READ_MARKER.`$`).findAll() + val oldReadMarkerEvents = TimelineEventEntity + .where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.BOTH) + .isNotNull(TimelineEventEntityFields.READ_MARKER.`$`) + .findAll() + oldReadMarkerEvents.forEach { it.readMarker = null } val readMarkerEntity = ReadMarkerEntity.getOrCreate(realm, roomId).apply { this.eventId = content.eventId } // Attach to timelineEvent if known - val timelineEventEntity = TimelineEventEntity.where(realm, roomId = roomId, eventId = content.eventId).findFirst() - timelineEventEntity?.readMarker = readMarkerEntity + val timelineEventEntities = TimelineEventEntity.where(realm, roomId = roomId, eventId = content.eventId).findAll() + timelineEventEntities.forEach { it.readMarker = readMarkerEntity } } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt index 7364c254a8..e23a9084a6 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt @@ -80,7 +80,9 @@ class ReadMarkerHelper @Inject constructor() { val newJumpToReadMarkerVisible = if (readMarkerId == null) { false } else { - val positionOfReadMarker = timelineEventController.searchPositionOfEvent(readMarkerId) + val correctedReadMarkerId = nonNullState.timeline?.getFirstDisplayableEventId(readMarkerId) + ?: readMarkerId + val positionOfReadMarker = timelineEventController.searchPositionOfEvent(correctedReadMarkerId) if (positionOfReadMarker == null) { nonNullState.timeline?.isLive == true && lastVisibleItem > 0 } else { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index f490bf66ab..e391c0d13f 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -250,6 +250,7 @@ class RoomDetailFragment : if (scrollPosition == null) { scrollOnHighlightedEventCallback.scheduleScrollTo(it) } else { + recyclerView.stopScroll() layoutManager.scrollToPosition(scrollPosition) } } @@ -445,7 +446,7 @@ class RoomDetailFragment : layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true) val stateRestorer = LayoutManagerStateRestorer(layoutManager).register() scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager, timelineEventController) - scrollOnHighlightedEventCallback = ScrollOnHighlightedEventCallback(layoutManager, timelineEventController) + scrollOnHighlightedEventCallback = ScrollOnHighlightedEventCallback(recyclerView, layoutManager, timelineEventController) recyclerView.layoutManager = layoutManager recyclerView.itemAnimator = null recyclerView.setHasFixedSize(true) @@ -958,7 +959,7 @@ class RoomDetailFragment : val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() val firstVisibleItem = timelineEventController.adapter.getModelAtPosition(firstVisibleItemPosition) val nextReadMarkerId = when (firstVisibleItem) { - is BaseEventItem -> firstVisibleItem.getEventId() + is BaseEventItem -> firstVisibleItem.getEventIds().firstOrNull() else -> null } if (nextReadMarkerId != null) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt index 08add3f0c7..52ebba817a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt @@ -17,9 +17,11 @@ package im.vector.riotx.features.home.room.detail import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.riotx.core.platform.DefaultListUpdateCallback import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController +import kotlinx.android.synthetic.main.fragment_room_detail.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -29,7 +31,8 @@ import java.util.concurrent.atomic.AtomicReference /** * This handles scrolling to an event which wasn't yet loaded when scheduled. */ -class ScrollOnHighlightedEventCallback(private val layoutManager: LinearLayoutManager, +class ScrollOnHighlightedEventCallback(private val recyclerView: RecyclerView, + private val layoutManager: LinearLayoutManager, private val timelineEventController: TimelineEventController) : DefaultListUpdateCallback { private val scheduledEventId = AtomicReference() @@ -56,6 +59,7 @@ class ScrollOnHighlightedEventCallback(private val layoutManager: LinearLayoutMa // Do not scroll it item is already visible if (positionToScroll !in firstVisibleItem..lastVisibleItem) { Timber.v("Scroll to $positionToScroll") + recyclerView.stopScroll() layoutManager.scrollToPosition(positionToScroll) } scheduledEventId.set(null) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index 3dda4b333c..519d1f71c7 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -50,7 +50,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private val mergedHeaderItemFactory: MergedHeaderItemFactory, @TimelineEventControllerHandler private val backgroundHandler: Handler -) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener { +) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor { interface Callback : BaseCallback, ReactionPillCallback, AvatarCallback, UrlClickCallback, ReadReceiptsCallback { fun onLoadMore(direction: Timeline.Direction) @@ -91,6 +91,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } private var showingForwardLoader = false + // Map eventId to adapter position + private val adapterPositionMapping = HashMap() private val modelCache = arrayListOf() private var currentSnapshot: List = emptyList() private var inSubmitList: Boolean = false @@ -98,6 +100,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec var callback: Callback? = null + private val listUpdateCallback = object : ListUpdateCallback { override fun onChanged(position: Int, count: Int, payload: Any?) { @@ -141,9 +144,22 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } init { + addInterceptor(this) requestModelBuild() } + // Update position when we are building new items + override fun intercept(models: MutableList>) { + adapterPositionMapping.clear() + models.forEachIndexed { index, epoxyModel -> + if (epoxyModel is BaseEventItem) { + epoxyModel.getEventIds().forEach { + adapterPositionMapping[it] = index + } + } + } + } + fun update(viewState: RoomDetailViewState, readMarkerVisible: Boolean) { if (timeline != viewState.timeline) { timeline = viewState.timeline @@ -161,7 +177,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec synchronized(modelCache) { for (i in 0 until modelCache.size) { if (modelCache[i]?.eventId == viewState.highlightedEventId - || modelCache[i]?.eventId == eventIdToHighlight) { + || modelCache[i]?.eventId == eventIdToHighlight) { modelCache[i] = null } } @@ -186,6 +202,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec timelineMediaSizeProvider.recyclerView = recyclerView } + override fun buildModels() { val timestamp = System.currentTimeMillis() showingForwardLoader = LoadingItem_() @@ -232,8 +249,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec // Should be build if not cached or if cached but contains mergedHeader or formattedDay // We then are sure we always have items up to date. if (modelCache[position] == null - || modelCache[position]?.mergedHeaderModel != null - || modelCache[position]?.formattedDayModel != null) { + || modelCache[position]?.mergedHeaderModel != null + || modelCache[position]?.formattedDayModel != null) { modelCache[position] = buildItemModels(position, currentSnapshot) } } @@ -269,13 +286,13 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) } val mergedHeaderModel = mergedHeaderItemFactory.create(event, - nextEvent = nextEvent, - items = items, - addDaySeparator = addDaySeparator, - readMarkerVisible = readMarkerVisible, - currentPosition = currentPosition, - eventIdToHighlight = eventIdToHighlight, - callback = callback + nextEvent = nextEvent, + items = items, + addDaySeparator = addDaySeparator, + readMarkerVisible = readMarkerVisible, + currentPosition = currentPosition, + eventIdToHighlight = eventIdToHighlight, + callback = callback ) { requestModelBuild() } @@ -314,30 +331,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } fun searchPositionOfEvent(eventId: String?): Int? = synchronized(modelCache) { - // Search in the cache - if (eventId == null) { - return null - } - var realPosition = 0 - if (showingForwardLoader) { - realPosition++ - } - for (i in 0 until modelCache.size) { - val itemCache = modelCache[i] ?: continue - if (itemCache.eventId == eventId) { - return realPosition - } - if (itemCache.eventModel != null && !mergedHeaderItemFactory.isCollapsed(itemCache.localId)) { - realPosition++ - } - if (itemCache.mergedHeaderModel != null) { - realPosition++ - } - if (itemCache.formattedDayModel != null) { - realPosition++ - } - } - return null + return adapterPositionMapping[eventId] } fun isLoadingForward() = showingForwardLoader diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt index c7d75998a2..64547bbcf7 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -29,7 +29,6 @@ import androidx.core.view.children import androidx.core.view.isGone import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute -import com.airbnb.epoxy.VisibilityState import im.vector.matrix.android.api.session.room.send.SendState import im.vector.riotx.R import im.vector.riotx.core.resources.ColorProvider @@ -158,8 +157,8 @@ abstract class AbsMessageItem : BaseEventItem() { return true } - override fun getEventId(): String? { - return attributes.informationData.eventId + override fun getEventIds(): List { + return listOf(attributes.informationData.eventId) } protected open fun renderSendState(root: View, textView: TextView?, failureIndicator: ImageView? = null) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt index 7727b07cd8..c6e813e878 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt @@ -44,12 +44,15 @@ abstract class BaseEventItem : VectorEpoxyModel override fun bind(holder: H) { super.bind(holder) - holder holder.leftGuideline.setGuidelineBegin(leftGuideline) holder.checkableBackground.isChecked = highlighted } - abstract fun getEventId(): String? + /** + * Returns the eventIds associated with the EventItem. + * Will generally get only one, but it handles the merging items. + */ + abstract fun getEventIds(): List abstract class BaseHolder(@IdRes val stubId: Int) : VectorEpoxyHolder() { val leftGuideline by bind(R.id.messageStartGuideline) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/DefaultItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/DefaultItem.kt index 0e2d87512b..cd1e39d37f 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/DefaultItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/DefaultItem.kt @@ -55,7 +55,7 @@ abstract class DefaultItem : BaseEventItem() { holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener) } - override fun getEventId(): String? { + override fun getEventIds(): List { return informationData.eventId } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt index 727a585d71..a15d9fa333 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt @@ -90,8 +90,8 @@ abstract class MergedHeaderItem : BaseEventItem() { } - override fun getEventId(): String? { - return attributes.mergeData.firstOrNull()?.eventId + override fun getEventIds(): List { + return attributes.mergeData.map { it.eventId } } data class Data( diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt index c398970e8e..2906eb58ba 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt @@ -70,8 +70,8 @@ abstract class NoticeItem : BaseEventItem() { } - override fun getEventId(): String? { - return attributes.informationData.eventId + override fun getEventIds(): List { + return listOf(attributes.informationData.eventId) } override fun getViewType() = STUB_ID From a060431aaf778caf3e5a0365f5c8d2886daa85c3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 26 Sep 2019 13:51:44 +0200 Subject: [PATCH 041/197] Fix my dirty code --- .../homeserver/HomeServerCapabilities.kt | 2 +- .../mapper/HomeServerCapabilitiesMapper.kt | 4 ++-- .../query/HomeServerCapabilitiesQueries.kt | 17 ++++++++--------- .../DefaultGetHomeServerCapabilitiesTask.kt | 15 +++++++-------- .../DefaultHomeServerCapabilitiesService.kt | 18 ++++++------------ 5 files changed, 24 insertions(+), 32 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilities.kt index 215516a6c1..9b01cdef3b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilities.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilities.kt @@ -20,7 +20,7 @@ data class HomeServerCapabilities( /** * Max size of file which can be uploaded to the homeserver in bytes. [MAX_UPLOAD_FILE_SIZE_UNKNOWN] if unknown or not retrieved yet */ - val maxUploadFileSize: Long + val maxUploadFileSize: Long = MAX_UPLOAD_FILE_SIZE_UNKNOWN ) { companion object { const val MAX_UPLOAD_FILE_SIZE_UNKNOWN = -1L diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/HomeServerCapabilitiesMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/HomeServerCapabilitiesMapper.kt index 23ab7b64be..44f3d69db5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/HomeServerCapabilitiesMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/HomeServerCapabilitiesMapper.kt @@ -26,13 +26,13 @@ internal object HomeServerCapabilitiesMapper { fun map(entity: HomeServerCapabilitiesEntity): HomeServerCapabilities { return HomeServerCapabilities( - entity.maxUploadFileSize + maxUploadFileSize = entity.maxUploadFileSize ) } fun map(domain: HomeServerCapabilities): HomeServerCapabilitiesEntity { return HomeServerCapabilitiesEntity( - domain.maxUploadFileSize + maxUploadFileSize = domain.maxUploadFileSize ) } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/HomeServerCapabilitiesQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/HomeServerCapabilitiesQueries.kt index 64cd6e4770..e7002a9005 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/HomeServerCapabilitiesQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/HomeServerCapabilitiesQueries.kt @@ -21,17 +21,16 @@ import io.realm.Realm import io.realm.kotlin.createObject import io.realm.kotlin.where +/** + * Get the current HomeServerCapabilitiesEntity, return null if it does not exist + */ +internal fun HomeServerCapabilitiesEntity.Companion.get(realm: Realm): HomeServerCapabilitiesEntity? { + return realm.where().findFirst() +} + /** * Get the current HomeServerCapabilitiesEntity, create one if it does not exist */ internal fun HomeServerCapabilitiesEntity.Companion.getOrCreate(realm: Realm): HomeServerCapabilitiesEntity { - var homeServerCapabilitiesEntity = realm.where().findFirst() - if (homeServerCapabilitiesEntity == null) { - realm.executeTransaction { - realm.createObject() - } - homeServerCapabilitiesEntity = realm.where().findFirst()!! - } - - return homeServerCapabilitiesEntity + return get(realm) ?: realm.createObject() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt index defcfbaa81..f88178cebd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt @@ -56,16 +56,15 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( } - private fun insertInDb(getUploadCapabilitiesResult: GetUploadCapabilitiesResult) { - monarchy - .writeAsync { realm -> - val homeServerCapabilitiesEntity = HomeServerCapabilitiesEntity.getOrCreate(realm) + private suspend fun insertInDb(getUploadCapabilitiesResult: GetUploadCapabilitiesResult) { + monarchy.awaitTransaction { realm -> + val homeServerCapabilitiesEntity = HomeServerCapabilitiesEntity.getOrCreate(realm) - homeServerCapabilitiesEntity.maxUploadFileSize = getUploadCapabilitiesResult.maxUploadSize - ?: HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN + homeServerCapabilitiesEntity.maxUploadFileSize = getUploadCapabilitiesResult.maxUploadSize + ?: HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN - homeServerCapabilitiesEntity.lastUpdatedTimestamp = Date().time - } + homeServerCapabilitiesEntity.lastUpdatedTimestamp = Date().time + } } companion object { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt index 0af849af2e..ea41eb12b5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt @@ -21,24 +21,18 @@ import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService import im.vector.matrix.android.internal.database.mapper.HomeServerCapabilitiesMapper import im.vector.matrix.android.internal.database.model.HomeServerCapabilitiesEntity -import im.vector.matrix.android.internal.database.query.getOrCreate +import im.vector.matrix.android.internal.database.query.get +import io.realm.Realm import javax.inject.Inject internal class DefaultHomeServerCapabilitiesService @Inject constructor(private val monarchy: Monarchy) : HomeServerCapabilitiesService { override fun getHomeServerCapabilities(): HomeServerCapabilities { - var entity: HomeServerCapabilitiesEntity? = null - monarchy.doWithRealm { realm -> - entity = HomeServerCapabilitiesEntity.getOrCreate(realm) - } - - return with(entity) { - if (this != null) { - HomeServerCapabilitiesMapper.map(this) - } else { - // Should not happen - HomeServerCapabilities(HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN) + return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + HomeServerCapabilitiesEntity.get(realm)?.let { + HomeServerCapabilitiesMapper.map(it) } } + ?: HomeServerCapabilities() } } From 8605095668aceda0e35ba3a6a3c8dde6bd05b9de Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 26 Sep 2019 16:49:41 +0200 Subject: [PATCH 042/197] Fix quality code issues --- .../session/room/timeline/DefaultTimeline.kt | 15 +++++-- .../room/timeline/TimelineHiddenReadMarker.kt | 5 ++- .../timeline/TimelineHiddenReadReceipts.kt | 4 +- .../core/ui/views/JumpToReadMarkerView.kt | 7 +--- .../home/room/detail/RoomDetailFragment.kt | 39 +++++++++---------- .../detail/timeline/item/AbsMessageItem.kt | 15 +++++-- 6 files changed, 52 insertions(+), 33 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index 52ef816e99..45efe052a7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -258,12 +258,21 @@ internal class DefaultTimeline( } // Otherwise, we should check if the event is in the db, but is hidden because of filters return Realm.getInstance(realmConfiguration).use { localRealm -> - val nonFilteredEvents = buildEventQuery(localRealm).sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING).findAll() - val nonFilteredEvent = nonFilteredEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, eventId).findFirst() + val nonFilteredEvents = buildEventQuery(localRealm) + .sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING) + .findAll() + + val nonFilteredEvent = nonFilteredEvents.where() + .equalTo(TimelineEventEntityFields.EVENT_ID, eventId) + .findFirst() + val filteredEvents = nonFilteredEvents.where().filterEventsWithSettings().findAll() val isEventInDb = nonFilteredEvent != null - val isHidden = isEventInDb && filteredEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, eventId).findFirst() == null + val isHidden = isEventInDb && filteredEvents.where() + .equalTo(TimelineEventEntityFields.EVENT_ID, eventId) + .findFirst() == null + if (isHidden) { val displayIndex = nonFilteredEvent?.root?.displayIndex if (displayIndex != null) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt index 03d79c2e00..58097c0433 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt @@ -65,7 +65,10 @@ internal class TimelineHiddenReadMarker constructor(private val roomId: String, val hiddenEvent = readMarker.timelineEvent?.firstOrNull() ?: return@OrderedRealmCollectionChangeListener - val isLoaded = nonFilteredEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, hiddenEvent.eventId).findFirst() != null + val isLoaded = nonFilteredEvents.where() + .equalTo(TimelineEventEntityFields.EVENT_ID, hiddenEvent.eventId) + .findFirst() != null + val displayIndex = hiddenEvent.root?.displayIndex if (isLoaded && displayIndex != null) { // Then we are looking for the first displayable event after the hidden one diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt index 0c538f794e..39c2535282 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt @@ -74,8 +74,10 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu for (index in 0 until hiddenReadReceipts.size) { val summary = hiddenReadReceipts[index] ?: continue val timelineEvent = summary.timelineEvent?.firstOrNull() ?: continue - val isLoaded = nonFilteredEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, timelineEvent.eventId).findFirst() != null + val isLoaded = nonFilteredEvents.where() + .equalTo(TimelineEventEntityFields.EVENT_ID, timelineEvent.eventId).findFirst() != null val displayIndex = timelineEvent.root?.displayIndex + if (isLoaded && displayIndex != null) { // Then we are looking for the first displayable event after the hidden one val firstDisplayedEvent = filteredEvents.where() diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt index ed81f82e72..3cfd6cf4f8 100644 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt @@ -25,6 +25,7 @@ import android.view.ViewGroup import android.widget.LinearLayout import android.widget.RelativeLayout import androidx.core.content.ContextCompat +import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import butterknife.ButterKnife @@ -77,11 +78,7 @@ class JumpToReadMarkerView @JvmOverloads constructor( fun render(show: Boolean, readMarkerId: String?) { this.readMarkerId = readMarkerId - visibility = if(show){ - View.VISIBLE - }else { - View.INVISIBLE - } + isInvisible = !show } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 40643f1311..423dd7dc5d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -488,27 +488,26 @@ class RoomDetailFragment : timelineEventController.callback = this if (vectorPreferences.swipeToReplyIsEnabled()) { - val swipeCallback = RoomMessageTouchHelperCallback(requireContext(), - R.drawable.ic_reply, - object : RoomMessageTouchHelperCallback.QuickReplayHandler { - override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { - (model as? AbsMessageItem)?.attributes?.informationData?.let { - val eventId = it.eventId - roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId, composerLayout.composerEditText.text.toString())) - } - } + val quickReplyHandler = object : RoomMessageTouchHelperCallback.QuickReplayHandler { + override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { + (model as? AbsMessageItem)?.attributes?.informationData?.let { + val eventId = it.eventId + roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId, composerLayout.composerEditText.text.toString())) + } + } - override fun canSwipeModel(model: EpoxyModel<*>): Boolean { - return when (model) { - is MessageFileItem, - is MessageImageVideoItem, - is MessageTextItem -> { - return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED - } - else -> false - } - } - }) + override fun canSwipeModel(model: EpoxyModel<*>): Boolean { + return when (model) { + is MessageFileItem, + is MessageImageVideoItem, + is MessageTextItem -> { + return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED + } + else -> false + } + } + } + val swipeCallback = RoomMessageTouchHelperCallback(requireContext(), R.drawable.ic_reply, quickReplyHandler) val touchHelper = ItemTouchHelper(swipeCallback) touchHelper.attachToRecyclerView(recyclerView) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt index 51169fe81b..a4bb5c88cd 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -90,7 +90,12 @@ abstract class AbsMessageItem : BaseEventItem() { holder.timeView.visibility = View.VISIBLE holder.timeView.text = attributes.informationData.time holder.memberNameView.text = attributes.informationData.memberName - attributes.avatarRenderer.render(attributes.informationData.avatarUrl, attributes.informationData.senderId, attributes.informationData.memberName?.toString(), holder.avatarImageView) + attributes.avatarRenderer.render( + attributes.informationData.avatarUrl, + attributes.informationData.senderId, + attributes.informationData.memberName?.toString(), + holder.avatarImageView + ) holder.avatarImageView.setOnLongClickListener(attributes.itemLongClickListener) holder.memberNameView.setOnLongClickListener(attributes.itemLongClickListener) } else { @@ -104,8 +109,12 @@ abstract class AbsMessageItem : BaseEventItem() { } holder.view.setOnClickListener(attributes.itemClickListener) holder.view.setOnLongClickListener(attributes.itemLongClickListener) - - holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener) + + holder.readReceiptsView.render( + attributes.informationData.readReceipts, + attributes.avatarRenderer, + _readReceiptsClickListener + ) holder.readMarkerView.bindView( attributes.informationData.eventId, attributes.informationData.hasReadMarker, From 28315be7b9bc2810ffa712e75dd83b73ce1e42d7 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 26 Sep 2019 17:05:18 +0200 Subject: [PATCH 043/197] Update CHANGES --- CHANGES.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index dc4c743bc4..7c37f66b4d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,12 +7,15 @@ Features: Improvements: - Persist active tab between sessions (#503) - Do not upload file too big for the homeserver (#587) + - Handle read markers (#84) Other changes: - Bugfix: - Fix issue on upload error in loop (#587) + - Fix opening a permalink: the targeted event is displayed twice (#556) + - Fix opening a permalink paginates all the history up to the last event (#282) Translations: - From 53b1b89c4758b273e517dd2e7b467b5f92949fdf Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 26 Sep 2019 17:13:52 +0200 Subject: [PATCH 044/197] after login, the icon in the top left is a green 'A' for (all communities) rather than my avatar (#267) --- CHANGES.md | 1 + .../im/vector/matrix/rx/LiveDataObservable.kt | 1 - .../java/im/vector/matrix/rx/RxSession.kt | 5 +++ .../android/api/session/user/UserService.kt | 5 ++- .../matrix/android/api/util/Optional.kt | 42 +++++++++++++++++++ .../session/user/DefaultUserService.kt | 5 ++- .../riotx/features/home/HomeDrawerFragment.kt | 3 +- .../features/home/group/GroupListViewModel.kt | 34 +++++++++------ 8 files changed, 78 insertions(+), 18 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Optional.kt diff --git a/CHANGES.md b/CHANGES.md index dc4c743bc4..fd141bcdfb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -13,6 +13,7 @@ Other changes: Bugfix: - Fix issue on upload error in loop (#587) + - after login, the icon in the top left is a green 'A' for (all communities) rather than my avatar (#267) Translations: - diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/LiveDataObservable.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/LiveDataObservable.kt index a1943bbe1c..9eaeff762e 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/LiveDataObservable.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/LiveDataObservable.kt @@ -20,7 +20,6 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import io.reactivex.Observable import io.reactivex.android.MainThreadDisposable -import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers private class LiveDataObservable( diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt index f3fb06a45a..d8c254844b 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt @@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.sync.SyncState import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.api.util.Optional import io.reactivex.Observable import io.reactivex.Single @@ -45,6 +46,10 @@ class RxSession(private val session: Session) { return session.livePushers().asObservable() } + fun liveUser(userId: String): Observable> { + return session.liveUser(userId).asObservable() + } + fun liveUsers(): Observable> { return session.liveUsers().asObservable() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt index d3c58edd94..97b02fafea 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt @@ -21,6 +21,7 @@ import androidx.paging.PagedList import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.api.util.Optional /** * This interface defines methods to get users. It's implemented at the session level. @@ -47,9 +48,9 @@ interface UserService { /** * Observe a live user from a userId * @param userId the userId to look for. - * @return a Livedata of user with userId + * @return a LiveData of user with userId */ - fun liveUser(userId: String): LiveData + fun liveUser(userId: String): LiveData> /** * Observe a live list of users sorted alphabetically diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Optional.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Optional.kt new file mode 100644 index 0000000000..19fbe2cc88 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Optional.kt @@ -0,0 +1,42 @@ +/* + + * Copyright 2019 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.matrix.android.api.util + +data class Optional constructor(private val value: T?) { + + fun get(): T { + return value!! + } + + fun getOrNull(): T? { + return value + } + + fun getOrElse(fn: () -> T): T { + return value ?: fn() + } + + companion object { + fun from(value: T?): Optional { + return Optional(value) + } + } + +} + +fun T?.toOptional() = Optional(this) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt index 2925997347..40e79d7735 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt @@ -26,6 +26,8 @@ import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.user.UserService import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.api.util.Optional +import im.vector.matrix.android.api.util.toOptional import im.vector.matrix.android.internal.database.RealmLiveData import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.UserEntity @@ -66,7 +68,7 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona return userEntity.asDomain() } - override fun liveUser(userId: String): LiveData { + override fun liveUser(userId: String): LiveData> { val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm -> UserEntity.where(realm, userId) } @@ -74,6 +76,7 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona results .map { it.asDomain() } .firstOrNull() + .toOptional() } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt index ad39839321..e5f0c5b2d3 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt @@ -51,7 +51,8 @@ class HomeDrawerFragment : VectorBaseFragment() { val groupListFragment = GroupListFragment.newInstance() replaceChildFragment(groupListFragment, R.id.homeDrawerGroupListContainer) } - session.liveUser(session.myUserId).observeK(this) { user -> + session.liveUser(session.myUserId).observeK(this) { optionalUser -> + val user = optionalUser?.getOrNull() if (user != null) { avatarRenderer.render(user.avatarUrl, user.userId, user.displayName, homeDrawerHeaderAvatarView) homeDrawerUsernameView.text = user.displayName diff --git a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt index 0be22e411e..3f2a2e2958 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt @@ -33,6 +33,8 @@ import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.utils.LiveEvent +import io.reactivex.Observable +import io.reactivex.functions.BiFunction const val ALL_COMMUNITIES_GROUP_ID = "ALL_COMMUNITIES_GROUP_ID" @@ -91,20 +93,26 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro } private fun observeGroupSummaries() { - session - .rx() - .liveGroupSummaries() - // Keep only joined groups. Group invitations will be managed later - .map { it.filter { groupSummary -> groupSummary.membership == Membership.JOIN } } - .map { - val myUser = session.getUser(session.myUserId) - val allCommunityGroup = GroupSummary( - groupId = ALL_COMMUNITIES_GROUP_ID, - membership = Membership.JOIN, - displayName = stringProvider.getString(R.string.group_all_communities), - avatarUrl = myUser?.avatarUrl ?: "") - listOf(allCommunityGroup) + it + Observable.combineLatest, List>( + session + .rx() + .liveUser(session.myUserId) + .map { optionalUser -> + GroupSummary( + groupId = ALL_COMMUNITIES_GROUP_ID, + membership = Membership.JOIN, + displayName = stringProvider.getString(R.string.group_all_communities), + avatarUrl = optionalUser.getOrNull()?.avatarUrl ?: "") + }, + session + .rx() + .liveGroupSummaries() + // Keep only joined groups. Group invitations will be managed later + .map { it.filter { groupSummary -> groupSummary.membership == Membership.JOIN } }, + BiFunction { allCommunityGroup, communityGroups -> + listOf(allCommunityGroup) + communityGroups } + ) .execute { async -> val newSelectedGroup = selectedGroup ?: async()?.firstOrNull() copy(asyncGroups = async, selectedGroup = newSelectedGroup) From 84542326f433ba741e9e08392d85770c62219bfc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 26 Sep 2019 18:06:24 +0200 Subject: [PATCH 045/197] HomeDetailFragment observe the selectedGroupStore instead of passing argument --- .../riotx/features/home/HomeDetailFragment.kt | 39 ++++++++----------- .../features/home/HomeDetailViewModel.kt | 14 +++++++ .../features/home/HomeDetailViewState.kt | 3 ++ .../riotx/features/home/HomeNavigator.kt | 3 +- .../features/home/group/GroupListViewModel.kt | 4 +- 5 files changed, 37 insertions(+), 26 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt index 112fae1b24..f6e8cc199b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt @@ -17,18 +17,17 @@ package im.vector.riotx.features.home import android.os.Bundle -import android.os.Parcelable import android.view.LayoutInflater import androidx.core.view.forEachIndexed import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProviders -import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.google.android.material.bottomnavigation.BottomNavigationItemView import com.google.android.material.bottomnavigation.BottomNavigationMenuView import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState +import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.platform.ToolbarConfigurable @@ -38,26 +37,16 @@ import im.vector.riotx.features.home.room.list.RoomListFragment import im.vector.riotx.features.home.room.list.RoomListParams import im.vector.riotx.features.home.room.list.UnreadCounterBadgeView import im.vector.riotx.features.workers.signout.SignOutViewModel -import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_home_detail.* import javax.inject.Inject -@Parcelize -data class HomeDetailParams( - val groupId: String, - val groupName: String, - val groupAvatar: String -) : Parcelable - - private const val INDEX_CATCHUP = 0 private const val INDEX_PEOPLE = 1 private const val INDEX_ROOMS = 2 class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate { - private val params: HomeDetailParams by args() private val unreadCounterBadgeViews = arrayListOf() private val viewModel: HomeDetailViewModel by fragmentViewModel() @@ -84,11 +73,25 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate { setupToolbar() setupKeysBackupBanner() + viewModel.selectSubscribe(this, HomeDetailViewState::groupSummary) { groupSummary -> + onGroupChange(groupSummary.orNull()) + } viewModel.selectSubscribe(this, HomeDetailViewState::displayMode) { displayMode -> switchDisplayMode(displayMode) } } + private fun onGroupChange(groupSummary: GroupSummary?) { + groupSummary?.let { + avatarRenderer.render( + it.avatarUrl, + it.groupId, + it.displayName, + groupToolbarAvatarImageView + ) + } + } + private fun setupKeysBackupBanner() { // Keys backup banner // Use the SignOutViewModel, it observe the keys backup state and this is what we need here @@ -130,12 +133,6 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate { parentActivity.configure(groupToolbar) } groupToolbar.title = "" - avatarRenderer.render( - params.groupAvatar, - params.groupId, - params.groupName, - groupToolbarAvatarImageView - ) groupToolbarAvatarImageView.setOnClickListener { navigationViewModel.goTo(HomeActivity.Navigation.OpenDrawer) } @@ -207,10 +204,8 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate { companion object { - fun newInstance(args: HomeDetailParams): HomeDetailFragment { - return HomeDetailFragment().apply { - setArguments(args) - } + fun newInstance(): HomeDetailFragment { + return HomeDetailFragment() } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt index 688d2b6b7b..a825b64979 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt @@ -25,6 +25,7 @@ import im.vector.matrix.android.api.session.Session import im.vector.matrix.rx.rx import im.vector.riotx.core.di.HasScreenInjector import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.features.home.group.SelectedGroupStore import im.vector.riotx.features.home.room.list.RoomListFragment import im.vector.riotx.features.ui.UiStateRepository import io.reactivex.schedulers.Schedulers @@ -36,6 +37,7 @@ import io.reactivex.schedulers.Schedulers class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: HomeDetailViewState, private val session: Session, private val uiStateRepository: UiStateRepository, + private val selectedGroupStore: SelectedGroupStore, private val homeRoomListStore: HomeRoomListObservableStore) : VectorViewModel(initialState) { @@ -62,6 +64,7 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho init { observeSyncState() + observeSelectedGroupStore() observeRoomSummaries() } @@ -88,6 +91,17 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho .disposeOnClear() } + private fun observeSelectedGroupStore() { + selectedGroupStore + .observe() + .subscribe { + setState { + copy(groupSummary = it) + } + } + .disposeOnClear() + } + private fun observeRoomSummaries() { homeRoomListStore .observe() diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewState.kt index a8f89cc566..cb2c07835d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewState.kt @@ -16,11 +16,14 @@ package im.vector.riotx.features.home +import arrow.core.Option import com.airbnb.mvrx.MvRxState +import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.sync.SyncState import im.vector.riotx.features.home.room.list.RoomListFragment data class HomeDetailViewState( + val groupSummary: Option = Option.empty(), val displayMode: RoomListFragment.DisplayMode = RoomListFragment.DisplayMode.HOME, val notificationCountCatchup: Int = 0, val notificationHighlightCatchup: Boolean = false, diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeNavigator.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeNavigator.kt index 680d7bbc33..ca1d12d95f 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeNavigator.kt @@ -36,8 +36,7 @@ class HomeNavigator @Inject constructor() { activity?.let { it.drawerLayout?.closeDrawer(GravityCompat.START) - val args = HomeDetailParams(groupSummary.groupId, groupSummary.displayName, groupSummary.avatarUrl) - val homeDetailFragment = HomeDetailFragment.newInstance(args) + val homeDetailFragment = HomeDetailFragment.newInstance() it.replaceFragment(homeDetailFragment, R.id.homeDetailFragmentContainer) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt index 3f2a2e2958..2ca8c414fb 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt @@ -39,7 +39,7 @@ import io.reactivex.functions.BiFunction const val ALL_COMMUNITIES_GROUP_ID = "ALL_COMMUNITIES_GROUP_ID" class GroupListViewModel @AssistedInject constructor(@Assisted initialState: GroupListViewState, - private val selectedGroupHolder: SelectedGroupStore, + private val selectedGroupStore: SelectedGroupStore, private val session: Session, private val stringProvider: StringProvider ) : VectorViewModel(initialState) { @@ -73,7 +73,7 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro if (it != null) { _openGroupLiveData.postLiveEvent(it) val optionGroup = Option.fromNullable(it) - selectedGroupHolder.post(optionGroup) + selectedGroupStore.post(optionGroup) } } } From 2d95fe921df8adcb5d593949ee05f19bbd663f86 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 26 Sep 2019 18:42:27 +0200 Subject: [PATCH 046/197] after login, the icon in the top left is a green 'A' for (all communities) rather than my avatar (#267) - part2 (Toolbar) --- .../features/home/HomeDetailViewModel.kt | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt index a825b64979..b92c821671 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt @@ -16,18 +16,27 @@ package im.vector.riotx.features.home +import arrow.core.Option +import arrow.core.toOption import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.group.model.GroupSummary +import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.rx.rx +import im.vector.riotx.R import im.vector.riotx.core.di.HasScreenInjector import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.features.home.group.ALL_COMMUNITIES_GROUP_ID import im.vector.riotx.features.home.group.SelectedGroupStore import im.vector.riotx.features.home.room.list.RoomListFragment import im.vector.riotx.features.ui.UiStateRepository +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers /** @@ -38,7 +47,8 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho private val session: Session, private val uiStateRepository: UiStateRepository, private val selectedGroupStore: SelectedGroupStore, - private val homeRoomListStore: HomeRoomListObservableStore) + private val homeRoomListStore: HomeRoomListObservableStore, + private val stringProvider: StringProvider) : VectorViewModel(initialState) { @AssistedInject.Factory @@ -94,6 +104,37 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho private fun observeSelectedGroupStore() { selectedGroupStore .observe() + // TODO I do not like that, but it crashes with + // Thread: RxComputationThreadPool-2, Exception: java.lang.IllegalStateException: Cannot invoke observeForever on a background thread + .observeOn(AndroidSchedulers.mainThread()) + .flatMap { optionGroupSummary -> + val group = optionGroupSummary.orNull() + when { + group == null -> + Observable.just(Option.empty()) + group.groupId == ALL_COMMUNITIES_GROUP_ID -> + session + .rx() + .liveUser(session.myUserId) + .map { optionalUser -> + GroupSummary( + groupId = ALL_COMMUNITIES_GROUP_ID, + membership = Membership.JOIN, + displayName = stringProvider.getString(R.string.group_all_communities), + avatarUrl = optionalUser.getOrNull()?.avatarUrl ?: "") + .toOption() + } + else -> + session + .rx() + .liveGroupSummaries() + .map { it.filter { groupSummary -> groupSummary.groupId == group.groupId } } + .map { + it.firstOrNull().toOption() + } + } + } + .observeOn(Schedulers.computation()) .subscribe { setState { copy(groupSummary = it) From de30e7c1c600ca4b699582206f5d3c7321b0b261 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 26 Sep 2019 19:00:38 +0200 Subject: [PATCH 047/197] Code cleanup --- .../features/home/HomeDetailViewModel.kt | 66 +++++++++---------- 1 file changed, 30 insertions(+), 36 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt index b92c821671..5a1364e9a4 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt @@ -16,7 +16,8 @@ package im.vector.riotx.features.home -import arrow.core.Option +import arrow.core.None +import arrow.core.firstOrNone import arrow.core.toOption import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory @@ -111,7 +112,7 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho val group = optionGroupSummary.orNull() when { group == null -> - Observable.just(Option.empty()) + Observable.just(None) group.groupId == ALL_COMMUNITIES_GROUP_ID -> session .rx() @@ -122,16 +123,14 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho membership = Membership.JOIN, displayName = stringProvider.getString(R.string.group_all_communities), avatarUrl = optionalUser.getOrNull()?.avatarUrl ?: "") - .toOption() } + .map { it.toOption() } else -> session .rx() .liveGroupSummaries() .map { it.filter { groupSummary -> groupSummary.groupId == group.groupId } } - .map { - it.firstOrNull().toOption() - } + .map { it.firstOrNone() } } } .observeOn(Schedulers.computation()) @@ -147,38 +146,33 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho homeRoomListStore .observe() .observeOn(Schedulers.computation()) - .subscribe { list -> - list.let { summaries -> - val peopleNotifications = summaries - .filter { it.isDirect } - .map { it.notificationCount } - .takeIf { it.isNotEmpty() } - ?.sumBy { i -> i } - ?: 0 - val peopleHasHighlight = summaries - .filter { it.isDirect } - .any { it.highlightCount > 0 } + .map { it.asSequence() } + .subscribe { summaries -> + val peopleNotifications = summaries + .filter { it.isDirect } + .map { it.notificationCount } + .sumBy { i -> i } + val peopleHasHighlight = summaries + .filter { it.isDirect } + .any { it.highlightCount > 0 } - val roomsNotifications = summaries - .filter { !it.isDirect } - .map { it.notificationCount } - .takeIf { it.isNotEmpty() } - ?.sumBy { i -> i } - ?: 0 - val roomsHasHighlight = summaries - .filter { !it.isDirect } - .any { it.highlightCount > 0 } + val roomsNotifications = summaries + .filter { !it.isDirect } + .map { it.notificationCount } + .sumBy { i -> i } + val roomsHasHighlight = summaries + .filter { !it.isDirect } + .any { it.highlightCount > 0 } - setState { - copy( - notificationCountCatchup = peopleNotifications + roomsNotifications, - notificationHighlightCatchup = peopleHasHighlight || roomsHasHighlight, - notificationCountPeople = peopleNotifications, - notificationHighlightPeople = peopleHasHighlight, - notificationCountRooms = roomsNotifications, - notificationHighlightRooms = roomsHasHighlight - ) - } + setState { + copy( + notificationCountCatchup = peopleNotifications + roomsNotifications, + notificationHighlightCatchup = peopleHasHighlight || roomsHasHighlight, + notificationCountPeople = peopleNotifications, + notificationHighlightPeople = peopleHasHighlight, + notificationCountRooms = roomsNotifications, + notificationHighlightRooms = roomsHasHighlight + ) } } .disposeOnClear() From b5c6c1af0d32937a8167c5619afaf3d35cb9d4d3 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 30 Sep 2019 19:09:10 +0200 Subject: [PATCH 048/197] Realm: allow to clear corrupted session db --- .../SessionRealmConfigurationFactory.kt | 73 +++++++++++++++++++ .../android/internal/session/SessionModule.kt | 15 +--- 2 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt new file mode 100644 index 0000000000..e0e609c011 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2019 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.matrix.android.internal.database + +import android.content.Context +import im.vector.matrix.android.internal.database.model.SessionRealmModule +import im.vector.matrix.android.internal.session.SessionModule +import io.realm.Realm +import io.realm.RealmConfiguration +import timber.log.Timber +import java.io.File +import javax.inject.Inject + +private const val REALM_SHOULD_CLEAR_FLAG_ = "REALM_SHOULD_CLEAR_FLAG_" + +/** + * This class is handling creation of RealmConfiguration for a session. + * It will handle corrupted realm by clearing the db file. It allows to just clear cache without losing your crypto keys. + * It's clearly not perfect but there is no way to catch the native crash. + */ +internal class SessionRealmConfigurationFactory @Inject constructor(private val realmKeysUtils: RealmKeysUtils, + context: Context) { + + private val sharedPreferences = context.getSharedPreferences("im.vector.matrix.android.realm", Context.MODE_PRIVATE) + + + fun create(directory: File, userMd5: String): RealmConfiguration { + val shouldClearRealm = sharedPreferences.getBoolean("$REALM_SHOULD_CLEAR_FLAG_$userMd5", false) + if (shouldClearRealm) { + directory.deleteRecursively() + } + sharedPreferences + .edit() + .putBoolean("$REALM_SHOULD_CLEAR_FLAG_$userMd5", true) + .apply() + + val realmConfiguration = RealmConfiguration.Builder() + .directory(directory) + .name("disk_store.realm") + .apply { + realmKeysUtils.configureEncryption(this, "${SessionModule.DB_ALIAS_PREFIX}$userMd5") + } + .modules(SessionRealmModule()) + .deleteRealmIfMigrationNeeded() + .build() + + // Try creating a realm instance and if it succeeds we can clear the flag + Realm.getInstance(realmConfiguration).use { + Timber.v("Successfully create realm instance") + sharedPreferences + .edit() + .putBoolean("$REALM_SHOULD_CLEAR_FLAG_$userMd5", false) + .apply() + } + return realmConfiguration + } + + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index 7b655dd939..b3224ae0b3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -30,7 +30,7 @@ import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService import im.vector.matrix.android.api.session.securestorage.SecureStorageService import im.vector.matrix.android.internal.database.LiveEntityObserver -import im.vector.matrix.android.internal.database.RealmKeysUtils +import im.vector.matrix.android.internal.database.SessionRealmConfigurationFactory import im.vector.matrix.android.internal.database.model.SessionRealmModule import im.vector.matrix.android.internal.di.* import im.vector.matrix.android.internal.network.AccessTokenInterceptor @@ -54,6 +54,7 @@ internal abstract class SessionModule { @Module companion object { + internal const val DB_ALIAS_PREFIX = "session_db_" @JvmStatic @@ -94,18 +95,10 @@ internal abstract class SessionModule { @Provides @SessionDatabase @SessionScope - fun providesRealmConfiguration(realmKeysUtils: RealmKeysUtils, + fun providesRealmConfiguration(realmConfigurationFactory: SessionRealmConfigurationFactory, @UserCacheDirectory directory: File, @UserMd5 userMd5: String): RealmConfiguration { - return RealmConfiguration.Builder() - .directory(directory) - .name("disk_store.realm") - .apply { - realmKeysUtils.configureEncryption(this, "$DB_ALIAS_PREFIX$userMd5") - } - .modules(SessionRealmModule()) - .deleteRealmIfMigrationNeeded() - .build() + return realmConfigurationFactory.create(directory, userMd5) } @JvmStatic From 0814f53fed0f4c867d90413d62b1851e7bb9ce15 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 30 Sep 2019 19:36:19 +0200 Subject: [PATCH 049/197] Group avatar: clean and optimize a bit. --- .../java/im/vector/matrix/rx/RxSession.kt | 2 +- .../im/vector/riotx/core/utils/RxStore.kt | 26 +++++++++---- .../features/home/HomeActivityViewModel.kt | 3 +- .../riotx/features/home/HomeDetailFragment.kt | 2 + .../features/home/HomeDetailViewModel.kt | 38 ------------------- .../features/home/group/GroupListViewModel.kt | 13 ++++++- 6 files changed, 35 insertions(+), 49 deletions(-) diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt index d8c254844b..0d0cb2f461 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt @@ -47,7 +47,7 @@ class RxSession(private val session: Session) { } fun liveUser(userId: String): Observable> { - return session.liveUser(userId).asObservable() + return session.liveUser(userId).asObservable().distinctUntilChanged() } fun liveUsers(): Observable> { diff --git a/vector/src/main/java/im/vector/riotx/core/utils/RxStore.kt b/vector/src/main/java/im/vector/riotx/core/utils/RxStore.kt index 89780b2463..94f2a9c912 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/RxStore.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/RxStore.kt @@ -20,19 +20,31 @@ import com.jakewharton.rxrelay2.BehaviorRelay import io.reactivex.Observable import io.reactivex.schedulers.Schedulers -open class RxStore(defaultValue: T? = null) { +open class RxStore(private val defaultValue: T? = null) { - private val storeSubject: BehaviorRelay = if (defaultValue == null) { - BehaviorRelay.create() - } else { - BehaviorRelay.createDefault(defaultValue) + var storeRelay = createRelay() + + fun clear() { + storeRelay = createRelay() + } + + fun get(): T? { + return storeRelay.value } fun observe(): Observable { - return storeSubject.hide().observeOn(Schedulers.computation()) + return storeRelay.hide().observeOn(Schedulers.computation()) } fun post(value: T) { - storeSubject.accept(value) + storeRelay.accept(value) + } + + private fun createRelay(): BehaviorRelay { + return if (defaultValue == null) { + BehaviorRelay.create() + } else { + BehaviorRelay.createDefault(defaultValue) + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewModel.kt index f8c1eca19e..971f3d602c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewModel.kt @@ -98,7 +98,8 @@ class HomeActivityViewModel @AssistedInject constructor(@Assisted initialState: override fun onCleared() { super.onCleared() - + selectedGroupStore.clear() + homeRoomListStore.clear() session.removeListener(this) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt index f6e8cc199b..955973011c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt @@ -38,6 +38,7 @@ import im.vector.riotx.features.home.room.list.RoomListParams import im.vector.riotx.features.home.room.list.UnreadCounterBadgeView import im.vector.riotx.features.workers.signout.SignOutViewModel import kotlinx.android.synthetic.main.fragment_home_detail.* +import timber.log.Timber import javax.inject.Inject @@ -196,6 +197,7 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate { } override fun invalidate() = withState(viewModel) { + Timber.v(it.toString()) unreadCounterBadgeViews[INDEX_CATCHUP].render(UnreadCounterBadgeView.State(it.notificationCountCatchup, it.notificationHighlightCatchup)) unreadCounterBadgeViews[INDEX_PEOPLE].render(UnreadCounterBadgeView.State(it.notificationCountPeople, it.notificationHighlightPeople)) unreadCounterBadgeViews[INDEX_ROOMS].render(UnreadCounterBadgeView.State(it.notificationCountRooms, it.notificationHighlightRooms)) diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt index 5a1364e9a4..c8e3231362 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt @@ -16,28 +16,19 @@ package im.vector.riotx.features.home -import arrow.core.None -import arrow.core.firstOrNone -import arrow.core.toOption import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.session.Session -import im.vector.matrix.android.api.session.group.model.GroupSummary -import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.rx.rx -import im.vector.riotx.R import im.vector.riotx.core.di.HasScreenInjector import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.resources.StringProvider -import im.vector.riotx.features.home.group.ALL_COMMUNITIES_GROUP_ID import im.vector.riotx.features.home.group.SelectedGroupStore import im.vector.riotx.features.home.room.list.RoomListFragment import im.vector.riotx.features.ui.UiStateRepository -import io.reactivex.Observable -import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers /** @@ -105,35 +96,6 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho private fun observeSelectedGroupStore() { selectedGroupStore .observe() - // TODO I do not like that, but it crashes with - // Thread: RxComputationThreadPool-2, Exception: java.lang.IllegalStateException: Cannot invoke observeForever on a background thread - .observeOn(AndroidSchedulers.mainThread()) - .flatMap { optionGroupSummary -> - val group = optionGroupSummary.orNull() - when { - group == null -> - Observable.just(None) - group.groupId == ALL_COMMUNITIES_GROUP_ID -> - session - .rx() - .liveUser(session.myUserId) - .map { optionalUser -> - GroupSummary( - groupId = ALL_COMMUNITIES_GROUP_ID, - membership = Membership.JOIN, - displayName = stringProvider.getString(R.string.group_all_communities), - avatarUrl = optionalUser.getOrNull()?.avatarUrl ?: "") - } - .map { it.toOption() } - else -> - session - .rx() - .liveGroupSummaries() - .map { it.filter { groupSummary -> groupSummary.groupId == group.groupId } } - .map { it.firstOrNone() } - } - } - .observeOn(Schedulers.computation()) .subscribe { setState { copy(groupSummary = it) diff --git a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt index 2ca8c414fb..c7278ff81d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt @@ -71,7 +71,11 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro private fun observeSelectionState() { selectSubscribe(GroupListViewState::selectedGroup) { if (it != null) { - _openGroupLiveData.postLiveEvent(it) + val selectedGroup = selectedGroupStore.get()?.orNull() + // We only wan to open group if the updated selectedGroup is a different one. + if (selectedGroup?.groupId != it.groupId) { + _openGroupLiveData.postLiveEvent(it) + } val optionGroup = Option.fromNullable(it) selectedGroupStore.post(optionGroup) } @@ -114,7 +118,12 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro } ) .execute { async -> - val newSelectedGroup = selectedGroup ?: async()?.firstOrNull() + val currentSelectedGroupId = selectedGroup?.groupId + val newSelectedGroup = if (currentSelectedGroupId != null) { + async()?.find { it.groupId == currentSelectedGroupId } + } else { + async()?.firstOrNull() + } copy(asyncGroups = async, selectedGroup = newSelectedGroup) } } From e842bf13b25a84d891b8ab94e12aae7e492a07ae Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 30 Sep 2019 14:56:06 +0200 Subject: [PATCH 050/197] Timeline: fix back pagination state --- .../session/room/timeline/DefaultTimeline.kt | 28 +++++++++++-------- .../room/timeline/TokenChunkEventPersistor.kt | 17 ++++++----- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index 45efe052a7..3afde4efb5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.session.room.timeline import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.crypto.CryptoService +import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.model.ReadReceipt import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.Timeline @@ -351,7 +352,7 @@ internal class DefaultTimeline( updateState(Timeline.Direction.BACKWARDS) { it.copy( hasMoreInCache = lastBuiltEvent == null || lastBuiltEvent.displayIndex > lastCacheEvent?.root?.displayIndex ?: Int.MAX_VALUE, - hasReachedEnd = chunkEntity?.isLastBackward ?: false + hasReachedEnd = chunkEntity?.isLastBackward ?: false || lastCacheEvent?.root?.type == EventType.STATE_ROOM_CREATE ) } } @@ -499,9 +500,9 @@ internal class DefaultTimeline( return } val params = PaginationTask.Params(roomId = roomId, - from = token, - direction = direction.toPaginationDirection(), - limit = limit) + from = token, + direction = direction.toPaginationDirection(), + limit = limit) Timber.v("Should fetch $limit items $direction") cancelableBag += paginationTask @@ -510,13 +511,18 @@ internal class DefaultTimeline( this.constraints = TaskConstraints(connectedToNetwork = true) this.callback = object : MatrixCallback { override fun onSuccess(data: TokenChunkEventPersistor.Result) { - if (data == TokenChunkEventPersistor.Result.SUCCESS) { - Timber.v("Success fetching $limit items $direction from pagination request") - } else { - // Database won't be updated, so we force pagination request - BACKGROUND_HANDLER.post { - executePaginationTask(direction, limit) + when (data) { + TokenChunkEventPersistor.Result.SUCCESS -> { + Timber.v("Success fetching $limit items $direction from pagination request") } + TokenChunkEventPersistor.Result.REACHED_END -> { + postSnapshot() + } + TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE -> + // Database won't be updated, so we force pagination request + BACKGROUND_HANDLER.post { + executePaginationTask(direction, limit) + } } } @@ -572,7 +578,7 @@ internal class DefaultTimeline( val timelineEvent = buildTimelineEvent(eventEntity) if (timelineEvent.isEncrypted() - && timelineEvent.root.mxDecryptionResult == null) { + && timelineEvent.root.mxDecryptionResult == null) { timelineEvent.root.eventId?.let { eventDecryptor.requestDecryption(it) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt index 0305002959..2703b5fb91 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -100,6 +100,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy enum class Result { SHOULD_FETCH_MORE, + REACHED_END, SUCCESS } @@ -124,10 +125,8 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy prevToken = receivedChunk.end } - if (ChunkEntity.find(realm, roomId, nextToken = nextToken) != null || ChunkEntity.find(realm, roomId, prevToken = prevToken) != null) { - Timber.v("Already inserted - SKIP") - return@awaitTransaction - } + val shouldSkip = ChunkEntity.find(realm, roomId, nextToken = nextToken) != null + || ChunkEntity.find(realm, roomId, prevToken = prevToken) != null val prevChunk = ChunkEntity.find(realm, roomId, nextToken = prevToken) val nextChunk = ChunkEntity.find(realm, roomId, prevToken = nextToken) @@ -146,7 +145,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) { Timber.v("Reach end of $roomId") currentChunk.isLastBackward = true - } else { + } else if (!shouldSkip) { Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}") val eventIds = ArrayList(receivedChunk.events.size) for (event in receivedChunk.events) { @@ -180,8 +179,12 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy currentChunk.updateSenderDataFor(eventIds) } } - return if (receivedChunk.events.isEmpty() && receivedChunk.stateEvents.isEmpty() && receivedChunk.start != receivedChunk.end) { - Result.SHOULD_FETCH_MORE + return if (receivedChunk.events.isEmpty()) { + if (receivedChunk.start != receivedChunk.end) { + Result.SHOULD_FETCH_MORE + } else { + Result.REACHED_END + } } else { Result.SUCCESS } From 31397869b2c5d2a69d15eaf2d4f4ca25ae1c5a62 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 1 Oct 2019 12:33:38 +0200 Subject: [PATCH 051/197] Read marker: refine JumpToReafMarkerView --- .../riotx/core/ui/views/JumpToReadMarkerView.kt | 11 +++-------- .../src/main/res/layout/view_jump_to_read_marker.xml | 5 ++++- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt index 3cfd6cf4f8..c44b10e31f 100644 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt @@ -60,14 +60,9 @@ class JumpToReadMarkerView @JvmOverloads constructor( private fun setupView() { inflate(context, R.layout.view_jump_to_read_marker, this) setBackgroundColor(ContextCompat.getColor(context, R.color.notification_accent_color)) - jumpToReadMarkerLabelView.movementMethod = BetterLinkMovementMethod.getInstance() - isClickable = true - jumpToReadMarkerLabelView.text = span(resources.getString(R.string.room_jump_to_first_unread)) { - textDecorationLine = "underline" - onClick = { - readMarkerId?.also { - callback?.onJumpToReadMarkerClicked(it) - } + jumpToReadMarkerLabelView.setOnClickListener { + readMarkerId?.also { + callback?.onJumpToReadMarkerClicked(it) } } closeJumpToReadMarkerView.setOnClickListener { diff --git a/vector/src/main/res/layout/view_jump_to_read_marker.xml b/vector/src/main/res/layout/view_jump_to_read_marker.xml index 4ded65e8f8..aac22b3311 100644 --- a/vector/src/main/res/layout/view_jump_to_read_marker.xml +++ b/vector/src/main/res/layout/view_jump_to_read_marker.xml @@ -10,11 +10,13 @@ android:id="@+id/jumpToReadMarkerLabelView" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="16dp" android:layout_toStartOf="@+id/closeJumpToReadMarkerView" android:drawableStart="@drawable/arrow_up_circle" android:drawablePadding="10dp" + android:background="?attr/selectableItemBackground" android:gravity="center_vertical" + android:paddingStart="16dp" + android:paddingEnd="16dp" android:paddingTop="12dp" android:paddingBottom="12dp" android:text="@string/room_jump_to_first_unread" @@ -23,6 +25,7 @@ Date: Tue, 1 Oct 2019 17:39:20 +0200 Subject: [PATCH 052/197] Hot fix: fix home navigation issue --- .../vector/riotx/core/di/VectorComponent.kt | 3 -- .../im/vector/riotx/core/utils/RxStore.kt | 10 +---- .../riotx/features/home/HomeActivity.kt | 9 ++-- .../features/home/HomeActivityViewModel.kt | 2 - .../riotx/features/home/HomeNavigator.kt | 43 ------------------- .../features/home/group/GroupListFragment.kt | 10 +++-- .../features/home/group/GroupListViewModel.kt | 2 +- 7 files changed, 15 insertions(+), 64 deletions(-) delete mode 100644 vector/src/main/java/im/vector/riotx/features/home/HomeNavigator.kt diff --git a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt index 10b3730022..4aab312e2f 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt @@ -32,7 +32,6 @@ import im.vector.riotx.features.configuration.VectorConfiguration import im.vector.riotx.features.crypto.keysrequest.KeyRequestHandler import im.vector.riotx.features.crypto.verification.IncomingVerificationRequestHandler import im.vector.riotx.features.home.AvatarRenderer -import im.vector.riotx.features.home.HomeNavigator import im.vector.riotx.features.home.HomeRoomListObservableStore import im.vector.riotx.features.home.group.SelectedGroupStore import im.vector.riotx.features.html.EventHtmlRenderer @@ -81,8 +80,6 @@ interface VectorComponent { fun navigator(): Navigator - fun homeNavigator(): HomeNavigator - fun homeRoomListObservableStore(): HomeRoomListObservableStore fun selectedGroupStore(): SelectedGroupStore diff --git a/vector/src/main/java/im/vector/riotx/core/utils/RxStore.kt b/vector/src/main/java/im/vector/riotx/core/utils/RxStore.kt index 94f2a9c912..b539ade931 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/RxStore.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/RxStore.kt @@ -22,15 +22,7 @@ import io.reactivex.schedulers.Schedulers open class RxStore(private val defaultValue: T? = null) { - var storeRelay = createRelay() - - fun clear() { - storeRelay = createRelay() - } - - fun get(): T? { - return storeRelay.value - } + private val storeRelay = createRelay() fun observe(): Observable { return storeRelay.hide().observeOn(Schedulers.computation()) diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt index c3636cde7a..bca02d3fa4 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt @@ -53,6 +53,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { // Supported navigation actions for this Activity sealed class Navigation { object OpenDrawer : Navigation() + object OpenGroup : Navigation() } private val homeActivityViewModel: HomeActivityViewModel by viewModel() @@ -60,7 +61,6 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { @Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var homeActivityViewModelFactory: HomeActivityViewModel.Factory - @Inject lateinit var homeNavigator: HomeNavigator @Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler @Inject lateinit var pushManager: PushersManager @Inject lateinit var notificationDrawerManager: NotificationDrawerManager @@ -79,7 +79,6 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - homeNavigator.activity = this FcmHelper.ensureFcmTokenIsRetrieved(this, pushManager) navigationViewModel = ViewModelProviders.of(this).get(HomeNavigationViewModel::class.java) drawerLayout.addDrawerListener(drawerListener) @@ -93,6 +92,11 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { navigationViewModel.navigateTo.observeEvent(this) { navigation -> when (navigation) { is Navigation.OpenDrawer -> drawerLayout.openDrawer(GravityCompat.START) + is Navigation.OpenGroup -> { + drawerLayout.closeDrawer(GravityCompat.START) + val homeDetailFragment = HomeDetailFragment.newInstance() + replaceFragment(homeDetailFragment, R.id.homeDetailFragmentContainer) + } } } @@ -134,7 +138,6 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { override fun onDestroy() { drawerLayout.removeDrawerListener(drawerListener) - homeNavigator.activity = null super.onDestroy() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewModel.kt index 971f3d602c..6d52d87e7d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewModel.kt @@ -98,8 +98,6 @@ class HomeActivityViewModel @AssistedInject constructor(@Assisted initialState: override fun onCleared() { super.onCleared() - selectedGroupStore.clear() - homeRoomListStore.clear() session.removeListener(this) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeNavigator.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeNavigator.kt deleted file mode 100644 index ca1d12d95f..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeNavigator.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2019 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.riotx.features.home - -import androidx.core.view.GravityCompat -import im.vector.matrix.android.api.session.group.model.GroupSummary -import im.vector.riotx.R -import im.vector.riotx.core.extensions.replaceFragment -import kotlinx.android.synthetic.main.activity_home.* -import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class HomeNavigator @Inject constructor() { - - var activity: HomeActivity? = null - private var rootRoomId: String? = null - - fun openSelectedGroup(groupSummary: GroupSummary) { - Timber.v("Open selected group ${groupSummary.groupId}") - activity?.let { - it.drawerLayout?.closeDrawer(GravityCompat.START) - - val homeDetailFragment = HomeDetailFragment.newInstance() - it.replaceFragment(homeDetailFragment, R.id.homeDetailFragmentContainer) - } - } -} diff --git a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListFragment.kt index 75cc567c81..2615643781 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListFragment.kt @@ -17,8 +17,10 @@ package im.vector.riotx.features.home.group import android.os.Bundle +import androidx.lifecycle.ViewModelProviders import com.airbnb.mvrx.Incomplete import com.airbnb.mvrx.Success +import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.fragmentViewModel import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.riotx.R @@ -26,7 +28,8 @@ import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.platform.StateView import im.vector.riotx.core.platform.VectorBaseFragment -import im.vector.riotx.features.home.HomeNavigator +import im.vector.riotx.features.home.HomeActivity +import im.vector.riotx.features.home.HomeNavigationViewModel import kotlinx.android.synthetic.main.fragment_group_list.* import javax.inject.Inject @@ -38,10 +41,10 @@ class GroupListFragment : VectorBaseFragment(), GroupSummaryController.Callback } } + private lateinit var navigationViewModel: HomeNavigationViewModel private val viewModel: GroupListViewModel by fragmentViewModel() @Inject lateinit var groupListViewModelFactory: GroupListViewModel.Factory - @Inject lateinit var homeNavigator: HomeNavigator @Inject lateinit var groupController: GroupSummaryController override fun getLayoutResId() = R.layout.fragment_group_list @@ -52,12 +55,13 @@ class GroupListFragment : VectorBaseFragment(), GroupSummaryController.Callback override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) + navigationViewModel = ViewModelProviders.of(requireActivity()).get(HomeNavigationViewModel::class.java) groupController.callback = this stateView.contentView = groupListEpoxyRecyclerView groupListEpoxyRecyclerView.setController(groupController) viewModel.subscribe { renderState(it) } viewModel.openGroupLiveData.observeEvent(this) { - homeNavigator.openSelectedGroup(it) + navigationViewModel.goTo(HomeActivity.Navigation.OpenGroup) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt index c7278ff81d..dbd422e2e5 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt @@ -71,7 +71,7 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro private fun observeSelectionState() { selectSubscribe(GroupListViewState::selectedGroup) { if (it != null) { - val selectedGroup = selectedGroupStore.get()?.orNull() + val selectedGroup = _openGroupLiveData.value?.peekContent() // We only wan to open group if the updated selectedGroup is a different one. if (selectedGroup?.groupId != it.groupId) { _openGroupLiveData.postLiveEvent(it) From 44f6391cb42bcb7bb5729dab8dd6d409023d9d91 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 1 Oct 2019 20:11:15 +0200 Subject: [PATCH 053/197] Optimize: use LazyThreeTen --- vector/build.gradle | 4 +++- vector/src/main/java/im/vector/riotx/VectorApplication.kt | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/vector/build.gradle b/vector/build.gradle index 16bf70aef0..3073d0f8f7 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -234,7 +234,9 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta1' implementation 'androidx.core:core-ktx:1.0.2' - implementation 'com.jakewharton.threetenabp:threetenabp:1.1.1' + implementation "org.threeten:threetenbp:1.4.0:no-tzdb" + implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.7.0" + implementation "com.squareup.moshi:moshi-adapters:$moshi_version" kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" diff --git a/vector/src/main/java/im/vector/riotx/VectorApplication.kt b/vector/src/main/java/im/vector/riotx/VectorApplication.kt index bf56e71915..4884354ef3 100644 --- a/vector/src/main/java/im/vector/riotx/VectorApplication.kt +++ b/vector/src/main/java/im/vector/riotx/VectorApplication.kt @@ -31,9 +31,9 @@ import androidx.multidex.MultiDex import com.airbnb.epoxy.EpoxyAsyncUtil import com.airbnb.epoxy.EpoxyController import com.facebook.stetho.Stetho +import com.gabrielittner.threetenbp.LazyThreeTen import com.github.piasy.biv.BigImageViewer import com.github.piasy.biv.loader.glide.GlideImageLoader -import com.jakewharton.threetenabp.AndroidThreeTen import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.MatrixConfiguration import im.vector.matrix.android.api.auth.Authenticator @@ -96,7 +96,8 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration. Stetho.initializeWithDefaults(this) } logInfo() - AndroidThreeTen.init(this) + LazyThreeTen.init(this) + BigImageViewer.initialize(GlideImageLoader.with(applicationContext)) EpoxyController.defaultDiffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler() EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler() From 275dd20412c4a498293dcef2809bf4ef1dab0390 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 1 Oct 2019 20:12:01 +0200 Subject: [PATCH 054/197] Optimize: don't build OkHttp in Application OnCreate if we don't need it --- .../matrix/android/internal/auth/DefaultAuthenticator.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt index 949aa6611e..9ae9af8b98 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt @@ -39,9 +39,10 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import javax.inject.Inject +import javax.inject.Provider internal class DefaultAuthenticator @Inject constructor(@Unauthenticated - private val okHttpClient: OkHttpClient, + private val okHttpClient: Provider, private val retrofitFactory: RetrofitFactory, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val sessionParamsStore: SessionParamsStore, @@ -119,7 +120,7 @@ internal class DefaultAuthenticator @Inject constructor(@Unauthenticated } private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI { - val retrofit = retrofitFactory.create(okHttpClient, homeServerConnectionConfig.homeServerUri.toString()) + val retrofit = retrofitFactory.create(okHttpClient.get(), homeServerConnectionConfig.homeServerUri.toString()) return retrofit.create(AuthAPI::class.java) } From 650a151b183f534e649a6a3d2973f9dab27c05fa Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 1 Oct 2019 20:12:15 +0200 Subject: [PATCH 055/197] Optimize: remove some epoxy building from main thread --- .../features/home/group/GroupListFragment.kt | 2 +- .../home/group/GroupSummaryController.kt | 18 ++++++-- .../home/room/list/RoomListFragment.kt | 3 +- .../home/room/list/RoomSummaryController.kt | 46 ++++++++++++------- 4 files changed, 48 insertions(+), 21 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListFragment.kt index 2615643781..df45bbc9a0 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListFragment.kt @@ -70,7 +70,7 @@ class GroupListFragment : VectorBaseFragment(), GroupSummaryController.Callback is Incomplete -> stateView.state = StateView.State.Loading is Success -> stateView.state = StateView.State.Content } - groupController.setData(state) + groupController.update(state) } override fun onGroupSelected(groupSummary: GroupSummary) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/group/GroupSummaryController.kt b/vector/src/main/java/im/vector/riotx/features/home/group/GroupSummaryController.kt index 785f833077..91fb02a74e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/group/GroupSummaryController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/group/GroupSummaryController.kt @@ -16,17 +16,29 @@ package im.vector.riotx.features.home.group +import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.TypedEpoxyController import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.riotx.features.home.AvatarRenderer import javax.inject.Inject -class GroupSummaryController @Inject constructor(private val avatarRenderer: AvatarRenderer): TypedEpoxyController() { +class GroupSummaryController @Inject constructor(private val avatarRenderer: AvatarRenderer) : EpoxyController() { var callback: Callback? = null + private var viewState: GroupListViewState? = null - override fun buildModels(viewState: GroupListViewState) { - buildGroupModels(viewState.asyncGroups(), viewState.selectedGroup) + init { + requestModelBuild() + } + + fun update(viewState: GroupListViewState) { + this.viewState = viewState + requestModelBuild() + } + + override fun buildModels() { + val nonNullViewState = viewState ?: return + buildGroupModels(nonNullViewState.asyncGroups(), nonNullViewState.selectedGroup) } private fun buildGroupModels(summaries: List?, selected: GroupSummary?) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt index afe3579d76..cc2224b64a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt @@ -41,6 +41,7 @@ import im.vector.riotx.features.home.room.list.widget.FabMenuView import im.vector.riotx.features.notifications.NotificationDrawerManager import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_room_list.* +import timber.log.Timber import javax.inject.Inject @Parcelize @@ -180,7 +181,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O is Success -> renderSuccess(state) is Fail -> renderFailure(state.asyncFilteredRooms.error) } - roomController.setData(state) + roomController.update(state) } private fun renderSuccess(state: RoomListViewState) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt index 42e3a3db85..f2e9fbc947 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt @@ -17,40 +17,54 @@ package im.vector.riotx.features.home.room.list import androidx.annotation.StringRes +import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.TypedEpoxyController import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.home.room.filtered.FilteredRoomFooterItem import im.vector.riotx.features.home.room.filtered.filteredRoomFooterItem +import timber.log.Timber import javax.inject.Inject class RoomSummaryController @Inject constructor(private val stringProvider: StringProvider, private val roomSummaryItemFactory: RoomSummaryItemFactory, private val roomListNameFilter: RoomListNameFilter -) : TypedEpoxyController() { +) : EpoxyController() { var listener: Listener? = null - override fun buildModels(viewState: RoomListViewState) { - if (viewState.displayMode == RoomListFragment.DisplayMode.FILTERED) { - buildFilteredRooms(viewState) + private var viewState: RoomListViewState? = null + + init { + requestModelBuild() + } + + fun update(viewState: RoomListViewState) { + this.viewState = viewState + requestModelBuild() + } + + override fun buildModels() { + val nonNullViewState = viewState ?: return + if (nonNullViewState.displayMode == RoomListFragment.DisplayMode.FILTERED) { + buildFilteredRooms(nonNullViewState) } else { - val roomSummaries = viewState.asyncFilteredRooms() + val roomSummaries = nonNullViewState.asyncFilteredRooms() roomSummaries?.forEach { (category, summaries) -> if (summaries.isEmpty()) { return@forEach } else { - val isExpanded = viewState.isCategoryExpanded(category) - buildRoomCategory(viewState, summaries, category.titleRes, viewState.isCategoryExpanded(category)) { + val isExpanded = nonNullViewState.isCategoryExpanded(category) + buildRoomCategory(nonNullViewState, summaries, category.titleRes, nonNullViewState.isCategoryExpanded(category)) { listener?.onToggleRoomCategory(category) } if (isExpanded) { buildRoomModels(summaries, - viewState.joiningRoomsIds, - viewState.joiningErrorRoomsIds, - viewState.rejectingRoomsIds, - viewState.rejectingErrorRoomsIds) + nonNullViewState.joiningRoomsIds, + nonNullViewState.joiningErrorRoomsIds, + nonNullViewState.rejectingRoomsIds, + nonNullViewState.rejectingErrorRoomsIds) } } } @@ -66,10 +80,10 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri .filter { it.membership == Membership.JOIN && roomListNameFilter.test(it) } buildRoomModels(filteredSummaries, - viewState.joiningRoomsIds, - viewState.joiningErrorRoomsIds, - viewState.rejectingRoomsIds, - viewState.rejectingErrorRoomsIds) + viewState.joiningRoomsIds, + viewState.joiningErrorRoomsIds, + viewState.rejectingRoomsIds, + viewState.rejectingErrorRoomsIds) addFilterFooter(viewState) } @@ -105,7 +119,7 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri showHighlighted(showHighlighted) listener { mutateExpandedState() - setData(viewState) + update(viewState) } } } From ff7856c535d2ab7013938881f12771a8d7024c0a Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 2 Oct 2019 19:30:01 +0200 Subject: [PATCH 056/197] Optimize: start removing some constraint layout from timeline --- .../detail/timeline/item/BaseEventItem.kt | 9 +- .../res/layout/item_timeline_event_base.xml | 141 +++++++++--------- .../item_timeline_event_base_noinfo.xml | 94 ++++++------ ...item_timeline_event_merged_header_stub.xml | 77 +++++----- vector/src/main/res/values/styles_riot.xml | 16 +- 5 files changed, 164 insertions(+), 173 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt index c6e813e878..8ac8264cad 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt @@ -17,8 +17,11 @@ package im.vector.riotx.features.home.room.detail.timeline.item import android.view.View import android.view.ViewStub +import android.widget.RelativeLayout import androidx.annotation.IdRes import androidx.constraintlayout.widget.Guideline +import androidx.core.view.marginStart +import androidx.core.view.updateLayoutParams import com.airbnb.epoxy.EpoxyAttribute import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyHolder @@ -44,7 +47,9 @@ abstract class BaseEventItem : VectorEpoxyModel override fun bind(holder: H) { super.bind(holder) - holder.leftGuideline.setGuidelineBegin(leftGuideline) + holder.leftGuideline.updateLayoutParams { + this.marginStart = leftGuideline + } holder.checkableBackground.isChecked = highlighted } @@ -55,7 +60,7 @@ abstract class BaseEventItem : VectorEpoxyModel abstract fun getEventIds(): List abstract class BaseHolder(@IdRes val stubId: Int) : VectorEpoxyHolder() { - val leftGuideline by bind(R.id.messageStartGuideline) + val leftGuideline by bind(R.id.messageStartGuideline) val checkableBackground by bind(R.id.messageSelectedBackground) val readReceiptsView by bind(R.id.readReceiptsView) val readMarkerView by bind(R.id.readMarkerView) diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml index 4eb9be0b9f..ee5d34bd40 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -1,6 +1,5 @@ - + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_alignParentTop="true" + android:layout_alignBottom="@+id/readMarkerView" + android:background="?riotx_highlighted_message_background" /> - - - + + + + android:layout="@layout/item_timeline_event_media_message_stub" /> + android:layout_height="wrap_content" + style="@style/TimelineContentStubBaseParams" + android:layout="@layout/item_timeline_event_file_stub" /> + android:layout="@layout/item_timeline_event_redacted_stub" /> + - - + android:layout_below="@+id/viewStubContainer" + android:layout_toEndOf="@+id/messageStartGuideline" + android:orientation="vertical"> - + - + + + + + + android:layout_marginEnd="8dp" + android:layout_marginBottom="2dp" + android:background="?attr/vctr_unread_marker_line_color" + android:visibility="invisible" /> - \ No newline at end of file + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml index fc4a527d03..99fb052a96 100644 --- a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml +++ b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml @@ -1,6 +1,5 @@ - + + + android:layout_marginStart="52dp" + android:orientation="vertical" /> - + + android:layout="@layout/item_timeline_event_default_stub" /> + android:layout="@layout/item_timeline_event_blank_stub" /> + style="@style/TimelineContentStubBaseParams" + android:layout="@layout/item_timeline_event_merged_header_stub" /> - + + + android:layout_below="@id/viewStubContainer" + android:orientation="vertical"> - + + - \ No newline at end of file + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_merged_header_stub.xml b/vector/src/main/res/layout/item_timeline_event_merged_header_stub.xml index 46c84aa4e7..ede5b0e749 100644 --- a/vector/src/main/res/layout/item_timeline_event_merged_header_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_merged_header_stub.xml @@ -1,61 +1,58 @@ - + android:layout_height="wrap_content" + android:orientation="vertical"> - + - + + + + + + android:background="?attr/riotx_header_panel_background"/> + - \ No newline at end of file + \ No newline at end of file diff --git a/vector/src/main/res/values/styles_riot.xml b/vector/src/main/res/values/styles_riot.xml index d1894254cd..1b19c87e08 100644 --- a/vector/src/main/res/values/styles_riot.xml +++ b/vector/src/main/res/values/styles_riot.xml @@ -282,7 +282,7 @@ - - - - - + + + + + \ No newline at end of file From 82fc97f619ce43271d9ead18740e430148c536b3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 11 Oct 2019 09:32:43 +0200 Subject: [PATCH 123/197] Add dictionary specific to the project to VCS --- .gitignore | 10 +++++----- .idea/dictionaries/bmarty.xml | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 .idea/dictionaries/bmarty.xml diff --git a/.gitignore b/.gitignore index 26ee897e5e..e54a3c380e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,11 @@ *.iml .gradle /local.properties -.idea/* -/.idea/* -/.idea/libraries -/.idea/modules.xml -/.idea/workspace.xml +# idea files: exclude everything except dictionnaries +.idea/caches +.idea/codeStyles +.idea/libraries +.idea/*.xml .DS_Store /build /captures diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml new file mode 100644 index 0000000000..01981ada12 --- /dev/null +++ b/.idea/dictionaries/bmarty.xml @@ -0,0 +1,19 @@ + + + + backstack + bytearray + ciphertext + decryptor + emoji + emojis + hmac + ktlint + linkified + linkify + megolm + pbkdf + pkcs + + + \ No newline at end of file From 0a0c344bfb326c0657046a0a50bdf36cd1c99b31 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 11 Oct 2019 10:10:16 +0200 Subject: [PATCH 124/197] Upgrade RecyclerView version to fix issues with a11y. Also minor upgrade of some other libs --- matrix-sdk-android/build.gradle | 6 +++--- vector/build.gradle | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 328cfbee20..3e6d3ea88b 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -102,7 +102,7 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation "androidx.appcompat:appcompat:1.1.0" - implementation "androidx.recyclerview:recyclerview:1.1.0-beta04" + implementation "androidx.recyclerview:recyclerview:1.1.0-beta05" implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version" @@ -110,8 +110,8 @@ dependencies { // Network implementation 'com.squareup.retrofit2:retrofit:2.6.2' implementation 'com.squareup.retrofit2:converter-moshi:2.6.2' - implementation 'com.squareup.okhttp3:okhttp:4.2.0' - implementation 'com.squareup.okhttp3:logging-interceptor:4.2.0' + implementation 'com.squareup.okhttp3:okhttp:4.2.2' + implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2' implementation 'com.novoda:merlin:1.2.0' implementation "com.squareup.moshi:moshi-adapters:$moshi_version" kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" diff --git a/vector/build.gradle b/vector/build.gradle index 3992bba8a1..78018a6107 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -281,7 +281,7 @@ dependencies { // UI implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' - implementation 'com.google.android.material:material:1.1.0-alpha10' + implementation 'com.google.android.material:material:1.1.0-beta01' implementation 'me.gujun.android:span:1.7' implementation "ru.noties.markwon:core:$markwon_version" implementation "ru.noties.markwon:html:$markwon_version" From ee5ebb4b83cf4b1873fd466e3973e13b399fc1b4 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 11 Oct 2019 12:20:39 +0200 Subject: [PATCH 125/197] Attachments: get better layout --- .../riotx/core/extensions/ViewExtensions.kt | 7 ++ .../riotx/core/utils/KeyboardStateUtils.kt | 23 ++++++ .../riotx/core/utils/PermissionsTools.kt | 1 + .../attachments/AttachmentTypeSelectorView.kt | 75 ++++++++++++------- .../features/attachments/AttachmentsHelper.kt | 6 ++ .../features/attachments/AttachmentsMapper.kt | 16 ++-- .../home/room/detail/RoomDetailFragment.kt | 59 +++++++++------ vector/src/main/res/values/styles_riot.xml | 4 +- 8 files changed, 132 insertions(+), 59 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/core/utils/KeyboardStateUtils.kt diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/ViewExtensions.kt b/vector/src/main/java/im/vector/riotx/core/extensions/ViewExtensions.kt index c84f09927d..631df78b5e 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/ViewExtensions.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/ViewExtensions.kt @@ -47,4 +47,11 @@ fun EditText.showPassword(visible: Boolean, updateCursor: Boolean = true) { inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD } if (updateCursor) setSelection(text?.length ?: 0) +} + +fun View.getMeasurements(): Pair { + measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED) + val width = measuredWidth + val height = measuredHeight + return width to height } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/core/utils/KeyboardStateUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/KeyboardStateUtils.kt new file mode 100644 index 0000000000..dd41d0d28b --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/utils/KeyboardStateUtils.kt @@ -0,0 +1,23 @@ +package im.vector.riotx.core.utils + +import android.app.Activity +import android.graphics.Rect +import android.view.View +import android.view.ViewTreeObserver + +class KeyboardStateUtils(activity: Activity) : ViewTreeObserver.OnGlobalLayoutListener { + + private val contentView: View = activity.findViewById(android.R.id.content).also { + it.viewTreeObserver.addOnGlobalLayoutListener(this) + } + var isKeyboardShowing: Boolean = false + + override fun onGlobalLayout() { + val rect = Rect() + contentView.getWindowVisibleDisplayFrame(rect) + val screenHeight = contentView.rootView.height + + val keypadHeight = screenHeight - rect.bottom + isKeyboardShowing = keypadHeight > screenHeight * 0.15 + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt b/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt index 03cf248b79..f93f470a23 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt @@ -67,6 +67,7 @@ const val PERMISSION_REQUEST_CODE_VIDEO_CALL = 572 const val PERMISSION_REQUEST_CODE_EXPORT_KEYS = 573 const val PERMISSION_REQUEST_CODE_CHANGE_AVATAR = 574 const val PERMISSION_REQUEST_CODE_DOWNLOAD_FILE = 575 +const val PERMISSION_REQUEST_CODE_PICK_ATTACHMENT = 576 /** * Log the used permissions statuses. diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentTypeSelectorView.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentTypeSelectorView.kt index bc34353467..c8868b4b96 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentTypeSelectorView.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentTypeSelectorView.kt @@ -40,16 +40,18 @@ import androidx.core.view.doOnNextLayout import com.amulyakhare.textdrawable.TextDrawable import com.amulyakhare.textdrawable.util.ColorGenerator import im.vector.riotx.R -import im.vector.riotx.core.utils.DimensionConverter +import im.vector.riotx.core.extensions.getMeasurements import kotlin.math.max +private const val ANIMATION_DURATION = 250 + class AttachmentTypeSelectorView(context: Context, inflater: LayoutInflater, var callback: Callback?) : PopupWindow(context) { interface Callback { - fun onTypeSelected(type: Int) + fun onTypeSelected(type: Type) } private val iconColorGenerator = ColorGenerator.MATERIAL @@ -66,12 +68,12 @@ class AttachmentTypeSelectorView(context: Context, init { val root = FrameLayout(context) val layout = inflater.inflate(R.layout.view_attachment_type_selector, root, true) - galleryButton = layout.findViewById(R.id.attachmentGalleryButton).configure(TYPE_GALLERY) - cameraButton = layout.findViewById(R.id.attachmentCameraButton).configure(TYPE_CAMERA) - fileButton = layout.findViewById(R.id.attachmentFileButton).configure(TYPE_FILE) - stickersButton = layout.findViewById(R.id.attachmentStickersButton).configure(TYPE_STICKER) - audioButton = layout.findViewById(R.id.attachmentAudioButton).configure(TYPE_AUDIO) - contactButton = layout.findViewById(R.id.attachmentContactButton).configure(TYPE_CONTACT) + galleryButton = layout.findViewById(R.id.attachmentGalleryButton).configure(Type.GALLERY) + cameraButton = layout.findViewById(R.id.attachmentCameraButton).configure(Type.CAMERA) + fileButton = layout.findViewById(R.id.attachmentFileButton).configure(Type.FILE) + stickersButton = layout.findViewById(R.id.attachmentStickersButton).configure(Type.STICKER) + audioButton = layout.findViewById(R.id.attachmentAudioButton).configure(Type.AUDIO) + contactButton = layout.findViewById(R.id.attachmentContactButton).configure(Type.CONTACT) contentView = layout width = LinearLayout.LayoutParams.MATCH_PARENT height = LinearLayout.LayoutParams.WRAP_CONTENT @@ -82,8 +84,20 @@ class AttachmentTypeSelectorView(context: Context, isTouchable = true } - fun show(anchor: View) { - showAtLocation(anchor, Gravity.BOTTOM, 0, 0) + fun show(anchor: View, isKeyboardOpen: Boolean) { + this.anchor = anchor + val anchorCoordinates = IntArray(2) + anchor.getLocationOnScreen(anchorCoordinates) + if (isKeyboardOpen) { + showAtLocation(anchor, Gravity.NO_GRAVITY, 0, anchorCoordinates[1] + anchor.height) + } else { + val contentViewHeight = if (contentView.height == 0) { + contentView.getMeasurements().second + } else { + contentView.height + } + showAtLocation(anchor, Gravity.NO_GRAVITY, 0, anchorCoordinates[1] - contentViewHeight) + } contentView.doOnNextLayout { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { animateWindowInCircular(anchor, contentView) @@ -124,10 +138,10 @@ class AttachmentTypeSelectorView(context: Context, private fun animateWindowInCircular(anchor: View?, contentView: View) { val coordinates = getClickCoordinates(anchor, contentView) val animator = ViewAnimationUtils.createCircularReveal(contentView, - coordinates.first, - coordinates.second, - 0f, - max(contentView.width, contentView.height).toFloat()) + coordinates.first, + coordinates.second, + 0f, + max(contentView.width, contentView.height).toFloat()) animator.duration = ANIMATION_DURATION.toLong() animator.start() } @@ -142,10 +156,10 @@ class AttachmentTypeSelectorView(context: Context, private fun animateWindowOutCircular(anchor: View?, contentView: View) { val coordinates = getClickCoordinates(anchor, contentView) val animator = ViewAnimationUtils.createCircularReveal(getContentView(), - coordinates.first, - coordinates.second, - max(getContentView().width, getContentView().height).toFloat(), - 0f) + coordinates.first, + coordinates.second, + max(getContentView().width, getContentView().height).toFloat(), + 0f) animator.duration = ANIMATION_DURATION.toLong() animator.addListener(object : AnimatorListenerAdapter() { @@ -182,13 +196,13 @@ class AttachmentTypeSelectorView(context: Context, return Pair(x, y) } - private fun ImageButton.configure(type: Int): ImageButton { + private fun ImageButton.configure(type: Type): ImageButton { this.background = TextDrawable.builder().buildRound("", iconColorGenerator.getColor(type)) this.setOnClickListener(TypeClickListener(type)) return this } - private inner class TypeClickListener(private val type: Int) : View.OnClickListener { + private inner class TypeClickListener(private val type: Type) : View.OnClickListener { override fun onClick(v: View) { dismiss() @@ -197,16 +211,21 @@ class AttachmentTypeSelectorView(context: Context, } - companion object { + enum class Type { + + CAMERA, + GALLERY, + FILE, + STICKER, + AUDIO, + CONTACT; + + fun requirePermission(): Boolean { + return this != CAMERA && this != STICKER + } - const val TYPE_CAMERA = 0 - const val TYPE_GALLERY = 1 - const val TYPE_FILE = 2 - const val TYPE_STICKER = 3 - const val TYPE_AUDIO = 4 - const val TYPE_CONTACT = 5 - private const val ANIMATION_DURATION = 250 } + } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt index b1b10a7797..46f25ca3e2 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt @@ -26,6 +26,7 @@ import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.riotx.core.platform.Restorable private const val CAPTURE_PATH_KEY = "CAPTURE_PATH_KEY" +private const val PENDING_TYPE_KEY = "PENDING_TYPE_KEY" /** * This class helps to handle attachments by providing simple methods. @@ -49,6 +50,7 @@ class AttachmentsHelper private constructor(private val pickerManagerFactory: Pi } private var capturePath: String? = null + var pendingType: AttachmentTypeSelectorView.Type? = null private val imagePicker by lazy { pickerManagerFactory.createImagePicker() @@ -76,6 +78,9 @@ class AttachmentsHelper private constructor(private val pickerManagerFactory: Pi capturePath?.also { outState.putString(CAPTURE_PATH_KEY, it) } + pendingType?.also { + outState.putSerializable(PENDING_TYPE_KEY, it) + } } override fun onRestoreInstanceState(savedInstanceState: Bundle?) { @@ -83,6 +88,7 @@ class AttachmentsHelper private constructor(private val pickerManagerFactory: Pi if (capturePath != null) { cameraImagePicker.reinitialize(capturePath) } + pendingType = savedInstanceState?.getSerializable(PENDING_TYPE_KEY) as? AttachmentTypeSelectorView.Type } // Public Methods diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt index 469cef1427..ca46936af9 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt @@ -28,7 +28,7 @@ fun ChosenFile.toContentAttachmentData(): ContentAttachmentData { mimeType = mimeType, type = mapType(), size = size, - date = createdAt.time, + date = createdAt?.time ?: System.currentTimeMillis(), name = displayName ) } @@ -39,7 +39,7 @@ fun ChosenAudio.toContentAttachmentData(): ContentAttachmentData { mimeType = mimeType, type = mapType(), size = size, - date = createdAt.time, + date = createdAt?.time ?: System.currentTimeMillis(), name = displayName, duration = duration ) @@ -61,9 +61,9 @@ fun ChosenImage.toContentAttachmentData(): ContentAttachmentData { type = mapType(), name = displayName, size = size, - height = height.toLong(), - width = width.toLong(), - date = createdAt.time + height = height?.toLong(), + width = width?.toLong(), + date = createdAt?.time ?: System.currentTimeMillis() ) } @@ -73,9 +73,9 @@ fun ChosenVideo.toContentAttachmentData(): ContentAttachmentData { mimeType = mimeType, type = ContentAttachmentData.Type.VIDEO, size = size, - date = createdAt.time, - height = height.toLong(), - width = width.toLong(), + date = createdAt?.time ?: System.currentTimeMillis(), + height = height?.toLong(), + width = width?.toLong(), duration = duration, name = displayName ) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index d3ce4cacb1..bdc6408e88 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -115,6 +115,7 @@ import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* import org.commonmark.parser.Parser import timber.log.Timber import java.io.File +import java.security.Key import javax.inject.Inject @@ -198,6 +199,7 @@ class RoomDetailFragment : private lateinit var actionViewModel: ActionsHandler private lateinit var layoutManager: LinearLayoutManager private lateinit var attachmentsHelper: AttachmentsHelper + private lateinit var keyboardStateUtils: KeyboardStateUtils @BindView(R.id.composerLayout) lateinit var composerLayout: TextComposerView @@ -205,6 +207,7 @@ class RoomDetailFragment : private var lockSendButton = false + override fun injectWith(injector: ScreenComponent) { injector.inject(this) } @@ -213,6 +216,7 @@ class RoomDetailFragment : super.onActivityCreated(savedInstanceState) actionViewModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java) attachmentsHelper = AttachmentsHelper.create(this, this).register() + keyboardStateUtils = KeyboardStateUtils(requireActivity()) setupToolbar(roomToolbar) setupRecyclerView() setupComposer() @@ -310,9 +314,9 @@ class RoomDetailFragment : AlertDialog.Builder(requireActivity()) .setTitle(R.string.dialog_title_error) .setMessage(getString(R.string.error_file_too_big, - error.filename, - TextUtils.formatFileSize(requireContext(), error.fileSizeInBytes), - TextUtils.formatFileSize(requireContext(), error.homeServerLimitInBytes) + error.filename, + TextUtils.formatFileSize(requireContext(), error.fileSizeInBytes), + TextUtils.formatFileSize(requireContext(), error.homeServerLimitInBytes) )) .setPositiveButton(R.string.ok, null) .show() @@ -389,11 +393,11 @@ class RoomDetailFragment : if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { val parser = Parser.builder().build() val document = parser.parse(messageContent.formattedBody - ?: messageContent.body) + ?: messageContent.body) formattedBody = eventHtmlRenderer.render(document) } composerLayout.composerRelatedMessageContent.text = formattedBody - ?: nonFormattedBody + ?: nonFormattedBody updateComposerText(defaultContent) @@ -402,11 +406,11 @@ class RoomDetailFragment : avatarRenderer.render(event.senderAvatar, event.root.senderId - ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) + ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) avatarRenderer.render(event.senderAvatar, - event.root.senderId ?: "", - event.senderName, - composerLayout.composerRelatedMessageAvatar) + event.root.senderId ?: "", + event.senderName, + composerLayout.composerRelatedMessageAvatar) composerLayout.expand { //need to do it here also when not using quick reply focusComposerAndShowKeyboard() @@ -443,9 +447,9 @@ class RoomDetailFragment : when (requestCode) { REACTION_SELECT_REQUEST_CODE -> { val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID) - ?: return + ?: return val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT) - ?: return + ?: return //TODO check if already reacted with that? roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId)) } @@ -453,6 +457,7 @@ class RoomDetailFragment : } } + // PRIVATE METHODS ***************************************************************************** @@ -624,7 +629,7 @@ class RoomDetailFragment : if (!::attachmentTypeSelector.isInitialized) { attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this) } - attachmentTypeSelector.show(composerLayout.attachmentButton) + attachmentTypeSelector.show(composerLayout.attachmentButton, keyboardStateUtils.isKeyboardShowing) } } @@ -825,11 +830,16 @@ class RoomDetailFragment : if (allGranted(grantResults)) { if (requestCode == PERMISSION_REQUEST_CODE_DOWNLOAD_FILE) { val action = roomDetailViewModel.pendingAction - if (action != null) { roomDetailViewModel.pendingAction = null roomDetailViewModel.process(action) } + } else if (requestCode == PERMISSION_REQUEST_CODE_PICK_ATTACHMENT) { + val pendingType = attachmentsHelper.pendingType + if (pendingType != null) { + attachmentsHelper.pendingType = null + launchAttachmentProcess(pendingType) + } } } } @@ -1109,15 +1119,22 @@ class RoomDetailFragment : // AttachmentTypeSelectorView.Callback - override fun onTypeSelected(type: Int) { - when (type) { - AttachmentTypeSelectorView.TYPE_CAMERA -> attachmentsHelper.openCamera() - AttachmentTypeSelectorView.TYPE_FILE -> attachmentsHelper.selectFile() - AttachmentTypeSelectorView.TYPE_GALLERY -> attachmentsHelper.selectGallery() - AttachmentTypeSelectorView.TYPE_AUDIO -> attachmentsHelper.selectAudio() - AttachmentTypeSelectorView.TYPE_CONTACT -> vectorBaseActivity.notImplemented("Picking contacts") - AttachmentTypeSelectorView.TYPE_STICKER -> vectorBaseActivity.notImplemented("Adding stickers") + override fun onTypeSelected(type: AttachmentTypeSelectorView.Type) { + if (!type.requirePermission() || checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_PICK_ATTACHMENT)) { + launchAttachmentProcess(type) + } else { + attachmentsHelper.pendingType = type + } + } + private fun launchAttachmentProcess(type: AttachmentTypeSelectorView.Type) { + when (type) { + AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera() + AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile() + AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery() + AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio() + AttachmentTypeSelectorView.Type.CONTACT -> vectorBaseActivity.notImplemented("Picking contacts") + AttachmentTypeSelectorView.Type.STICKER -> vectorBaseActivity.notImplemented("Adding stickers") } } diff --git a/vector/src/main/res/values/styles_riot.xml b/vector/src/main/res/values/styles_riot.xml index 2019fd8a2a..c5b04de730 100644 --- a/vector/src/main/res/values/styles_riot.xml +++ b/vector/src/main/res/values/styles_riot.xml @@ -326,8 +326,8 @@ From 116d569fa8436bd820d6923e2e89dff0a5e55b8e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 11 Oct 2019 14:35:37 +0200 Subject: [PATCH 126/197] Fix regression after merge conflict: big font for messages with only big emoji --- .../timeline/factory/MessageItemFactory.kt | 17 +++++++++++++---- .../helper/MessageItemAttributesFactory.kt | 3 +++ .../room/detail/timeline/item/AbsMessageItem.kt | 2 ++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 58bda1eaf5..4619eb4c8e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -19,9 +19,9 @@ package im.vector.riotx.features.home.room.detail.timeline.factory import android.text.SpannableStringBuilder import android.text.Spanned import android.text.TextPaint +import android.text.style.AbsoluteSizeSpan import android.text.style.ClickableSpan import android.text.style.ForegroundColorSpan -import android.text.style.RelativeSizeSpan import android.view.View import dagger.Lazy import im.vector.matrix.android.api.permalinks.MatrixLinkify @@ -39,6 +39,8 @@ import im.vector.riotx.core.linkify.VectorLinkify import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.utils.DebouncedClickListener +import im.vector.riotx.core.utils.DimensionConverter +import im.vector.riotx.core.utils.containsOnlyEmojis import im.vector.riotx.core.utils.isLocalFile import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.helper.* @@ -241,12 +243,13 @@ class MessageItemFactory @Inject constructor( return MessageTextItem_() .apply { if (informationData.hasBeenEdited) { - val spannable = annotateWithEdited(linkifiedBody, callback, informationData) + val spannable = annotateWithEdited(linkifiedBody, callback, attributes.dimensionConverter, informationData) message(spannable) } else { message(linkifiedBody) } } + .useBigFont(linkifiedBody.length <= MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT * 2 && containsOnlyEmojis(linkifiedBody.toString())) .searchForPills(isFormatted) .leftGuideline(avatarSizeProvider.leftGuideline) .attributes(attributes) @@ -257,6 +260,7 @@ class MessageItemFactory @Inject constructor( private fun annotateWithEdited(linkifiedBody: CharSequence, callback: TimelineEventController.Callback?, + dimensionConverter: DimensionConverter, informationData: MessageInformationData): SpannableStringBuilder { val spannable = SpannableStringBuilder() spannable.append(linkifiedBody) @@ -271,7 +275,8 @@ class MessageItemFactory @Inject constructor( editEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) - spannable.setSpan(RelativeSizeSpan(.9f), editStart, editEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + // Note: text size is set to 14sp + spannable.setSpan(AbsoluteSizeSpan(dimensionConverter.spToPx(13)), editStart, editEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) spannable.setSpan(object : ClickableSpan() { override fun onClick(widget: View?) { callback?.onEditedDecorationClicked(informationData) @@ -321,7 +326,7 @@ class MessageItemFactory @Inject constructor( return MessageTextItem_() .apply { if (informationData.hasBeenEdited) { - val spannable = annotateWithEdited(message, callback, informationData) + val spannable = annotateWithEdited(message, callback, attributes.dimensionConverter, informationData) message(spannable) } else { message(message) @@ -351,4 +356,8 @@ class MessageItemFactory @Inject constructor( VectorLinkify.addLinks(spannable, true) return spannable } + + companion object { + private const val MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT = 5 + } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt index 0e1229eeca..dddf507453 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt @@ -22,6 +22,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.riotx.EmojiCompatFontProvider import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.utils.DebouncedClickListener +import im.vector.riotx.core.utils.DimensionConverter import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.item.AbsMessageItem @@ -32,6 +33,7 @@ class MessageItemAttributesFactory @Inject constructor( private val avatarRenderer: AvatarRenderer, private val colorProvider: ColorProvider, private val avatarSizeProvider: AvatarSizeProvider, + private val dimensionConverter: DimensionConverter, private val emojiCompatFontProvider: EmojiCompatFontProvider) { fun create(messageContent: MessageContent?, @@ -42,6 +44,7 @@ class MessageItemAttributesFactory @Inject constructor( informationData = informationData, avatarRenderer = avatarRenderer, colorProvider = colorProvider, + dimensionConverter = dimensionConverter, itemLongClickListener = View.OnLongClickListener { view -> callback?.onEventLongClicked(informationData, messageContent, view) ?: false }, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt index bddee50861..461028c3d0 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -29,6 +29,7 @@ import im.vector.riotx.R import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.ui.views.ReadMarkerView import im.vector.riotx.core.utils.DebouncedClickListener +import im.vector.riotx.core.utils.DimensionConverter import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.reactions.widget.ReactionButton @@ -172,6 +173,7 @@ abstract class AbsMessageItem : BaseEventItem() { val informationData: MessageInformationData, val avatarRenderer: AvatarRenderer, val colorProvider: ColorProvider, + val dimensionConverter: DimensionConverter, val itemLongClickListener: View.OnLongClickListener? = null, val itemClickListener: View.OnClickListener? = null, val memberClickListener: View.OnClickListener? = null, From 3622c0ecb428711edc3bd82c3640bcbd9cf5f326 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 11 Oct 2019 15:42:22 +0200 Subject: [PATCH 127/197] Mark all as read --- CHANGES.md | 1 + .../android/api/session/room/RoomService.kt | 5 +++ .../session/room/DefaultRoomService.kt | 10 ++++++ .../internal/session/room/RoomModule.kt | 17 ++++----- .../session/room/read/MarkAllRoomsReadTask.kt | 35 +++++++++++++++++++ .../session/sync/ReadReceiptHandler.kt | 2 +- .../api/pushrules/PushrulesConditionTest.kt | 20 ++++++++--- .../debug/DebugMaterialThemeActivity.kt | 2 +- .../riotx/features/home/HomeActivity.kt | 2 +- .../home/room/list/RoomListActions.kt | 6 +--- .../home/room/list/RoomListFragment.kt | 14 ++++++++ .../home/room/list/RoomListViewModel.kt | 11 ++++++ vector/src/main/res/menu/room_list.xml | 9 +++++ vector/src/main/res/menu/vector_home.xml | 24 ------------- 14 files changed, 111 insertions(+), 47 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/MarkAllRoomsReadTask.kt create mode 100644 vector/src/main/res/menu/room_list.xml delete mode 100755 vector/src/main/res/menu/vector_home.xml diff --git a/CHANGES.md b/CHANGES.md index 42fb2cc291..dd7f47de03 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,7 @@ Improvements: - Persist active tab between sessions (#503) - Do not upload file too big for the homeserver (#587) - Handle read markers (#84) + - Mark all messages as read (#396) Other changes: - Accessibility improvements to read receipts in the room timeline and reactions emoji chooser diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt index 175d393c86..c7fedb2627 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt @@ -53,4 +53,9 @@ interface RoomService { * @return the [LiveData] of [RoomSummary] */ fun liveRoomSummaries(): LiveData> + + /** + * Mark all rooms as read + */ + fun markAllAsRead(roomIds: List, callback: MatrixCallback): Cancelable } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt index c64676c8c2..962b7b54d6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt @@ -32,6 +32,7 @@ import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.room.create.CreateRoomTask import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask +import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import io.realm.Realm @@ -41,6 +42,7 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona private val roomSummaryMapper: RoomSummaryMapper, private val createRoomTask: CreateRoomTask, private val joinRoomTask: JoinRoomTask, + private val markAllRoomsReadTask: MarkAllRoomsReadTask, private val roomFactory: RoomFactory, private val taskExecutor: TaskExecutor) : RoomService { @@ -80,4 +82,12 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona } .executeBy(taskExecutor) } + + override fun markAllAsRead(roomIds: List, callback: MatrixCallback): Cancelable { + return markAllRoomsReadTask + .configureWith(MarkAllRoomsReadTask.Params(roomIds)) { + this.callback = callback + } + .executeBy(taskExecutor) + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt index b3db84c9c6..5755d6b46e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt @@ -40,22 +40,14 @@ import im.vector.matrix.android.internal.session.room.membership.leaving.Default import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask import im.vector.matrix.android.internal.session.room.prune.DefaultPruneEventTask import im.vector.matrix.android.internal.session.room.prune.PruneEventTask +import im.vector.matrix.android.internal.session.room.read.DefaultMarkAllRoomsReadTask import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask +import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask -import im.vector.matrix.android.internal.session.room.relation.DefaultFetchEditHistoryTask -import im.vector.matrix.android.internal.session.room.relation.DefaultFindReactionEventForUndoTask -import im.vector.matrix.android.internal.session.room.relation.DefaultUpdateQuickReactionTask -import im.vector.matrix.android.internal.session.room.relation.FetchEditHistoryTask -import im.vector.matrix.android.internal.session.room.relation.FindReactionEventForUndoTask -import im.vector.matrix.android.internal.session.room.relation.UpdateQuickReactionTask +import im.vector.matrix.android.internal.session.room.relation.* import im.vector.matrix.android.internal.session.room.state.DefaultSendStateTask import im.vector.matrix.android.internal.session.room.state.SendStateTask import im.vector.matrix.android.internal.session.room.timeline.* -import im.vector.matrix.android.internal.session.room.timeline.ClearUnlinkedEventsTask -import im.vector.matrix.android.internal.session.room.timeline.DefaultGetContextOfEventTask -import im.vector.matrix.android.internal.session.room.timeline.DefaultPaginationTask -import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask -import im.vector.matrix.android.internal.session.room.timeline.PaginationTask import retrofit2.Retrofit @Module @@ -110,6 +102,9 @@ internal abstract class RoomModule { @Binds abstract fun bindSetReadMarkersTask(setReadMarkersTask: DefaultSetReadMarkersTask): SetReadMarkersTask + @Binds + abstract fun bindMarkAllRoomsReadTask(markAllRoomsReadTask: DefaultMarkAllRoomsReadTask): MarkAllRoomsReadTask + @Binds abstract fun bindFindReactionEventForUndoTask(findReactionEventForUndoTask: DefaultFindReactionEventForUndoTask): FindReactionEventForUndoTask diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/MarkAllRoomsReadTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/MarkAllRoomsReadTask.kt new file mode 100644 index 0000000000..99376a981a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/MarkAllRoomsReadTask.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019 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.matrix.android.internal.session.room.read + +import im.vector.matrix.android.internal.task.Task +import javax.inject.Inject + +internal interface MarkAllRoomsReadTask : Task { + data class Params( + val roomIds: List + ) +} + +internal class DefaultMarkAllRoomsReadTask @Inject constructor(private val readMarkersTask: SetReadMarkersTask) : MarkAllRoomsReadTask { + + override suspend fun execute(params: MarkAllRoomsReadTask.Params) { + params.roomIds.forEach { roomId -> + readMarkersTask.execute(SetReadMarkersTask.Params(roomId, markAllAsRead = true)) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/ReadReceiptHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/ReadReceiptHandler.kt index 4dbcc7168f..62fbd42ed5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/ReadReceiptHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/ReadReceiptHandler.kt @@ -25,7 +25,7 @@ import io.realm.Realm import timber.log.Timber import javax.inject.Inject -// the receipts dictionnaries +// the receipts dictionaries // key : $EventId // value : dict key $UserId // value dict key ts diff --git a/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt b/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt index 36aded79ad..882e171e4a 100644 --- a/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt +++ b/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt @@ -32,6 +32,7 @@ import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.api.util.Optional import org.junit.Assert import org.junit.Test @@ -172,7 +173,6 @@ class PushrulesConditionTest { } class MockRoomService() : RoomService { - override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback): Cancelable { TODO("not implemented") // To change body of created functions use File | Settings | File Templates. } @@ -192,9 +192,21 @@ class PushrulesConditionTest { override fun liveRoomSummaries(): LiveData> { return MutableLiveData() } + + override fun markAllAsRead(roomIds: List, callback: MatrixCallback): Cancelable { + TODO("not implemented") // To change body of created functions use File | Settings | File Templates. + } } class MockRoom(override val roomId: String, val _numberOfJoinedMembers: Int) : Room { + override fun getReadMarkerLive(): LiveData> { + TODO("not implemented") // To change body of created functions use File | Settings | File Templates. + } + + override fun getMyReadReceiptLive(): LiveData> { + TODO("not implemented") // To change body of created functions use File | Settings | File Templates. + } + override fun resendTextMessage(localEcho: TimelineEvent): Cancelable? { TODO("not implemented") // To change body of created functions use File | Settings | File Templates. } @@ -242,7 +254,7 @@ class PushrulesConditionTest { override fun fetchEditHistory(eventId: String, callback: MatrixCallback>) { } - override fun liveTimeLineEvent(eventId: String): LiveData { + override fun getTimeLineEventLive(eventId: String): LiveData> { TODO("not implemented") // To change body of created functions use File | Settings | File Templates. } @@ -250,7 +262,7 @@ class PushrulesConditionTest { return _numberOfJoinedMembers } - override fun liveRoomSummary(): LiveData { + override fun getRoomSummaryLive(): LiveData> { TODO("not implemented") // To change body of created functions use File | Settings | File Templates. } @@ -347,7 +359,7 @@ class PushrulesConditionTest { TODO("not implemented") // To change body of created functions use File | Settings | File Templates. } - override fun getEventSummaryLive(eventId: String): LiveData { + override fun getEventSummaryLive(eventId: String): LiveData> { TODO("not implemented") // To change body of created functions use File | Settings | File Templates. } diff --git a/vector/src/debug/java/im/vector/riotx/features/debug/DebugMaterialThemeActivity.kt b/vector/src/debug/java/im/vector/riotx/features/debug/DebugMaterialThemeActivity.kt index ab6b86801a..542b0a1cbb 100644 --- a/vector/src/debug/java/im/vector/riotx/features/debug/DebugMaterialThemeActivity.kt +++ b/vector/src/debug/java/im/vector/riotx/features/debug/DebugMaterialThemeActivity.kt @@ -59,7 +59,7 @@ abstract class DebugMaterialThemeActivity : AppCompatActivity() { } override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.vector_home, menu) + menuInflater.inflate(R.menu.home, menu) return true } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt index 9071b51acf..1e0121a500 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt @@ -182,7 +182,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { } } - return true + return super.onOptionsItemSelected(item) } override fun onBackPressed() { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListActions.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListActions.kt index 7302d9d2b8..8271086421 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListActions.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListActions.kt @@ -19,14 +19,10 @@ package im.vector.riotx.features.home.room.list import im.vector.matrix.android.api.session.room.model.RoomSummary sealed class RoomListActions { - data class SelectRoom(val roomSummary: RoomSummary) : RoomListActions() - data class ToggleCategory(val category: RoomCategory) : RoomListActions() - data class AcceptInvitation(val roomSummary: RoomSummary) : RoomListActions() - data class RejectInvitation(val roomSummary: RoomSummary) : RoomListActions() - data class FilterWith(val filter: String) : RoomListActions() + object MarkAllRoomsRead : RoomListActions() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt index d0957f752b..75bf15efa7 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt @@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.list import android.os.Bundle import android.os.Parcelable +import android.view.MenuItem import androidx.annotation.StringRes import androidx.core.content.ContextCompat import androidx.core.view.isVisible @@ -78,6 +79,19 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O injector.inject(this) } + override fun getMenuRes() = R.menu.room_list + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_home_mark_all_as_read -> { + roomListViewModel.accept(RoomListActions.MarkAllRoomsRead) + return true + } + } + + return super.onOptionsItemSelected(item) + } + override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) setupCreateRoomButton() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt index 0edf3b72e0..292e5405c4 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt @@ -78,6 +78,7 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room is RoomListActions.AcceptInvitation -> handleAcceptInvitation(action) is RoomListActions.RejectInvitation -> handleRejectInvitation(action) is RoomListActions.FilterWith -> handleFilter(action) + is RoomListActions.MarkAllRoomsRead -> handleMarkAllRoomsRead() } } @@ -193,6 +194,16 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room }) } + private fun handleMarkAllRoomsRead() = withState { state -> + state.asyncFilteredRooms.invoke() + ?.flatMap { it.value } + ?.filter { it.membership == Membership.JOIN } + ?.map { it.roomId } + ?.toList() + ?.let { session.markAllAsRead(it, object : MatrixCallback {}) } + } + + private fun buildRoomSummaries(rooms: List): RoomSummaries { val invites = ArrayList() val favourites = ArrayList() diff --git a/vector/src/main/res/menu/room_list.xml b/vector/src/main/res/menu/room_list.xml new file mode 100644 index 0000000000..60ffdcd87b --- /dev/null +++ b/vector/src/main/res/menu/room_list.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/menu/vector_home.xml b/vector/src/main/res/menu/vector_home.xml deleted file mode 100755 index fe24e6fdf5..0000000000 --- a/vector/src/main/res/menu/vector_home.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - From 0ca8696e882abae40928a3d037785e2935638bb8 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 11 Oct 2019 16:41:04 +0200 Subject: [PATCH 128/197] Attachments/Share: cleaning code and add contact picking --- vector/src/main/AndroidManifest.xml | 1 + .../riotx/core/utils/PermissionsTools.kt | 3 +- .../attachments/AttachmentTypeSelectorView.kt | 33 +++++++++++-------- .../features/attachments/AttachmentsHelper.kt | 24 +++++++++++++- .../features/attachments/AttachmentsMapper.kt | 22 ++++++++----- .../attachments/AttachmentsPickerCallback.kt | 25 +++++++++----- .../features/attachments/ContactAttachment.kt | 32 ++++++++++++++++++ .../attachments/PickerManagerFactory.kt | 17 ++++++++++ .../home/room/detail/RoomDetailFragment.kt | 15 ++++++--- .../features/share/IncomingShareActivity.kt | 13 +++----- .../layout/view_attachment_type_selector.xml | 12 +++---- vector/src/main/res/values/strings_riotX.xml | 9 +++++ 12 files changed, 153 insertions(+), 53 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/attachments/ContactAttachment.kt diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 1123335ceb..c56fc02eda 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ package="im.vector.riotx"> + (R.id.attachmentStickersButton).configure(Type.STICKER) audioButton = layout.findViewById(R.id.attachmentAudioButton).configure(Type.AUDIO) contactButton = layout.findViewById(R.id.attachmentContactButton).configure(Type.CONTACT) - contentView = layout + contentView = root width = LinearLayout.LayoutParams.MATCH_PARENT height = LinearLayout.LayoutParams.WRAP_CONTENT animationStyle = 0 @@ -197,7 +204,7 @@ class AttachmentTypeSelectorView(context: Context, } private fun ImageButton.configure(type: Type): ImageButton { - this.background = TextDrawable.builder().buildRound("", iconColorGenerator.getColor(type)) + this.background = TextDrawable.builder().buildRound("", iconColorGenerator.getColor(type.ordinal)) this.setOnClickListener(TypeClickListener(type)) return this } @@ -211,19 +218,17 @@ class AttachmentTypeSelectorView(context: Context, } - enum class Type { - - CAMERA, - GALLERY, - FILE, - STICKER, - AUDIO, - CONTACT; - - fun requirePermission(): Boolean { - return this != CAMERA && this != STICKER - } + /** + * The all possible types to pick with their required permissions. + */ + enum class Type(val permissionsBit: Int) { + CAMERA(PERMISSIONS_EMPTY), + GALLERY(PERMISSIONS_FOR_WRITING_FILES), + FILE(PERMISSIONS_FOR_WRITING_FILES), + STICKER(PERMISSIONS_EMPTY), + AUDIO(PERMISSIONS_FOR_WRITING_FILES), + CONTACT(PERMISSIONS_FOR_PICKING_CONTACT) } diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt index 46f25ca3e2..89a397c441 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt @@ -24,6 +24,7 @@ import com.kbeanie.multipicker.core.PickerManager import com.kbeanie.multipicker.utils.IntentUtils import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.riotx.core.platform.Restorable +import timber.log.Timber private const val CAPTURE_PATH_KEY = "CAPTURE_PATH_KEY" private const val PENDING_TYPE_KEY = "PENDING_TYPE_KEY" @@ -45,11 +46,17 @@ class AttachmentsHelper private constructor(private val pickerManagerFactory: Pi } interface Callback { - fun onAttachmentsReady(attachments: List) + fun onContactAttachmentReady(contactAttachment: ContactAttachment) { + Timber.v("On contact attachment ready: $contactAttachment") + } + + fun onContentAttachmentsReady(attachments: List) fun onAttachmentsProcessFailed() } + // Capture path allows to handle camera image picking. It must be restored if the activity gets killed. private var capturePath: String? = null + // The pending type is set if we have to handle permission request. It must be restored if the activity gets killed. var pendingType: AttachmentTypeSelectorView.Type? = null private val imagePicker by lazy { @@ -72,6 +79,10 @@ class AttachmentsHelper private constructor(private val pickerManagerFactory: Pi pickerManagerFactory.createAudioPicker() } + private val contactPicker by lazy { + pickerManagerFactory.createContactPicker() + } + // Restorable override fun onSaveInstanceState(outState: Bundle) { @@ -121,6 +132,13 @@ class AttachmentsHelper private constructor(private val pickerManagerFactory: Pi capturePath = cameraImagePicker.pickImage() } + /** + * Starts the process for handling contact picking + */ + fun selectContact() { + contactPicker.pickContact() + } + /** * This methods aims to handle on activity result data. * @@ -148,6 +166,8 @@ class AttachmentsHelper private constructor(private val pickerManagerFactory: Pi imagePicker.submit(IntentUtils.getPickerIntentForSharing(intent)) } else if (type.startsWith("video")) { videoPicker.submit(IntentUtils.getPickerIntentForSharing(intent)) + } else if (type.startsWith("audio")) { + videoPicker.submit(IntentUtils.getPickerIntentForSharing(intent)) } else if (type.startsWith("application") || type.startsWith("file") || type.startsWith("*")) { filePicker.submit(IntentUtils.getPickerIntentForSharing(intent)) } else { @@ -161,6 +181,8 @@ class AttachmentsHelper private constructor(private val pickerManagerFactory: Pi PICK_IMAGE_DEVICE -> imagePicker PICK_IMAGE_CAMERA -> cameraImagePicker PICK_FILE -> filePicker + PICK_CONTACT -> contactPicker + PICK_AUDIO -> audioPicker else -> null } } diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt index ca46936af9..5536e2094b 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt @@ -16,12 +16,18 @@ package im.vector.riotx.features.attachments -import com.kbeanie.multipicker.api.entity.ChosenAudio -import com.kbeanie.multipicker.api.entity.ChosenFile -import com.kbeanie.multipicker.api.entity.ChosenImage -import com.kbeanie.multipicker.api.entity.ChosenVideo +import com.kbeanie.multipicker.api.entity.* import im.vector.matrix.android.api.session.content.ContentAttachmentData +fun ChosenContact.toContactAttachment(): ContactAttachment { + return ContactAttachment( + displayName = displayName, + photoUri = photoUri, + emails = emails.toList(), + phones = phones.toList() + ) +} + fun ChosenFile.toContentAttachmentData(): ContentAttachmentData { return ContentAttachmentData( path = originalPath, @@ -61,8 +67,8 @@ fun ChosenImage.toContentAttachmentData(): ContentAttachmentData { type = mapType(), name = displayName, size = size, - height = height?.toLong(), - width = width?.toLong(), + height = height.toLong(), + width = width.toLong(), date = createdAt?.time ?: System.currentTimeMillis() ) } @@ -74,8 +80,8 @@ fun ChosenVideo.toContentAttachmentData(): ContentAttachmentData { type = ContentAttachmentData.Type.VIDEO, size = size, date = createdAt?.time ?: System.currentTimeMillis(), - height = height?.toLong(), - width = width?.toLong(), + height = height.toLong(), + width = width.toLong(), duration = duration, name = displayName ) diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsPickerCallback.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsPickerCallback.kt index 7df0b6f15b..e1fb2eec65 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsPickerCallback.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsPickerCallback.kt @@ -21,15 +21,22 @@ import com.kbeanie.multipicker.api.callbacks.ContactPickerCallback import com.kbeanie.multipicker.api.callbacks.FilePickerCallback import com.kbeanie.multipicker.api.callbacks.ImagePickerCallback import com.kbeanie.multipicker.api.callbacks.VideoPickerCallback -import com.kbeanie.multipicker.api.entity.ChosenAudio -import com.kbeanie.multipicker.api.entity.ChosenFile -import com.kbeanie.multipicker.api.entity.ChosenImage -import com.kbeanie.multipicker.api.entity.ChosenVideo +import com.kbeanie.multipicker.api.entity.* +import timber.log.Timber /** * This class delegates the PickerManager callbacks to an [AttachmentsHelper.Callback] */ -class AttachmentsPickerCallback(private val callback: AttachmentsHelper.Callback) : ImagePickerCallback, FilePickerCallback, VideoPickerCallback, AudioPickerCallback { +class AttachmentsPickerCallback(private val callback: AttachmentsHelper.Callback) : ImagePickerCallback, FilePickerCallback, VideoPickerCallback, AudioPickerCallback, ContactPickerCallback { + + override fun onContactChosen(contact: ChosenContact?) { + if (contact == null) { + callback.onAttachmentsProcessFailed() + } else { + val contactAttachment = contact.toContactAttachment() + callback.onContactAttachmentReady(contactAttachment) + } + } override fun onAudiosChosen(audios: MutableList?) { if (audios.isNullOrEmpty()) { @@ -38,7 +45,7 @@ class AttachmentsPickerCallback(private val callback: AttachmentsHelper.Callback val attachments = audios.map { it.toContentAttachmentData() } - callback.onAttachmentsReady(attachments) + callback.onContentAttachmentsReady(attachments) } } @@ -49,7 +56,7 @@ class AttachmentsPickerCallback(private val callback: AttachmentsHelper.Callback val attachments = files.map { it.toContentAttachmentData() } - callback.onAttachmentsReady(attachments) + callback.onContentAttachmentsReady(attachments) } } @@ -60,7 +67,7 @@ class AttachmentsPickerCallback(private val callback: AttachmentsHelper.Callback val attachments = images.map { it.toContentAttachmentData() } - callback.onAttachmentsReady(attachments) + callback.onContentAttachmentsReady(attachments) } } @@ -71,7 +78,7 @@ class AttachmentsPickerCallback(private val callback: AttachmentsHelper.Callback val attachments = videos.map { it.toContentAttachmentData() } - callback.onAttachmentsReady(attachments) + callback.onContentAttachmentsReady(attachments) } } diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/ContactAttachment.kt b/vector/src/main/java/im/vector/riotx/features/attachments/ContactAttachment.kt new file mode 100644 index 0000000000..dbbed4d5fc --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/attachments/ContactAttachment.kt @@ -0,0 +1,32 @@ +package im.vector.riotx.features.attachments + +/** + * Data class holding values of a picked contact + * Can be send as a text message waiting for the protocol to handle contact. + */ +data class ContactAttachment( + val displayName: String, + val photoUri: String?, + val phones: List = emptyList(), + val emails: List = emptyList() +) { + + fun toHumanReadable(): String { + val stringBuilder = StringBuilder(displayName) + phones.concatIn(stringBuilder) + emails.concatIn(stringBuilder) + return stringBuilder.toString() + } + + private fun List.concatIn(stringBuilder: StringBuilder) { + if (isNotEmpty()) { + stringBuilder.append("\n") + for (i in 0 until size - 1) { + val value = get(i) + stringBuilder.append(value).append("\n") + } + stringBuilder.append(last()) + } + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/PickerManagerFactory.kt b/vector/src/main/java/im/vector/riotx/features/attachments/PickerManagerFactory.kt index 23344b81ba..8bfe44308d 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/PickerManagerFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/PickerManagerFactory.kt @@ -25,6 +25,9 @@ import com.kbeanie.multipicker.api.FilePicker import com.kbeanie.multipicker.api.ImagePicker import com.kbeanie.multipicker.api.VideoPicker +/** + * Factory for creating different pickers. It allows to use with fragment or activity builders. + */ interface PickerManagerFactory { fun createImagePicker(): ImagePicker @@ -37,6 +40,8 @@ interface PickerManagerFactory { fun createAudioPicker(): AudioPicker + fun createContactPicker(): ContactPicker + } class ActivityPickerManagerFactory(private val activity: Activity, callback: AttachmentsHelper.Callback) : PickerManagerFactory { @@ -76,6 +81,12 @@ class ActivityPickerManagerFactory(private val activity: Activity, callback: Att it.setAudioPickerCallback(attachmentsPickerCallback) } } + + override fun createContactPicker(): ContactPicker { + return ContactPicker(activity).also { + it.setContactPickerCallback(attachmentsPickerCallback) + } + } } class FragmentPickerManagerFactory(private val fragment: Fragment, callback: AttachmentsHelper.Callback) : PickerManagerFactory { @@ -116,5 +127,11 @@ class FragmentPickerManagerFactory(private val fragment: Fragment, callback: Att } } + override fun createContactPicker(): ContactPicker { + return ContactPicker(fragment).also { + it.setContactPickerCallback(attachmentsPickerCallback) + } + } + } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index bdc6408e88..7b8a80939c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -80,6 +80,7 @@ import im.vector.riotx.core.utils.Debouncer import im.vector.riotx.core.utils.createUIHandler import im.vector.riotx.features.attachments.AttachmentTypeSelectorView import im.vector.riotx.features.attachments.AttachmentsHelper +import im.vector.riotx.features.attachments.ContactAttachment import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter @@ -1120,7 +1121,7 @@ class RoomDetailFragment : // AttachmentTypeSelectorView.Callback override fun onTypeSelected(type: AttachmentTypeSelectorView.Type) { - if (!type.requirePermission() || checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_PICK_ATTACHMENT)) { + if (checkPermissions(type.permissionsBit, this, PERMISSION_REQUEST_CODE_PICK_ATTACHMENT)) { launchAttachmentProcess(type) } else { attachmentsHelper.pendingType = type @@ -1133,19 +1134,23 @@ class RoomDetailFragment : AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile() AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery() AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio() - AttachmentTypeSelectorView.Type.CONTACT -> vectorBaseActivity.notImplemented("Picking contacts") + AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact() AttachmentTypeSelectorView.Type.STICKER -> vectorBaseActivity.notImplemented("Adding stickers") } } // AttachmentsHelper.Callback - override fun onAttachmentsReady(attachments: List) { - Timber.v("onAttachmentsReady") + override fun onContentAttachmentsReady(attachments: List) { roomDetailViewModel.process(RoomDetailActions.SendMedia(attachments)) } override fun onAttachmentsProcessFailed() { - Timber.v("onAttachmentsProcessFailed") + Toast.makeText(requireContext(), R.string.error_attachment, Toast.LENGTH_SHORT).show() + } + + override fun onContactAttachmentReady(contactAttachment: ContactAttachment) { + val formattedContact = contactAttachment.toHumanReadable() + roomDetailViewModel.process(RoomDetailActions.SendMessage(formattedContact, false)) } } diff --git a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt index 77fece859d..e5fbebf824 100644 --- a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt @@ -2,7 +2,6 @@ package im.vector.riotx.features.share import android.content.ClipDescription import android.content.Intent -import android.net.Uri import android.os.Bundle import android.widget.Toast import im.vector.matrix.android.api.session.content.ContentAttachmentData @@ -12,12 +11,10 @@ import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.replaceFragment import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.features.attachments.AttachmentsHelper -import im.vector.riotx.features.home.HomeActivity import im.vector.riotx.features.home.LoadingFragment import im.vector.riotx.features.home.room.list.RoomListFragment import im.vector.riotx.features.home.room.list.RoomListParams import im.vector.riotx.features.login.LoginActivity -import im.vector.riotx.features.login.LoginConfig import kotlinx.android.synthetic.main.activity_incoming_share.* import javax.inject.Inject @@ -25,7 +22,6 @@ import javax.inject.Inject class IncomingShareActivity : VectorBaseActivity(), AttachmentsHelper.Callback { - @Inject lateinit var sessionHolder: ActiveSessionHolder private lateinit var roomListFragment: RoomListFragment private lateinit var attachmentsHelper: AttachmentsHelper @@ -40,6 +36,8 @@ class IncomingShareActivity : override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + // If we are not logged in, stop the sharing process and open login screen. + // In the future, we might want to relaunch the sharing process after login. if (!sessionHolder.hasActiveSession()) { startLoginActivity() return @@ -63,7 +61,7 @@ class IncomingShareActivity : } } - override fun onAttachmentsReady(attachments: List) { + override fun onContentAttachmentsReady(attachments: List) { val roomListParams = RoomListParams(RoomListFragment.DisplayMode.SHARE, sharedData = SharedData.Attachments(attachments)) roomListFragment = RoomListFragment.newInstance(roomListParams) replaceFragment(roomListFragment, R.id.shareRoomListFragmentContainer) @@ -74,7 +72,7 @@ class IncomingShareActivity : } private fun cantManageShare() { - Toast.makeText(this, "Couldn't handle share data", Toast.LENGTH_LONG).show() + Toast.makeText(this, R.string.error_handling_incoming_share, Toast.LENGTH_LONG).show() finish() } @@ -93,9 +91,6 @@ class IncomingShareActivity : return false } - /** - * Start the login screen with identity server and home server pre-filled - */ private fun startLoginActivity() { val intent = LoginActivity.newIntent(this, null) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) diff --git a/vector/src/main/res/layout/view_attachment_type_selector.xml b/vector/src/main/res/layout/view_attachment_type_selector.xml index 772407ba3f..2af86d6c0d 100644 --- a/vector/src/main/res/layout/view_attachment_type_selector.xml +++ b/vector/src/main/res/layout/view_attachment_type_selector.xml @@ -34,7 +34,7 @@ + android:text="@string/attachment_type_camera" /> @@ -54,7 +54,7 @@ + android:text="@string/attachment_type_gallery" /> @@ -74,7 +74,7 @@ + android:text="@string/attachment_type_file" /> @@ -103,7 +103,7 @@ + android:text="@string/attachment_type_audio" /> @@ -123,7 +123,7 @@ + android:text="@string/attachment_type_contact" /> @@ -143,7 +143,7 @@ + android:text="@string/attachment_type_sticker" /> diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 370b17be22..6b861a1ecc 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -37,4 +37,13 @@ "The file '%1$s' (%2$s) is too large to upload. The limit is %3$s." + "An error occurred while retrieving the attachment." + "File" + "Contact" + "Camera" + "Audio" + "Gallery" + "Sticker" + Couldn\'t handle share data + From 946fc36a2679eddcc5e5dada4c3d49a73b3acf64 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 11 Oct 2019 17:12:51 +0200 Subject: [PATCH 129/197] Update contributing doc --- CONTRIBUTING.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0c1fd13d74..48f8e08f06 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,18 +40,25 @@ Please add a line to the top of the file `CHANGES.md` describing your change. Make sure the following commands execute without any error: +#### Internal tool + > ./tools/check/check_code_quality.sh +#### ktlint + > curl -sSLO https://github.com/pinterest/ktlint/releases/download/0.34.2/ktlint && chmod a+x ktlint -> ./ktlint --android -v +> ./ktlint --android --experimental -v Note that you can run -> ./ktlint --android -v -F +> ./ktlint --android --experimental -v -F -For ktlint to fix some detected errors for you +For ktlint to fix some detected errors for you (you still have to check and commit the fix of course) + +#### lint > ./gradlew lintGplayRelease +> ./gradlew lintFdroidRelease ### Unit tests @@ -61,7 +68,7 @@ Make sure the following commands execute without any error: ### Tests -RiotX is currently supported on Android Jelly Bean (API 16+): please test your change on an Android device (or Android emulator) running with API 16. Many issues can happen (including crashes) on older devices. +RiotX is currently supported on Android KitKat (API 19+): please test your change on an Android device (or Android emulator) running with API 19. Many issues can happen (including crashes) on older devices. Also, if possible, please test your change on a real device. Testing on Android emulator may not be sufficient. ### Internationalisation From 679b0fff985a9720015f72c047c265e1d10fd024 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 11 Oct 2019 17:12:53 +0200 Subject: [PATCH 130/197] Use klint and update CHANGES --- CHANGES.md | 1 + .../java/im/vector/riotx/core/extensions/ViewExtensions.kt | 2 +- .../java/im/vector/riotx/core/utils/KeyboardStateUtils.kt | 2 +- .../features/attachments/AttachmentTypeSelectorView.kt | 5 ----- .../vector/riotx/features/attachments/AttachmentsHelper.kt | 3 +-- .../features/attachments/AttachmentsPickerCallback.kt | 4 +--- .../vector/riotx/features/attachments/ContactAttachment.kt | 3 +-- .../riotx/features/attachments/PickerManagerFactory.kt | 3 --- .../riotx/features/home/room/detail/RoomDetailFragment.kt | 7 +------ .../vector/riotx/features/navigation/DefaultNavigator.kt | 1 - .../java/im/vector/riotx/features/navigation/Navigator.kt | 1 - .../vector/riotx/features/share/IncomingShareActivity.kt | 3 +-- .../main/java/im/vector/riotx/features/share/SharedData.kt | 3 +-- 13 files changed, 9 insertions(+), 29 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 42fb2cc291..5cac7e8f70 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,7 @@ Improvements: - Persist active tab between sessions (#503) - Do not upload file too big for the homeserver (#587) - Handle read markers (#84) + - Attachments: start using system pickers Other changes: - Accessibility improvements to read receipts in the room timeline and reactions emoji chooser diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/ViewExtensions.kt b/vector/src/main/java/im/vector/riotx/core/extensions/ViewExtensions.kt index 631df78b5e..e7bd2122b6 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/ViewExtensions.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/ViewExtensions.kt @@ -54,4 +54,4 @@ fun View.getMeasurements(): Pair { val width = measuredWidth val height = measuredHeight return width to height -} \ No newline at end of file +} diff --git a/vector/src/main/java/im/vector/riotx/core/utils/KeyboardStateUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/KeyboardStateUtils.kt index dd41d0d28b..d1db11c21d 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/KeyboardStateUtils.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/KeyboardStateUtils.kt @@ -20,4 +20,4 @@ class KeyboardStateUtils(activity: Activity) : ViewTreeObserver.OnGlobalLayoutLi val keypadHeight = screenHeight - rect.bottom isKeyboardShowing = keypadHeight > screenHeight * 0.15 } -} \ No newline at end of file +} diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentTypeSelectorView.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentTypeSelectorView.kt index d9fd3021e3..6d71d88eb2 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentTypeSelectorView.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentTypeSelectorView.kt @@ -141,7 +141,6 @@ class AttachmentTypeSelectorView(context: Context, button.startAnimation(animation) } - @TargetApi(Build.VERSION_CODES.LOLLIPOP) private fun animateWindowInCircular(anchor: View?, contentView: View) { val coordinates = getClickCoordinates(anchor, contentView) @@ -216,7 +215,6 @@ class AttachmentTypeSelectorView(context: Context, dismiss() callback?.onTypeSelected(type) } - } /** @@ -230,8 +228,5 @@ class AttachmentTypeSelectorView(context: Context, STICKER(PERMISSIONS_EMPTY), AUDIO(PERMISSIONS_FOR_WRITING_FILES), CONTACT(PERMISSIONS_FOR_PICKING_CONTACT) - } - - } diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt index 89a397c441..dd0d6cd79c 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt @@ -186,5 +186,4 @@ class AttachmentsHelper private constructor(private val pickerManagerFactory: Pi else -> null } } - -} \ No newline at end of file +} diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsPickerCallback.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsPickerCallback.kt index e1fb2eec65..5a2dd4d144 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsPickerCallback.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsPickerCallback.kt @@ -22,7 +22,6 @@ import com.kbeanie.multipicker.api.callbacks.FilePickerCallback import com.kbeanie.multipicker.api.callbacks.ImagePickerCallback import com.kbeanie.multipicker.api.callbacks.VideoPickerCallback import com.kbeanie.multipicker.api.entity.* -import timber.log.Timber /** * This class delegates the PickerManager callbacks to an [AttachmentsHelper.Callback] @@ -85,5 +84,4 @@ class AttachmentsPickerCallback(private val callback: AttachmentsHelper.Callback override fun onError(error: String?) { callback.onAttachmentsProcessFailed() } - -} \ No newline at end of file +} diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/ContactAttachment.kt b/vector/src/main/java/im/vector/riotx/features/attachments/ContactAttachment.kt index dbbed4d5fc..51c60ee4f6 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/ContactAttachment.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/ContactAttachment.kt @@ -28,5 +28,4 @@ data class ContactAttachment( stringBuilder.append(last()) } } - -} \ No newline at end of file +} diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/PickerManagerFactory.kt b/vector/src/main/java/im/vector/riotx/features/attachments/PickerManagerFactory.kt index 8bfe44308d..6c03f21ab3 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/PickerManagerFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/PickerManagerFactory.kt @@ -41,7 +41,6 @@ interface PickerManagerFactory { fun createAudioPicker(): AudioPicker fun createContactPicker(): ContactPicker - } class ActivityPickerManagerFactory(private val activity: Activity, callback: AttachmentsHelper.Callback) : PickerManagerFactory { @@ -132,6 +131,4 @@ class FragmentPickerManagerFactory(private val fragment: Fragment, callback: Att it.setContactPickerCallback(attachmentsPickerCallback) } } - } - diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 0599dce3d1..f30ee4d97d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -117,7 +117,6 @@ import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* import org.commonmark.parser.Parser import timber.log.Timber import java.io.File -import java.security.Key import javax.inject.Inject @Parcelize @@ -127,7 +126,6 @@ data class RoomDetailArgs( val sharedData: SharedData? = null ) : Parcelable - private const val REACTION_SELECT_REQUEST_CODE = 0 class RoomDetailFragment : @@ -207,7 +205,6 @@ class RoomDetailFragment : private var lockSendButton = false - override fun injectWith(injector: ScreenComponent) { injector.inject(this) } @@ -284,7 +281,6 @@ class RoomDetailFragment : null -> Timber.v("No share data to process") } } - } override fun onDestroy() { @@ -441,14 +437,13 @@ class RoomDetailFragment : ?: return val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT) ?: return - //TODO check if already reacted with that? + // TODO check if already reacted with that? roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId)) } } } } - // PRIVATE METHODS ***************************************************************************** private fun setupRecyclerView() { diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index 4a0852925e..a3f9c009ed 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -19,7 +19,6 @@ package im.vector.riotx.features.navigation import android.app.Activity import android.content.Context import android.content.Intent -import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom import im.vector.riotx.R import im.vector.riotx.core.platform.VectorBaseActivity diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt index 3591c1695c..4112dbbfc8 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt @@ -18,7 +18,6 @@ package im.vector.riotx.features.navigation import android.app.Activity import android.content.Context -import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom import im.vector.riotx.features.share.SharedData diff --git a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt index e5fbebf824..73f20cf201 100644 --- a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt @@ -18,7 +18,6 @@ import im.vector.riotx.features.login.LoginActivity import kotlinx.android.synthetic.main.activity_incoming_share.* import javax.inject.Inject - class IncomingShareActivity : VectorBaseActivity(), AttachmentsHelper.Callback { @@ -97,4 +96,4 @@ class IncomingShareActivity : startActivity(intent) finish() } -} \ No newline at end of file +} diff --git a/vector/src/main/java/im/vector/riotx/features/share/SharedData.kt b/vector/src/main/java/im/vector/riotx/features/share/SharedData.kt index 37776f004b..741d7b0eb3 100644 --- a/vector/src/main/java/im/vector/riotx/features/share/SharedData.kt +++ b/vector/src/main/java/im/vector/riotx/features/share/SharedData.kt @@ -27,5 +27,4 @@ sealed class SharedData: Parcelable { @Parcelize data class Attachments(val attachmentData: List): SharedData() - -} \ No newline at end of file +} From c57af9cf3e02384386f99a9dce30f2fab70e2680 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 11 Oct 2019 17:18:54 +0200 Subject: [PATCH 131/197] Better formatting --- CONTRIBUTING.md | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 48f8e08f06..d64dd7110e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,29 +42,39 @@ Make sure the following commands execute without any error: #### Internal tool -> ./tools/check/check_code_quality.sh +
+./tools/check/check_code_quality.sh
+
#### ktlint -> curl -sSLO https://github.com/pinterest/ktlint/releases/download/0.34.2/ktlint && chmod a+x ktlint -> ./ktlint --android --experimental -v +
+curl -sSLO https://github.com/pinterest/ktlint/releases/download/0.34.2/ktlint && chmod a+x ktlint
+./ktlint --android --experimental -v
+
Note that you can run -> ./ktlint --android --experimental -v -F +
+./ktlint --android --experimental -v -F
+
For ktlint to fix some detected errors for you (you still have to check and commit the fix of course) #### lint -> ./gradlew lintGplayRelease -> ./gradlew lintFdroidRelease +
+./gradlew lintGplayRelease
+./gradlew lintFdroidRelease
+
### Unit tests Make sure the following commands execute without any error: -> ./gradlew testGplayReleaseUnitTest +
+./gradlew testGplayReleaseUnitTest
+
### Tests From c28be6adb08be4c80bc98c30bd7bc63a722e0476 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 11 Oct 2019 17:23:25 +0200 Subject: [PATCH 132/197] Fix code quality check --- .../features/attachments/AttachmentsPickerCallback.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsPickerCallback.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsPickerCallback.kt index 5a2dd4d144..dc7b028aba 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsPickerCallback.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsPickerCallback.kt @@ -26,7 +26,12 @@ import com.kbeanie.multipicker.api.entity.* /** * This class delegates the PickerManager callbacks to an [AttachmentsHelper.Callback] */ -class AttachmentsPickerCallback(private val callback: AttachmentsHelper.Callback) : ImagePickerCallback, FilePickerCallback, VideoPickerCallback, AudioPickerCallback, ContactPickerCallback { +class AttachmentsPickerCallback(private val callback: AttachmentsHelper.Callback) + : ImagePickerCallback, + FilePickerCallback, + VideoPickerCallback, + AudioPickerCallback, + ContactPickerCallback { override fun onContactChosen(contact: ChosenContact?) { if (contact == null) { From e28e2dadb94d22f6a2d68e4b90030e9d62a1a29a Mon Sep 17 00:00:00 2001 From: Dominic Fischer Date: Sat, 12 Oct 2019 15:37:20 +0100 Subject: [PATCH 133/197] Some more clean up Signed-off-by: Dominic Fischer --- .../api/extensions/MatrixSdkExtensions.kt | 5 +- .../android/api/permalinks/MatrixLinkify.kt | 4 +- .../api/permalinks/PermalinkFactory.kt | 11 +- .../pushrules/ContainsDisplayNameCondition.kt | 14 +- .../android/api/session/events/model/Event.kt | 35 ++- .../api/session/room/model/PowerLevels.kt | 48 ++-- .../room/model/create/CreateRoomParams.kt | 10 +- .../internal/crypto/DefaultCryptoService.kt | 60 ++--- .../internal/crypto/DeviceListManager.kt | 112 ++++------ .../crypto/IncomingRoomKeyRequestManager.kt | 14 +- .../crypto/MXMegolmExportEncryption.kt | 50 ++--- .../android/internal/crypto/MXOlmDevice.kt | 14 +- .../internal/crypto/MyDeviceInfoHolder.kt | 14 +- .../internal/crypto/OneTimeKeysUploader.kt | 15 +- .../internal/crypto/RoomDecryptorProvider.kt | 10 +- .../EnsureOlmSessionsForDevicesAction.kt | 33 +-- .../EnsureOlmSessionsForUsersAction.kt | 28 +-- .../crypto/actions/MessageEncrypter.kt | 40 ++-- .../algorithms/megolm/MXMegolmDecryption.kt | 20 +- .../algorithms/megolm/MXMegolmEncryption.kt | 34 +-- .../crypto/algorithms/olm/MXOlmEncryption.kt | 13 +- .../internal/crypto/keysbackup/KeysBackup.kt | 211 +++++++----------- .../model/OlmInboundGroupSessionWrapper.kt | 4 +- .../crypto/store/db/RealmCryptoStore.kt | 5 +- .../ClaimOneTimeKeysForUsersDeviceTask.kt | 18 +- .../crypto/tasks/DownloadKeysForUsersTask.kt | 12 +- .../crypto/tasks/SetDeviceNameTask.kt | 3 +- .../internal/network/UserAgentHolder.kt | 5 +- .../DefaultContentUploadStateTracker.kt | 4 +- .../session/group/DefaultGetGroupDataTask.kt | 6 +- .../notification/ProcessEventForPushTask.kt | 47 ++-- .../session/room/membership/RoomMembers.kt | 3 +- .../relation/FindReactionEventForUndoTask.kt | 35 ++- .../room/timeline/TimelineEventDecryptor.kt | 21 +- .../securestorage/SecretStoringUtils.kt | 6 +- .../session/sync/CryptoSyncHandler.kt | 5 +- .../internal/session/sync/job/SyncService.kt | 3 +- .../user/accountdata/DirectChatsHelper.kt | 16 +- .../android/internal/task/ConfigurableTask.kt | 2 +- .../android/internal/util/CompatUtil.kt | 10 +- .../matrix/android/internal/util/Hash.kt | 9 +- .../fcm/VectorFirebaseMessagingService.kt | 5 +- .../riotx/core/dialogs/ExportKeysDialog.kt | 7 +- .../core/intent/ExternalIntentAnalyser.kt | 59 ++--- .../core/preference/BingRulePreference.kt | 15 +- .../riotx/core/resources/ResourceUtils.kt | 3 +- .../core/ui/views/NotificationAreaView.kt | 3 +- .../im/vector/riotx/core/utils/FileUtils.kt | 36 +-- .../riotx/core/utils/PermissionsTools.kt | 14 +- .../im/vector/riotx/core/utils/TextUtils.kt | 6 +- .../setup/KeysBackupSetupStep2Fragment.kt | 7 +- .../crypto/keysrequest/KeyRequestHandler.kt | 12 +- .../home/room/detail/RoomDetailViewModel.kt | 24 +- .../home/room/list/RoomListViewModel.kt | 16 +- .../homeserver/ServerUrlsRepository.kt | 5 +- .../features/rageshake/BugReportActivity.kt | 3 +- .../riotx/features/rageshake/BugReporter.kt | 141 ++++-------- .../riotx/features/settings/FontScale.kt | 17 +- .../riotx/features/settings/VectorLocale.kt | 26 +-- .../features/settings/VectorPreferences.kt | 57 ++--- .../VectorSettingsPreferencesFragment.kt | 3 +- .../VectorSettingsSecurityPrivacyFragment.kt | 30 +-- 62 files changed, 549 insertions(+), 949 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/MatrixSdkExtensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/MatrixSdkExtensions.kt index 95489da3fb..685a522f60 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/MatrixSdkExtensions.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/MatrixSdkExtensions.kt @@ -19,7 +19,6 @@ package im.vector.matrix.android.api.extensions import im.vector.matrix.android.api.comparators.DatedObjectComparators import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo -import java.util.Collections /* ========================================================================================== * MXDeviceInfo @@ -29,6 +28,6 @@ fun MXDeviceInfo.getFingerprintHumanReadable() = fingerprint() ?.chunked(4) ?.joinToString(separator = " ") -fun List.sortByLastSeen() { - Collections.sort(this, DatedObjectComparators.descComparator) +fun MutableList.sortByLastSeen() { + sortWith(DatedObjectComparators.descComparator) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/MatrixLinkify.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/MatrixLinkify.kt index a4b6d27535..fc02cf4a61 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/MatrixLinkify.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/MatrixLinkify.kt @@ -30,9 +30,9 @@ object MatrixLinkify { * * @param spannable the text in which the matrix items has to be clickable. */ - fun addLinks(spannable: Spannable?, callback: MatrixPermalinkSpan.Callback?): Boolean { + fun addLinks(spannable: Spannable, callback: MatrixPermalinkSpan.Callback?): Boolean { // sanity checks - if (spannable.isNullOrEmpty()) { + if (spannable.isEmpty()) { return false } val text = spannable.toString() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkFactory.kt index 5cff69b2c1..1af77869ee 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkFactory.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.api.permalinks -import android.text.TextUtils import im.vector.matrix.android.api.session.events.model.Event /** @@ -48,7 +47,7 @@ object PermalinkFactory { * @return the permalink, or null in case of error */ fun createPermalink(id: String): String? { - return if (TextUtils.isEmpty(id)) { + return if (id.isEmpty()) { null } else MATRIX_TO_URL_BASE + escape(id) } @@ -71,11 +70,11 @@ object PermalinkFactory { * @param url the universal link, Ex: "https://matrix.to/#/@benoit:matrix.org" * @return the id from the url, ex: "@benoit:matrix.org", or null if the url is not a permalink */ - fun getLinkedId(url: String?): String? { - val isSupported = url != null && url.startsWith(MATRIX_TO_URL_BASE) + fun getLinkedId(url: String): String? { + val isSupported = url.startsWith(MATRIX_TO_URL_BASE) return if (isSupported) { - url!!.substring(MATRIX_TO_URL_BASE.length) + url.substring(MATRIX_TO_URL_BASE.length) } else null } @@ -86,6 +85,6 @@ object PermalinkFactory { * @return the escaped id */ private fun escape(id: String): String { - return id.replace("/".toRegex(), "%2F") + return id.replace("/", "%2F") } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/ContainsDisplayNameCondition.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/ContainsDisplayNameCondition.kt index 17160aa5ec..166ec4f05f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/ContainsDisplayNameCondition.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/ContainsDisplayNameCondition.kt @@ -15,13 +15,11 @@ */ package im.vector.matrix.android.api.pushrules -import android.text.TextUtils import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.message.MessageContent import timber.log.Timber -import java.util.regex.Pattern class ContainsDisplayNameCondition : Condition(Kind.contains_display_name) { @@ -34,7 +32,7 @@ class ContainsDisplayNameCondition : Condition(Kind.contains_display_name) { } fun isSatisfied(event: Event, displayName: String): Boolean { - var message = when (event.type) { + val message = when (event.type) { EventType.MESSAGE -> { event.content.toModel() } @@ -59,20 +57,18 @@ class ContainsDisplayNameCondition : Condition(Kind.contains_display_name) { */ fun caseInsensitiveFind(subString: String, longString: String): Boolean { // add sanity checks - if (TextUtils.isEmpty(subString) || TextUtils.isEmpty(longString)) { + if (subString.isEmpty() || longString.isEmpty()) { return false } - var res = false - try { - val pattern = Pattern.compile("(\\W|^)" + Pattern.quote(subString) + "(\\W|$)", Pattern.CASE_INSENSITIVE) - res = pattern.matcher(longString).find() + val regex = Regex("(\\W|^)" + Regex.escape(subString) + "(\\W|$)", RegexOption.IGNORE_CASE) + return regex.containsMatchIn(longString) } catch (e: Exception) { Timber.e(e, "## caseInsensitiveFind() : failed") } - return res + return false } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt index c723009771..7aea73233d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.api.session.events.model -import android.text.TextUtils import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.session.crypto.MXCryptoError @@ -35,18 +34,16 @@ typealias Content = JsonDict * This methods is a facility method to map a json content to a model. */ inline fun Content?.toModel(catchError: Boolean = true): T? { - return this?.let { - val moshi = MoshiProvider.providesMoshi() - val moshiAdapter = moshi.adapter(T::class.java) - return try { - moshiAdapter.fromJsonValue(it) - } catch (e: Exception) { - if (catchError) { - Timber.e(e, "To model failed : $e") - null - } else { - throw e - } + val moshi = MoshiProvider.providesMoshi() + val moshiAdapter = moshi.adapter(T::class.java) + return try { + moshiAdapter.fromJsonValue(this) + } catch (e: Exception) { + if (catchError) { + Timber.e(e, "To model failed : $e") + null + } else { + throw e } } } @@ -55,12 +52,10 @@ inline fun Content?.toModel(catchError: Boolean = true): T? { * This methods is a facility method to map a model to a json Content */ @Suppress("UNCHECKED_CAST") -inline fun T?.toContent(): Content? { - return this?.let { - val moshi = MoshiProvider.providesMoshi() - val moshiAdapter = moshi.adapter(T::class.java) - return moshiAdapter.toJsonValue(it) as Content - } +inline fun T.toContent(): Content { + val moshi = MoshiProvider.providesMoshi() + val moshiAdapter = moshi.adapter(T::class.java) + return moshiAdapter.toJsonValue(this) as Content } /** @@ -106,7 +101,7 @@ data class Event( * @return true if this event is encrypted. */ fun isEncrypted(): Boolean { - return TextUtils.equals(type, EventType.ENCRYPTED) + return type == EventType.ENCRYPTED } /** diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/PowerLevels.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/PowerLevels.kt index 196f18404c..27f7820156 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/PowerLevels.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/PowerLevels.kt @@ -16,11 +16,9 @@ package im.vector.matrix.android.api.session.room.model -import android.text.TextUtils import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.session.events.model.EventType -import java.util.* /** * Class representing the EventType.EVENT_TYPE_STATE_ROOM_POWER_LEVELS state event content. @@ -45,14 +43,8 @@ data class PowerLevels( * @param userId the user id * @return the power level */ - fun getUserPowerLevel(userId: String): Int { - // sanity check - if (!TextUtils.isEmpty(userId)) { - val powerLevel = users[userId] - return powerLevel ?: usersDefault - } - - return usersDefault + fun getUserPowerLevel(userId: String): Int { + return users.getOrElse(userId) { usersDefault } } /** @@ -61,10 +53,8 @@ data class PowerLevels( * @param userId the user * @param powerLevel the new power level */ - fun setUserPowerLevel(userId: String?, powerLevel: Int) { - if (null != userId) { - users[userId] = Integer.valueOf(powerLevel) - } + fun setUserPowerLevel(userId: String, powerLevel: Int) { + users[userId] = powerLevel } /** @@ -74,8 +64,8 @@ data class PowerLevels( * @param userId the user id * @return true if the user can send the event */ - fun maySendEventOfType(eventTypeString: String, userId: String): Boolean { - return if (!TextUtils.isEmpty(eventTypeString) && !TextUtils.isEmpty(userId)) { + fun maySendEventOfType(eventTypeString: String, userId: String): Boolean { + return if (eventTypeString.isNotEmpty() && userId.isNotEmpty()) { getUserPowerLevel(userId) >= minimumPowerLevelForSendingEventAsMessage(eventTypeString) } else false } @@ -86,8 +76,8 @@ data class PowerLevels( * @param userId the user id * @return true if the user can send a room message */ - fun maySendMessage(userId: String): Boolean { - return maySendEventOfType(EventType.MESSAGE, userId) + fun maySendMessage(userId: String): Boolean { + return maySendEventOfType(EventType.MESSAGE, userId) } /** @@ -97,7 +87,7 @@ data class PowerLevels( * @param eventTypeString the type of event (in Event.EVENT_TYPE_XXX values) * @return the required minimum power level. */ - fun minimumPowerLevelForSendingEventAsMessage(eventTypeString: String?): Int { + fun minimumPowerLevelForSendingEventAsMessage(eventTypeString: String?): Int { return events[eventTypeString] ?: eventsDefault } @@ -108,7 +98,7 @@ data class PowerLevels( * @param eventTypeString the type of event (in Event.EVENT_TYPE_STATE_ values). * @return the required minimum power level. */ - fun minimumPowerLevelForSendingEventAsStateEvent(eventTypeString: String?): Int { + fun minimumPowerLevelForSendingEventAsStateEvent(eventTypeString: String?): Int { return events[eventTypeString] ?: stateDefault } @@ -118,18 +108,14 @@ data class PowerLevels( * @param key the notification key * @return the level */ - fun notificationLevel(key: String?): Int { - if (null != key && notifications.containsKey(key)) { - val valAsVoid = notifications[key] + fun notificationLevel(key: String): Int { + val valAsVoid = notifications[key] ?: return 50 - // the first implementation was a string value - return if (valAsVoid is String) { - Integer.parseInt(valAsVoid) - } else { - valAsVoid as Int - } + // the first implementation was a string value + return if (valAsVoid is String) { + valAsVoid.toInt() + } else { + valAsVoid as Int } - - return 50 } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt index 35109501e4..598aab2d30 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt @@ -145,15 +145,7 @@ class CreateRoomParams { */ fun setHistoryVisibility(historyVisibility: RoomHistoryVisibility?) { // Remove the existing value if any. - if (initialStates != null && !initialStates!!.isEmpty()) { - val newInitialStates = ArrayList() - for (event in initialStates!!) { - if (event.getClearType() != EventType.STATE_HISTORY_VISIBILITY) { - newInitialStates.add(event) - } - } - initialStates = newInitialStates - } + initialStates?.removeAll { it.getClearType() == EventType.STATE_HISTORY_VISIBILITY } if (historyVisibility != null) { val contentMap = HashMap() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt index 853360d12f..d3d96c16d1 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt @@ -359,29 +359,16 @@ internal class DefaultCryptoService @Inject constructor( */ override fun setDevicesKnown(devices: List, callback: MatrixCallback?) { // build a devices map - val devicesIdListByUserId = HashMap>() + val devicesIdListByUserId = devices.groupBy({ it.userId }, { it.deviceId }) - for (di in devices) { - var deviceIdsList: MutableList? = devicesIdListByUserId[di.userId]?.toMutableList() - - if (null == deviceIdsList) { - deviceIdsList = ArrayList() - devicesIdListByUserId[di.userId] = deviceIdsList - } - deviceIdsList.add(di.deviceId) - } - - val userIds = devicesIdListByUserId.keys - - for (userId in userIds) { + for ((userId, deviceIds) in devicesIdListByUserId) { val storedDeviceIDs = cryptoStore.getUserDevices(userId) // sanity checks if (null != storedDeviceIDs) { var isUpdated = false - val deviceIds = devicesIdListByUserId[userId] - deviceIds?.forEach { deviceId -> + deviceIds.forEach { deviceId -> val device = storedDeviceIDs[deviceId] // assume if the device is either verified or blocked @@ -549,16 +536,10 @@ internal class DefaultCryptoService @Inject constructor( val t0 = System.currentTimeMillis() Timber.v("## encryptEventContent() starts") runCatching { - safeAlgorithm.encryptEventContent(eventContent, eventType, userIds) - } - .fold( - { - Timber.v("## encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms") - callback.onSuccess(MXEncryptEventContentResult(it, EventType.ENCRYPTED)) - }, - { callback.onFailure(it) } - - ) + val content = safeAlgorithm.encryptEventContent(eventContent, eventType, userIds) + Timber.v("## encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms") + MXEncryptEventContentResult(content, EventType.ENCRYPTED) + }.foldToCallback(callback) } else { val algorithm = getEncryptionAlgorithm(roomId) val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON, @@ -776,7 +757,7 @@ internal class DefaultCryptoService @Inject constructor( GlobalScope.launch(coroutineDispatchers.main) { runCatching { exportRoomKeys(password, MXMegolmExportEncryption.DEFAULT_ITERATION_COUNT) - }.fold(callback::onSuccess, callback::onFailure) + }.foldToCallback(callback) } } @@ -813,8 +794,8 @@ internal class DefaultCryptoService @Inject constructor( progressListener: ProgressListener?, callback: MatrixCallback) { GlobalScope.launch(coroutineDispatchers.main) { - withContext(coroutineDispatchers.crypto) { - Try { + runCatching { + withContext(coroutineDispatchers.crypto) { Timber.v("## importRoomKeys starts") val t0 = System.currentTimeMillis() @@ -861,19 +842,14 @@ internal class DefaultCryptoService @Inject constructor( fun checkUnknownDevices(userIds: List, callback: MatrixCallback) { // force the refresh to ensure that the devices list is up-to-date GlobalScope.launch(coroutineDispatchers.crypto) { - runCatching { deviceListManager.downloadKeys(userIds, true) } - .fold( - { - val unknownDevices = getUnknownDevices(it) - if (unknownDevices.map.isEmpty()) { - callback.onSuccess(Unit) - } else { - // trigger an an unknown devices exception - callback.onFailure(Failure.CryptoError(MXCryptoError.UnknownDevice(unknownDevices))) - } - }, - { callback.onFailure(it) } - ) + runCatching { + val keys = deviceListManager.downloadKeys(userIds, true) + val unknownDevices = getUnknownDevices(keys) + if (unknownDevices.map.isNotEmpty()) { + // trigger an an unknown devices exception + throw Failure.CryptoError(MXCryptoError.UnknownDevice(unknownDevices)) + } + }.foldToCallback(callback) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt index be72851d97..5873bb25aa 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.internal.crypto -import android.text.TextUtils import im.vector.matrix.android.api.MatrixPatterns import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo @@ -27,7 +26,6 @@ import im.vector.matrix.android.internal.crypto.tasks.DownloadKeysForUsersTask import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.sync.SyncTokenStore import timber.log.Timber -import java.util.* import javax.inject.Inject // Legacy name: MXDeviceList @@ -39,13 +37,12 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM private val downloadKeysForUsersTask: DownloadKeysForUsersTask) { // HS not ready for retry - private val notReadyToRetryHS = HashSet() + private val notReadyToRetryHS = mutableSetOf() init { var isUpdated = false val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() - for (userId in deviceTrackingStatuses.keys) { - val status = deviceTrackingStatuses[userId]!! + for ((userId, status) in deviceTrackingStatuses) { if (TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == status || TRACKING_STATUS_UNREACHABLE_SERVER == status) { // if a download was in progress when we got shut down, it isn't any more. deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD @@ -66,7 +63,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM private fun canRetryKeysDownload(userId: String): Boolean { var res = false - if (!TextUtils.isEmpty(userId) && userId.contains(":")) { + if (userId.isNotEmpty() && userId.contains(":")) { try { synchronized(notReadyToRetryHS) { res = !notReadyToRetryHS.contains(userId.substring(userId.lastIndexOf(":") + 1)) @@ -119,27 +116,23 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM * @param changed the user ids list which have new devices * @param left the user ids list which left a room */ - fun handleDeviceListsChanges(changed: List?, left: List?) { + fun handleDeviceListsChanges(changed: Collection, left: Collection) { var isUpdated = false val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() - if (changed?.isNotEmpty() == true) { - for (userId in changed) { - if (deviceTrackingStatuses.containsKey(userId)) { - Timber.v("## invalidateUserDeviceList() : Marking device list outdated for $userId") - deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD - isUpdated = true - } + for (userId in changed) { + if (deviceTrackingStatuses.containsKey(userId)) { + Timber.v("## invalidateUserDeviceList() : Marking device list outdated for $userId") + deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD + isUpdated = true } } - if (left?.isNotEmpty() == true) { - for (userId in left) { - if (deviceTrackingStatuses.containsKey(userId)) { - Timber.v("## invalidateUserDeviceList() : No longer tracking device list for $userId") - deviceTrackingStatuses[userId] = TRACKING_STATUS_NOT_TRACKED - isUpdated = true - } + for (userId in left) { + if (deviceTrackingStatuses.containsKey(userId)) { + Timber.v("## invalidateUserDeviceList() : No longer tracking device list for $userId") + deviceTrackingStatuses[userId] = TRACKING_STATUS_NOT_TRACKED + isUpdated = true } } @@ -153,7 +146,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM * + update */ fun invalidateAllDeviceLists() { - handleDeviceListsChanges(ArrayList(cryptoStore.getDeviceTrackingStatuses().keys), null) + handleDeviceListsChanges(cryptoStore.getDeviceTrackingStatuses().keys, emptyList()) } /** @@ -163,9 +156,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM */ private fun onKeysDownloadFailed(userIds: List) { val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() - for (userId in userIds) { - deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD - } + userIds.associateWithTo(deviceTrackingStatuses) { TRACKING_STATUS_PENDING_DOWNLOAD } cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) } @@ -177,21 +168,15 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM */ private fun onKeysDownloadSucceed(userIds: List, failures: Map>?): MXUsersDevicesMap { if (failures != null) { - val keys = failures.keys - for (k in keys) { - val value = failures[k] - if (value!!.containsKey("status")) { - val statusCodeAsVoid = value["status"] - var statusCode = 0 - if (statusCodeAsVoid is Double) { - statusCode = statusCodeAsVoid.toInt() - } else if (statusCodeAsVoid is Int) { - statusCode = statusCodeAsVoid.toInt() - } - if (statusCode == 503) { - synchronized(notReadyToRetryHS) { - notReadyToRetryHS.add(k) - } + for ((k, value) in failures) { + val statusCode = when (val status = value["status"]) { + is Double -> status.toInt() + is Int -> status.toInt() + else -> 0 + } + if (statusCode == 503) { + synchronized(notReadyToRetryHS) { + notReadyToRetryHS.add(k) } } } @@ -228,11 +213,9 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM /** * Download the device keys for a list of users and stores the keys in the MXStore. * It must be called in getEncryptingThreadHandler() thread. - * The callback is called in the UI thread. * * @param userIds The users to fetch. * @param forceDownload Always download the keys even if cached. - * @param callback the asynchronous callback */ suspend fun downloadKeys(userIds: List?, forceDownload: Boolean): MXUsersDevicesMap { Timber.v("## downloadKeys() : forceDownload $forceDownload : $userIds") @@ -270,7 +253,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM Timber.v("## downloadKeys() : starts") val t0 = System.currentTimeMillis() val result = doKeyDownloadForUsers(downloadUsers) - Timber.v("## downloadKeys() : doKeyDownloadForUsers succeeds after " + (System.currentTimeMillis() - t0) + " ms") + Timber.v("## downloadKeys() : doKeyDownloadForUsers succeeds after ${System.currentTimeMillis() - t0} ms") result.also { it.addEntriesFromMap(stored) } @@ -303,16 +286,14 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM val devices = response.deviceKeys?.get(userId) Timber.v("## doKeyDownloadForUsers() : Got keys for $userId : $devices") if (devices != null) { - val mutableDevices = HashMap(devices) - val deviceIds = ArrayList(mutableDevices.keys) - for (deviceId in deviceIds) { + val mutableDevices = devices.toMutableMap() + for ((deviceId, deviceInfo) in devices) { // Get the potential previously store device keys for this device val previouslyStoredDeviceKeys = cryptoStore.getUserDevice(deviceId, userId) - val deviceInfo = mutableDevices[deviceId] // in some race conditions (like unit tests) // the self device must be seen as verified - if (TextUtils.equals(deviceInfo!!.deviceId, credentials.deviceId) && TextUtils.equals(userId, credentials.userId)) { + if (deviceInfo.deviceId == credentials.deviceId && userId == credentials.userId) { deviceInfo.verified = MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED } // Validate received keys @@ -365,13 +346,13 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM } // Check that the user_id and device_id in the received deviceKeys are correct - if (!TextUtils.equals(deviceKeys.userId, userId)) { - Timber.e("## validateDeviceKeys() : Mismatched user_id " + deviceKeys.userId + " from " + userId + ":" + deviceId) + if (deviceKeys.userId != userId) { + Timber.e("## validateDeviceKeys() : Mismatched user_id ${deviceKeys.userId} from $userId:$deviceId") return false } - if (!TextUtils.equals(deviceKeys.deviceId, deviceId)) { - Timber.e("## validateDeviceKeys() : Mismatched device_id " + deviceKeys.deviceId + " from " + userId + ":" + deviceId) + if (deviceKeys.deviceId != deviceId) { + Timber.e("## validateDeviceKeys() : Mismatched device_id ${deviceKeys.deviceId} from $userId:$deviceId") return false } @@ -379,21 +360,21 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM val signKey = deviceKeys.keys?.get(signKeyId) if (null == signKey) { - Timber.e("## validateDeviceKeys() : Device " + userId + ":" + deviceKeys.deviceId + " has no ed25519 key") + Timber.e("## validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no ed25519 key") return false } val signatureMap = deviceKeys.signatures?.get(userId) if (null == signatureMap) { - Timber.e("## validateDeviceKeys() : Device " + userId + ":" + deviceKeys.deviceId + " has no map for " + userId) + Timber.e("## validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no map for $userId") return false } val signature = signatureMap[signKeyId] if (null == signature) { - Timber.e("## validateDeviceKeys() : Device " + userId + ":" + deviceKeys.deviceId + " is not signed") + Timber.e("## validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} is not signed") return false } @@ -414,7 +395,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM } if (null != previouslyStoredDeviceKeys) { - if (!TextUtils.equals(previouslyStoredDeviceKeys.fingerprint(), signKey)) { + if (previouslyStoredDeviceKeys.fingerprint() != signKey) { // This should only happen if the list has been MITMed; we are // best off sticking with the original keys. // @@ -424,7 +405,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM + previouslyStoredDeviceKeys.fingerprint() + " -> " + signKey) Timber.e("## validateDeviceKeys() : $previouslyStoredDeviceKeys -> $deviceKeys") - Timber.e("## validateDeviceKeys() : " + previouslyStoredDeviceKeys.keys + " -> " + deviceKeys.keys) + Timber.e("## validateDeviceKeys() : ${previouslyStoredDeviceKeys.keys} -> ${deviceKeys.keys}") return false } @@ -438,27 +419,18 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM * This method must be called on getEncryptingThreadHandler() thread. */ suspend fun refreshOutdatedDeviceLists() { - val users = ArrayList() - val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() - for (userId in deviceTrackingStatuses.keys) { - if (TRACKING_STATUS_PENDING_DOWNLOAD == deviceTrackingStatuses[userId]) { - users.add(userId) - } + val users = deviceTrackingStatuses.keys.filterTo(mutableListOf()) { userId -> + TRACKING_STATUS_PENDING_DOWNLOAD == deviceTrackingStatuses[userId] } - if (users.size == 0) { + if (users.isEmpty()) { return } // update the statuses - for (userId in users) { - val status = deviceTrackingStatuses[userId] - if (null != status && TRACKING_STATUS_PENDING_DOWNLOAD == status) { - deviceTrackingStatuses.put(userId, TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) - } - } + users.associateWithTo(deviceTrackingStatuses) { TRACKING_STATUS_DOWNLOAD_IN_PROGRESS } cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) runCatching { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt index 10a2cb00be..368293d92f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.crypto -import android.text.TextUtils import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener import im.vector.matrix.android.api.session.events.model.Event @@ -25,7 +24,6 @@ import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyShare import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.session.SessionScope import timber.log.Timber -import java.util.* import javax.inject.Inject import kotlin.collections.ArrayList @@ -58,7 +56,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor( when (roomKeyShare?.action) { RoomKeyShare.ACTION_SHARE_REQUEST -> receivedRoomKeyRequests.add(IncomingRoomKeyRequest(event)) RoomKeyShare.ACTION_SHARE_CANCELLATION -> receivedRoomKeyRequestCancellations.add(IncomingRoomKeyRequestCancellation(event)) - else -> Timber.e("## onRoomKeyRequestEvent() : unsupported action " + roomKeyShare?.action) + else -> Timber.e("## onRoomKeyRequestEvent() : unsupported action ${roomKeyShare?.action}") } } @@ -68,7 +66,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor( * It must be called on CryptoThread */ fun processReceivedRoomKeyRequests() { - val roomKeyRequestsToProcess = ArrayList(receivedRoomKeyRequests) + val roomKeyRequestsToProcess = receivedRoomKeyRequests.toList() receivedRoomKeyRequests.clear() for (request in roomKeyRequestsToProcess) { val userId = request.userId @@ -77,7 +75,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor( val roomId = body!!.roomId val alg = body.algorithm - Timber.v("m.room_key_request from " + userId + ":" + deviceId + " for " + roomId + " / " + body.sessionId + " id " + request.requestId) + Timber.v("m.room_key_request from $userId:$deviceId for $roomId / ${body.sessionId} id ${request.requestId}") if (userId == null || credentials.userId != userId) { // TODO: determine if we sent this device the keys already: in Timber.e("## processReceivedRoomKeyRequests() : Ignoring room key request from other user for now") @@ -92,12 +90,12 @@ internal class IncomingRoomKeyRequestManager @Inject constructor( continue } if (!decryptor.hasKeysForKeyRequest(request)) { - Timber.e("## processReceivedRoomKeyRequests() : room key request for unknown session " + body.sessionId!!) + Timber.e("## processReceivedRoomKeyRequests() : room key request for unknown session ${body.sessionId!!}") cryptoStore.deleteIncomingRoomKeyRequest(request) continue } - if (TextUtils.equals(deviceId, credentials.deviceId) && TextUtils.equals(credentials.userId, userId)) { + if (deviceId == credentials.deviceId && credentials.userId == userId) { Timber.v("## processReceivedRoomKeyRequests() : oneself device - ignored") cryptoStore.deleteIncomingRoomKeyRequest(request) continue @@ -132,7 +130,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor( var receivedRoomKeyRequestCancellations: List? = null synchronized(this.receivedRoomKeyRequestCancellations) { - if (!this.receivedRoomKeyRequestCancellations.isEmpty()) { + if (this.receivedRoomKeyRequestCancellations.isNotEmpty()) { receivedRoomKeyRequestCancellations = this.receivedRoomKeyRequestCancellations.toList() this.receivedRoomKeyRequestCancellations.clear() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXMegolmExportEncryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXMegolmExportEncryption.kt index b9cb59a0f5..0aa7a15ce2 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXMegolmExportEncryption.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXMegolmExportEncryption.kt @@ -16,20 +16,19 @@ package im.vector.matrix.android.internal.crypto -import android.text.TextUtils import android.util.Base64 import im.vector.matrix.android.internal.extensions.toUnsignedInt import timber.log.Timber import java.io.ByteArrayOutputStream import java.nio.charset.Charset import java.security.SecureRandom -import java.util.* import javax.crypto.Cipher import javax.crypto.Mac import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec import kotlin.experimental.and import kotlin.experimental.xor +import kotlin.math.min /** * Utility class to import/export the crypto data @@ -51,7 +50,7 @@ object MXMegolmExportEncryption { * @return the AES key */ private fun getAesKey(keyBits: ByteArray): ByteArray { - return Arrays.copyOfRange(keyBits, 0, 32) + return keyBits.copyOfRange(0, 32) } /** @@ -61,7 +60,7 @@ object MXMegolmExportEncryption { * @return the Hmac key. */ private fun getHmacKey(keyBits: ByteArray): ByteArray { - return Arrays.copyOfRange(keyBits, 32, keyBits.size) + return keyBits.copyOfRange(32, keyBits.size) } /** @@ -77,7 +76,7 @@ object MXMegolmExportEncryption { val body = unpackMegolmKeyFile(data) // check we have a version byte - if (null == body || body.size == 0) { + if (null == body || body.isEmpty()) { Timber.e("## decryptMegolmKeyFile() : Invalid file: too short") throw Exception("Invalid file: too short") } @@ -93,27 +92,27 @@ object MXMegolmExportEncryption { throw Exception("Invalid file: too short") } - if (TextUtils.isEmpty(password)) { + if (password.isEmpty()) { throw Exception("Empty password is not supported") } - val salt = Arrays.copyOfRange(body, 1, 1 + 16) - val iv = Arrays.copyOfRange(body, 17, 17 + 16) + val salt = body.copyOfRange(1, 1 + 16) + val iv = body.copyOfRange(17, 17 + 16) val iterations = (body[33].toUnsignedInt() shl 24) or (body[34].toUnsignedInt() shl 16) or (body[35].toUnsignedInt() shl 8) or body[36].toUnsignedInt() - val ciphertext = Arrays.copyOfRange(body, 37, 37 + ciphertextLength) - val hmac = Arrays.copyOfRange(body, body.size - 32, body.size) + val ciphertext = body.copyOfRange(37, 37 + ciphertextLength) + val hmac = body.copyOfRange(body.size - 32, body.size) val deriveKey = deriveKeys(salt, iterations, password) - val toVerify = Arrays.copyOfRange(body, 0, body.size - 32) + val toVerify = body.copyOfRange(0, body.size - 32) val macKey = SecretKeySpec(getHmacKey(deriveKey), "HmacSHA256") val mac = Mac.getInstance("HmacSHA256") mac.init(macKey) val digest = mac.doFinal(toVerify) - if (!Arrays.equals(hmac, digest)) { + if (!hmac.contentEquals(digest)) { Timber.e("## decryptMegolmKeyFile() : Authentication check failed: incorrect password?") throw Exception("Authentication check failed: incorrect password?") } @@ -146,7 +145,7 @@ object MXMegolmExportEncryption { @Throws(Exception::class) @JvmOverloads fun encryptMegolmKeyFile(data: String, password: String, kdf_rounds: Int = DEFAULT_ITERATION_COUNT): ByteArray { - if (TextUtils.isEmpty(password)) { + if (password.isEmpty()) { throw Exception("Empty password is not supported") } @@ -196,7 +195,7 @@ object MXMegolmExportEncryption { System.arraycopy(cipherArray, 0, resultBuffer, idx, cipherArray.size) idx += cipherArray.size - val toSign = Arrays.copyOfRange(resultBuffer, 0, idx) + val toSign = resultBuffer.copyOfRange(0, idx) val macKey = SecretKeySpec(getHmacKey(deriveKey), "HmacSHA256") val mac = Mac.getInstance("HmacSHA256") @@ -234,7 +233,7 @@ object MXMegolmExportEncryption { // start the next line after the newline lineStart = lineEnd + 1 - if (TextUtils.equals(line, HEADER_LINE)) { + if (line == HEADER_LINE) { break } } @@ -244,15 +243,13 @@ object MXMegolmExportEncryption { // look for the end line while (true) { val lineEnd = fileStr.indexOf('\n', lineStart) - val line: String - - if (lineEnd < 0) { - line = fileStr.substring(lineStart).trim() + val line = if (lineEnd < 0) { + fileStr.substring(lineStart) } else { - line = fileStr.substring(lineStart, lineEnd).trim() - } + fileStr.substring(lineStart, lineEnd) + }.trim() - if (TextUtils.equals(line, TRAILER_LINE)) { + if (line == TRAILER_LINE) { break } @@ -290,7 +287,7 @@ object MXMegolmExportEncryption { for (i in 1..nLines) { outStream.write("\n".toByteArray()) - val len = Math.min(LINE_LENGTH, data.size - o) + val len = min(LINE_LENGTH, data.size - o) outStream.write(Base64.encode(data, o, len, Base64.DEFAULT)) o += LINE_LENGTH } @@ -318,7 +315,7 @@ object MXMegolmExportEncryption { // it is simpler than the generic algorithm because the expected key length is equal to the mac key length. // noticed as dklen/hlen val prf = Mac.getInstance("HmacSHA512") - prf.init(SecretKeySpec(password.toByteArray(charset("UTF-8")), "HmacSHA512")) + prf.init(SecretKeySpec(password.toByteArray(Charsets.UTF_8), "HmacSHA512")) // 512 bits key length val key = ByteArray(64) @@ -326,8 +323,7 @@ object MXMegolmExportEncryption { // U1 = PRF(Password, Salt || INT_32_BE(i)) prf.update(salt) - val int32BE = ByteArray(4) - Arrays.fill(int32BE, 0.toByte()) + val int32BE = ByteArray(4) { 0.toByte() } int32BE[3] = 1.toByte() prf.update(int32BE) prf.doFinal(Uc, 0) @@ -346,7 +342,7 @@ object MXMegolmExportEncryption { } } - Timber.v("## deriveKeys() : " + iterations + " in " + (System.currentTimeMillis() - t0) + " ms") + Timber.v("## deriveKeys() : $iterations in ${System.currentTimeMillis() - t0} ms") return key } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt index 9655806b2d..68aaaf3831 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.internal.crypto -import android.text.TextUtils import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.util.JSON_DICT_PARAMETERIZED_TYPE import im.vector.matrix.android.api.util.JsonDict @@ -33,7 +32,6 @@ import im.vector.matrix.android.internal.util.convertToUTF8 import org.matrix.olm.* import timber.log.Timber import java.net.URLEncoder -import java.util.* import javax.inject.Inject // The libolm wrapper. @@ -434,7 +432,7 @@ internal class MXOlmDevice @Inject constructor( * @return the base64-encoded secret key. */ fun getSessionKey(sessionId: String): String? { - if (!TextUtils.isEmpty(sessionId)) { + if (sessionId.isNotEmpty()) { try { return outboundGroupSessionStore[sessionId]!!.sessionKey() } catch (e: Exception) { @@ -451,7 +449,7 @@ internal class MXOlmDevice @Inject constructor( * @return the current chain index. */ fun getMessageIndex(sessionId: String): Int { - return if (!TextUtils.isEmpty(sessionId)) { + return if (sessionId.isNotEmpty()) { outboundGroupSessionStore[sessionId]!!.messageIndex() } else 0 } @@ -464,7 +462,7 @@ internal class MXOlmDevice @Inject constructor( * @return ciphertext */ fun encryptGroupMessage(sessionId: String, payloadString: String): String? { - if (!TextUtils.isEmpty(sessionId) && !TextUtils.isEmpty(payloadString)) { + if (sessionId.isNotEmpty() && payloadString.isNotEmpty()) { try { return outboundGroupSessionStore[sessionId]!!.encryptMessage(payloadString) } catch (e: Exception) { @@ -523,7 +521,7 @@ internal class MXOlmDevice @Inject constructor( } try { - if (!TextUtils.equals(session.olmInboundGroupSession!!.sessionIdentifier(), sessionId)) { + if (session.olmInboundGroupSession!!.sessionIdentifier() != sessionId) { Timber.e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey") session.olmInboundGroupSession!!.releaseSession() return false @@ -573,7 +571,7 @@ internal class MXOlmDevice @Inject constructor( } try { - if (!TextUtils.equals(session.olmInboundGroupSession?.sessionIdentifier(), sessionId)) { + if (session.olmInboundGroupSession?.sessionIdentifier() != sessionId) { Timber.e("## importInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey") if (session.olmInboundGroupSession != null) session.olmInboundGroupSession!!.releaseSession() continue @@ -758,7 +756,7 @@ internal class MXOlmDevice @Inject constructor( if (session != null) { // Check that the room id matches the original one for the session. This stops // the HS pretending a message was targeting a different room. - if (!TextUtils.equals(roomId, session.roomId)) { + if (roomId != session.roomId) { val errorDescription = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId) Timber.e("## getInboundGroupSession() : $errorDescription") throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, errorDescription) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MyDeviceInfoHolder.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MyDeviceInfoHolder.kt index b64904190b..f93245de12 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MyDeviceInfoHolder.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MyDeviceInfoHolder.kt @@ -16,12 +16,10 @@ package im.vector.matrix.android.internal.crypto -import android.text.TextUtils import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.session.SessionScope -import java.util.* import javax.inject.Inject @SessionScope @@ -42,11 +40,11 @@ internal class MyDeviceInfoHolder @Inject constructor( init { val keys = HashMap() - if (!TextUtils.isEmpty(olmDevice.deviceEd25519Key)) { + if (!olmDevice.deviceEd25519Key.isNullOrEmpty()) { keys["ed25519:" + credentials.deviceId] = olmDevice.deviceEd25519Key!! } - if (!TextUtils.isEmpty(olmDevice.deviceCurve25519Key)) { + if (!olmDevice.deviceCurve25519Key.isNullOrEmpty()) { keys["curve25519:" + credentials.deviceId] = olmDevice.deviceCurve25519Key!! } @@ -58,13 +56,7 @@ internal class MyDeviceInfoHolder @Inject constructor( // Add our own deviceinfo to the store val endToEndDevicesForUser = cryptoStore.getUserDevices(credentials.userId) - val myDevices: MutableMap - - if (null != endToEndDevicesForUser) { - myDevices = HashMap(endToEndDevicesForUser) - } else { - myDevices = HashMap() - } + val myDevices = endToEndDevicesForUser.orEmpty().toMutableMap() myDevices[myDevice.deviceId] = myDevice diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OneTimeKeysUploader.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OneTimeKeysUploader.kt index c929c33666..e6b57d149f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OneTimeKeysUploader.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OneTimeKeysUploader.kt @@ -24,8 +24,9 @@ import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.util.JsonCanonicalizer import org.matrix.olm.OlmAccount import timber.log.Timber -import java.util.* import javax.inject.Inject +import kotlin.math.floor +import kotlin.math.min @SessionScope internal class OneTimeKeysUploader @Inject constructor( @@ -77,7 +78,7 @@ internal class OneTimeKeysUploader @Inject constructor( // If we run out of slots when generating new keys then olm will // discard the oldest private keys first. This will eventually clean // out stale private keys that won't receive a message. - val keyLimit = Math.floor(maxOneTimeKeys / 2.0).toInt() + val keyLimit = floor(maxOneTimeKeys / 2.0).toInt() if (oneTimeKeyCount != null) { uploadOTK(oneTimeKeyCount!!, keyLimit) } else { @@ -116,7 +117,7 @@ internal class OneTimeKeysUploader @Inject constructor( // If we don't need to generate any more keys then we are done. return } - val keysThisLoop = Math.min(keyLimit - keyCount, ONE_TIME_KEY_GENERATION_MAX_NUMBER) + val keysThisLoop = min(keyLimit - keyCount, ONE_TIME_KEY_GENERATION_MAX_NUMBER) olmDevice.generateOneTimeKeys(keysThisLoop) val response = uploadOneTimeKeys() if (response.hasOneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)) { @@ -132,14 +133,14 @@ internal class OneTimeKeysUploader @Inject constructor( */ private suspend fun uploadOneTimeKeys(): KeysUploadResponse { val oneTimeKeys = olmDevice.getOneTimeKeys() - val oneTimeJson = HashMap() + val oneTimeJson = mutableMapOf() val curve25519Map = oneTimeKeys?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY) if (null != curve25519Map) { - for (key_id in curve25519Map.keys) { - val k = HashMap() - k["key"] = curve25519Map.getValue(key_id) + for ((key_id, value) in curve25519Map) { + val k = mutableMapOf() + k["key"] = value // the key is also signed val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, k) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/RoomDecryptorProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/RoomDecryptorProvider.kt index 9c542cd446..a17a524923 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/RoomDecryptorProvider.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/RoomDecryptorProvider.kt @@ -16,13 +16,11 @@ package im.vector.matrix.android.internal.crypto -import android.text.TextUtils import im.vector.matrix.android.internal.crypto.algorithms.IMXDecrypting import im.vector.matrix.android.internal.crypto.algorithms.megolm.MXMegolmDecryptionFactory import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmDecryptionFactory import im.vector.matrix.android.internal.session.SessionScope import timber.log.Timber -import java.util.* import javax.inject.Inject @SessionScope @@ -62,10 +60,8 @@ internal class RoomDecryptorProvider @Inject constructor( } if (roomId != null && roomId.isNotEmpty()) { synchronized(roomDecryptors) { - if (!roomDecryptors.containsKey(roomId)) { - roomDecryptors[roomId] = HashMap() - } - val alg = roomDecryptors[roomId]?.get(algorithm) + val decryptors = roomDecryptors.getOrPut(roomId) { mutableMapOf() } + val alg = decryptors[algorithm] if (alg != null) { return alg } @@ -89,7 +85,7 @@ internal class RoomDecryptorProvider @Inject constructor( } else -> olmDecryptionFactory.create() } - if (roomId != null && !TextUtils.isEmpty(roomId)) { + if (!roomId.isNullOrEmpty()) { synchronized(roomDecryptors) { roomDecryptors[roomId]?.put(algorithm, alg) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt index 6206072156..0283d3c85b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.crypto.actions -import android.text.TextUtils import im.vector.matrix.android.internal.crypto.MXOlmDevice import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXKey @@ -24,7 +23,6 @@ import im.vector.matrix.android.internal.crypto.model.MXOlmSessionResult import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask import timber.log.Timber -import java.util.* import javax.inject.Inject internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val olmDevice: MXOlmDevice, @@ -35,18 +33,14 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val val results = MXUsersDevicesMap() - val userIds = devicesByUser.keys - - for (userId in userIds) { - val deviceInfos = devicesByUser[userId] - - for (deviceInfo in deviceInfos!!) { + for ((userId, deviceInfos) in devicesByUser) { + for (deviceInfo in deviceInfos) { val deviceId = deviceInfo.deviceId val key = deviceInfo.identityKey() val sessionId = olmDevice.getSessionId(key!!) - if (TextUtils.isEmpty(sessionId)) { + if (sessionId.isNullOrEmpty()) { devicesWithoutSession.add(deviceInfo) } @@ -79,9 +73,8 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim) val oneTimeKeys = oneTimeKeysForUsersDeviceTask.execute(claimParams) Timber.v("## claimOneTimeKeysForUsersDevices() : keysClaimResponse.oneTimeKeys: $oneTimeKeys") - for (userId in userIds) { - val deviceInfos = devicesByUser[userId] - for (deviceInfo in deviceInfos!!) { + for ((userId, deviceInfos) in devicesByUser) { + for (deviceInfo in deviceInfos) { var oneTimeKey: MXKey? = null val deviceIds = oneTimeKeys.getUserDeviceIds(userId) if (null != deviceIds) { @@ -116,24 +109,22 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val val signKeyId = "ed25519:$deviceId" val signature = oneTimeKey.signatureForUserId(userId, signKeyId) - if (!TextUtils.isEmpty(signature) && !TextUtils.isEmpty(deviceInfo.fingerprint())) { + if (!signature.isNullOrEmpty() && !deviceInfo.fingerprint().isNullOrEmpty()) { var isVerified = false var errorMessage: String? = null - if (signature != null) { - try { - olmDevice.verifySignature(deviceInfo.fingerprint()!!, oneTimeKey.signalableJSONDictionary(), signature) - isVerified = true - } catch (e: Exception) { - errorMessage = e.message - } + try { + olmDevice.verifySignature(deviceInfo.fingerprint()!!, oneTimeKey.signalableJSONDictionary(), signature) + isVerified = true + } catch (e: Exception) { + errorMessage = e.message } // Check one-time key signature if (isVerified) { sessionId = olmDevice.createOutboundSession(deviceInfo.identityKey()!!, oneTimeKey.value) - if (!TextUtils.isEmpty(sessionId)) { + if (!sessionId.isNullOrEmpty()) { Timber.v("## verifyKeyAndStartSession() : Started new sessionid " + sessionId + " for device " + deviceInfo + "(theirOneTimeKey: " + oneTimeKey.value + ")") } else { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt index 4ed1ed75f6..3fd833dfb4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt @@ -16,14 +16,11 @@ package im.vector.matrix.android.internal.crypto.actions -import android.text.TextUtils import im.vector.matrix.android.internal.crypto.MXOlmDevice -import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXOlmSessionResult import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import timber.log.Timber -import java.util.* import javax.inject.Inject internal class EnsureOlmSessionsForUsersAction @Inject constructor(private val olmDevice: MXOlmDevice, @@ -36,27 +33,14 @@ internal class EnsureOlmSessionsForUsersAction @Inject constructor(private val o */ suspend fun handle(users: List): MXUsersDevicesMap { Timber.v("## ensureOlmSessionsForUsers() : ensureOlmSessionsForUsers $users") - val devicesByUser = HashMap>() - - for (userId in users) { - devicesByUser[userId] = ArrayList() - + val devicesByUser = users.associateWith { userId -> val devices = cryptoStore.getUserDevices(userId)?.values ?: emptyList() - for (device in devices) { - val key = device.identityKey() - - if (TextUtils.equals(key, olmDevice.deviceCurve25519Key)) { - // Don't bother setting up session to ourself - continue - } - - if (device.isVerified) { - // Don't bother setting up sessions with blocked users - continue - } - - devicesByUser[userId]!!.add(device) + devices.filter { + // Don't bother setting up session to ourself + it.identityKey() != olmDevice.deviceCurve25519Key && + // Don't bother setting up sessions with blocked users + !it.isVerified } } return ensureOlmSessionsForDevicesAction.handle(devicesByUser) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/MessageEncrypter.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/MessageEncrypter.kt index b360cd0234..ebe219600d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/MessageEncrypter.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/MessageEncrypter.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.crypto.actions -import android.text.TextUtils import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_OLM import im.vector.matrix.android.internal.crypto.MXOlmDevice @@ -25,7 +24,6 @@ import im.vector.matrix.android.internal.crypto.model.rest.EncryptedMessage import im.vector.matrix.android.internal.util.JsonCanonicalizer import im.vector.matrix.android.internal.util.convertToUTF8 import timber.log.Timber -import java.util.* import javax.inject.Inject internal class MessageEncrypter @Inject constructor(private val credentials: Credentials, @@ -40,18 +38,12 @@ internal class MessageEncrypter @Inject constructor(private val credentials: Cre * @return the content for an m.room.encrypted event. */ fun encryptMessage(payloadFields: Map, deviceInfos: List): EncryptedMessage { - val deviceInfoParticipantKey = HashMap() - val participantKeys = ArrayList() + val deviceInfoParticipantKey = deviceInfos.associateBy { it.identityKey()!! } - for (di in deviceInfos) { - participantKeys.add(di.identityKey()!!) - deviceInfoParticipantKey[di.identityKey()!!] = di - } - - val payloadJson = HashMap(payloadFields) + val payloadJson = payloadFields.toMutableMap() payloadJson["sender"] = credentials.userId - payloadJson["sender_device"] = credentials.deviceId + payloadJson["sender_device"] = credentials.deviceId!! // Include the Ed25519 key so that the recipient knows what // device this message came from. @@ -67,30 +59,24 @@ internal class MessageEncrypter @Inject constructor(private val credentials: Cre val ciphertext = HashMap() - for (deviceKey in participantKeys) { + for ((deviceKey, deviceInfo) in deviceInfoParticipantKey) { val sessionId = olmDevice.getSessionId(deviceKey) - if (!TextUtils.isEmpty(sessionId)) { + if (!sessionId.isNullOrEmpty()) { Timber.v("Using sessionid $sessionId for device $deviceKey") - val deviceInfo = deviceInfoParticipantKey[deviceKey] - payloadJson["recipient"] = deviceInfo!!.userId - - val recipientsKeysMap = HashMap() - recipientsKeysMap["ed25519"] = deviceInfo.fingerprint()!! - payloadJson["recipient_keys"] = recipientsKeysMap + payloadJson["recipient"] = deviceInfo.userId + payloadJson["recipient_keys"] = mapOf("ed25519" to deviceInfo.fingerprint()!!) val payloadString = convertToUTF8(JsonCanonicalizer.getCanonicalJson(Map::class.java, payloadJson)) - ciphertext[deviceKey] = olmDevice.encryptMessage(deviceKey, sessionId!!, payloadString)!! + ciphertext[deviceKey] = olmDevice.encryptMessage(deviceKey, sessionId, payloadString)!! } } - val res = EncryptedMessage() - - res.algorithm = MXCRYPTO_ALGORITHM_OLM - res.senderKey = olmDevice.deviceCurve25519Key - res.cipherText = ciphertext - - return res + return EncryptedMessage( + algorithm = MXCRYPTO_ALGORITHM_OLM, + senderKey = olmDevice.deviceCurve25519Key, + cipherText = ciphertext + ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt index 9c14ed555a..6ffaf776b2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.internal.crypto.algorithms.megolm -import android.text.TextUtils import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType @@ -148,7 +147,7 @@ internal class MXMegolmDecryption(private val userId: String, selfMap["deviceId"] = "*" recipients.add(selfMap) - if (!TextUtils.equals(sender, userId)) { + if (sender != userId) { val senderMap = HashMap() senderMap["userId"] = sender senderMap["deviceId"] = encryptedEventContent.deviceId!! @@ -176,17 +175,12 @@ internal class MXMegolmDecryption(private val userId: String, val encryptedEventContent = event.content.toModel() ?: return val pendingEventsKey = "${encryptedEventContent.senderKey}|${encryptedEventContent.sessionId}" - if (!pendingEvents.containsKey(pendingEventsKey)) { - pendingEvents[pendingEventsKey] = HashMap() - } + val timeline = pendingEvents.getOrPut(pendingEventsKey) { HashMap() } + val events = timeline.getOrPut(timelineId) { ArrayList() } - if (pendingEvents[pendingEventsKey]?.containsKey(timelineId) == false) { - pendingEvents[pendingEventsKey]?.put(timelineId, ArrayList()) - } - - if (pendingEvents[pendingEventsKey]?.get(timelineId)?.contains(event) == false) { - Timber.v("## addEventToPendingList() : add Event " + event.eventId + " in room id " + event.roomId) - pendingEvents[pendingEventsKey]?.get(timelineId)?.add(event) + if (event !in events) { + Timber.v("## addEventToPendingList() : add Event ${event.eventId} in room id ${event.roomId}") + events.add(event) } } @@ -203,7 +197,7 @@ internal class MXMegolmDecryption(private val userId: String, var keysClaimed: MutableMap = HashMap() val forwardingCurve25519KeyChain: MutableList = ArrayList() - if (TextUtils.isEmpty(roomKeyContent.roomId) || TextUtils.isEmpty(roomKeyContent.sessionId) || TextUtils.isEmpty(roomKeyContent.sessionKey)) { + if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.sessionId.isNullOrEmpty() || roomKeyContent.sessionKey.isNullOrEmpty()) { Timber.e("## onRoomKeyEvent() : Key event is missing fields") return } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt index 2f5e657376..45dbeb43c3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt @@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.crypto.algorithms.megolm -import android.text.TextUtils import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.session.events.model.Content @@ -38,7 +37,6 @@ import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask import im.vector.matrix.android.internal.util.JsonCanonicalizer import im.vector.matrix.android.internal.util.convertToUTF8 import timber.log.Timber -import java.util.* internal class MXMegolmEncryption( // The id of the room we will be sending to. @@ -85,7 +83,7 @@ internal class MXMegolmEncryption( keysClaimedMap["ed25519"] = olmDevice.deviceEd25519Key!! olmDevice.addInboundGroupSession(sessionId!!, olmDevice.getSessionKey(sessionId)!!, roomId, olmDevice.deviceCurve25519Key!!, - ArrayList(), keysClaimedMap, false) + emptyList(), keysClaimedMap, false) keysBackup.maybeBackupKeys() @@ -115,10 +113,8 @@ internal class MXMegolmEncryption( for (deviceId in deviceIds!!) { val deviceInfo = devicesInRoom.getObject(userId, deviceId) if (deviceInfo != null && null == safeSession.sharedWithDevices.getObject(userId, deviceId)) { - if (!shareMap.containsKey(userId)) { - shareMap[userId] = ArrayList() - } - shareMap[userId]!!.add(deviceInfo) + val devices = shareMap.getOrPut(userId) { ArrayList() } + devices.add(deviceInfo) } } } @@ -141,21 +137,17 @@ internal class MXMegolmEncryption( } // reduce the map size to avoid request timeout when there are too many devices (Users size * devices per user) val subMap = HashMap>() - val userIds = ArrayList() var devicesCount = 0 - for (userId in devicesByUsers.keys) { - devicesByUsers[userId]?.let { - userIds.add(userId) - subMap[userId] = it - devicesCount += it.size - } + for ((userId, devices) in devicesByUsers) { + subMap[userId] = devices + devicesCount += devices.size if (devicesCount > 100) { break } } - Timber.v("## shareKey() ; userId $userIds") + Timber.v("## shareKey() ; userId ${subMap.keys}") shareUserDevicesKey(session, subMap) - val remainingDevices = devicesByUsers.filterKeys { userIds.contains(it).not() } + val remainingDevices = devicesByUsers - subMap.keys shareKey(session, remainingDevices) } @@ -210,8 +202,7 @@ internal class MXMegolmEncryption( continue } Timber.v("## shareUserDevicesKey() : Sharing keys with device $userId:$deviceID") - //noinspection ArraysAsListWithZeroOrOneArgument,ArraysAsListWithZeroOrOneArgument - contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, Arrays.asList(sessionResult.deviceInfo))) + contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, listOf(sessionResult.deviceInfo))) haveTargets = true } } @@ -228,9 +219,8 @@ internal class MXMegolmEncryption( // attempted to share with) rather than the contentMap (those we did // share with), because we don't want to try to claim a one-time-key // for dead devices on every message. - for (userId in devicesByUser.keys) { - val devicesToShareWith = devicesByUser[userId] - for ((deviceId) in devicesToShareWith!!) { + for ((userId, devicesToShareWith) in devicesByUser) { + for ((deviceId) in devicesToShareWith) { session.sharedWithDevices.setObject(userId, deviceId, chainIndex) } } @@ -304,7 +294,7 @@ internal class MXMegolmEncryption( continue } - if (TextUtils.equals(deviceInfo.identityKey(), olmDevice.deviceCurve25519Key)) { + if (deviceInfo.identityKey() == olmDevice.deviceCurve25519Key) { // Don't bother sending to ourself continue } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmEncryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmEncryption.kt index df3695db94..b6d1b98546 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmEncryption.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmEncryption.kt @@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.crypto.algorithms.olm -import android.text.TextUtils import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.toContent import im.vector.matrix.android.internal.crypto.DeviceListManager @@ -28,7 +27,6 @@ import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore -import java.util.* internal class MXOlmEncryption( private var roomId: String, @@ -49,7 +47,7 @@ internal class MXOlmEncryption( val devices = cryptoStore.getUserDevices(userId)?.values ?: emptyList() for (device in devices) { val key = device.identityKey() - if (TextUtils.equals(key, olmDevice.deviceCurve25519Key)) { + if (key == olmDevice.deviceCurve25519Key) { // Don't bother setting up session to ourself continue } @@ -61,10 +59,11 @@ internal class MXOlmEncryption( } } - val messageMap = HashMap() - messageMap["room_id"] = roomId - messageMap["type"] = eventType - messageMap["content"] = eventContent + val messageMap = mapOf( + "room_id" to roomId, + "type" to eventType, + "content" to eventContent + ) messageEncrypter.encryptMessage(messageMap, deviceInfos) return messageMap.toContent()!! diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt index a66d50dfad..b90ba58b88 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt @@ -58,6 +58,7 @@ import im.vector.matrix.android.internal.task.TaskThread import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.JsonCanonicalizer import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers +import im.vector.matrix.android.internal.util.awaitCallback import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -68,6 +69,9 @@ import org.matrix.olm.OlmPkMessage import timber.log.Timber import java.security.InvalidParameterException import javax.inject.Inject +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine import kotlin.random.Random /** @@ -142,8 +146,8 @@ internal class KeysBackup @Inject constructor( progressListener: ProgressListener?, callback: MatrixCallback) { GlobalScope.launch(coroutineDispatchers.main) { - withContext(coroutineDispatchers.crypto) { - Try { + runCatching { + withContext(coroutineDispatchers.crypto) { val olmPkDecryption = OlmPkDecryption() val megolmBackupAuthData = MegolmBackupAuthData() @@ -394,7 +398,7 @@ internal class KeysBackup @Inject constructor( return keysBackupVersionTrust } - for (keyId in mySigs.keys) { + for ((keyId, mySignature) in mySigs) { // XXX: is this how we're supposed to get the device id? var deviceId: String? = null val components = keyId.split(":") @@ -412,7 +416,7 @@ internal class KeysBackup @Inject constructor( val fingerprint = device.fingerprint() if (fingerprint != null) { try { - olmDevice.verifySignature(fingerprint, authData.signalableJSONDictionary(), mySigs[keyId] as String) + olmDevice.verifySignature(fingerprint, authData.signalableJSONDictionary(), mySignature) isSignatureValid = true } catch (e: OlmException) { Timber.v(e, "getKeysBackupTrust: Bad signature from device ${device.deviceId}") @@ -617,8 +621,8 @@ internal class KeysBackup @Inject constructor( Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}") GlobalScope.launch(coroutineDispatchers.main) { - withContext(coroutineDispatchers.crypto) { - Try { + runCatching { + val decryption = withContext(coroutineDispatchers.crypto) { // Check if the recovery is valid before going any further if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysVersionResult)) { Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version") @@ -635,76 +639,59 @@ internal class KeysBackup @Inject constructor( decryption } - }.fold( - { - callback.onFailure(it) - }, - { decryption -> - stepProgressListener?.onStepProgress(StepProgressListener.Step.DownloadingKey) - // Get backed up keys from the homeserver - getKeys(sessionId, roomId, keysVersionResult.version!!, object : MatrixCallback { - override fun onSuccess(data: KeysBackupData) { - GlobalScope.launch(coroutineDispatchers.main) { - val importRoomKeysResult = withContext(coroutineDispatchers.crypto) { - val sessionsData = ArrayList() - // Restore that data - var sessionsFromHsCount = 0 - for (roomIdLoop in data.roomIdToRoomKeysBackupData.keys) { - for (sessionIdLoop in data.roomIdToRoomKeysBackupData[roomIdLoop]!!.sessionIdToKeyBackupData.keys) { - sessionsFromHsCount++ + stepProgressListener?.onStepProgress(StepProgressListener.Step.DownloadingKey) - val keyBackupData = data.roomIdToRoomKeysBackupData[roomIdLoop]!!.sessionIdToKeyBackupData[sessionIdLoop]!! + // Get backed up keys from the homeserver + val data = getKeys(sessionId, roomId, keysVersionResult.version!!) - val sessionData = decryptKeyBackupData(keyBackupData, sessionIdLoop, roomIdLoop, decryption) + withContext(coroutineDispatchers.crypto) { + val sessionsData = ArrayList() + // Restore that data + var sessionsFromHsCount = 0 + for ((roomIdLoop, backupData) in data.roomIdToRoomKeysBackupData) { + for ((sessionIdLoop, keyBackupData) in backupData.sessionIdToKeyBackupData) { + sessionsFromHsCount++ - sessionData?.let { - sessionsData.add(it) - } - } - } - Timber.v("restoreKeysWithRecoveryKey: Decrypted ${sessionsData.size} keys out" + - " of $sessionsFromHsCount from the backup store on the homeserver") + val sessionData = decryptKeyBackupData(keyBackupData, sessionIdLoop, roomIdLoop, decryption) - // Do not trigger a backup for them if they come from the backup version we are using - val backUp = keysVersionResult.version != keysBackupVersion?.version - if (backUp) { - Timber.v("restoreKeysWithRecoveryKey: Those keys will be backed up" + - " to backup version: ${keysBackupVersion?.version}") - } - - // Import them into the crypto store - val progressListener = if (stepProgressListener != null) { - object : ProgressListener { - override fun onProgress(progress: Int, total: Int) { - // Note: no need to post to UI thread, importMegolmSessionsData() will do it - stepProgressListener.onStepProgress(StepProgressListener.Step.ImportingKey(progress, total)) - } - } - } else { - null - } - - val result = megolmSessionDataImporter.handle(sessionsData, !backUp, uiHandler, progressListener) - - // Do not back up the key if it comes from a backup recovery - if (backUp) { - maybeBackupKeys() - } - - result - } - - callback.onSuccess(importRoomKeysResult) - } + sessionData?.let { + sessionsData.add(it) } - - override fun onFailure(failure: Throwable) { - callback.onFailure(failure) - } - }) + } } - ) + Timber.v("restoreKeysWithRecoveryKey: Decrypted ${sessionsData.size} keys out" + + " of $sessionsFromHsCount from the backup store on the homeserver") + + // Do not trigger a backup for them if they come from the backup version we are using + val backUp = keysVersionResult.version != keysBackupVersion?.version + if (backUp) { + Timber.v("restoreKeysWithRecoveryKey: Those keys will be backed up" + + " to backup version: ${keysBackupVersion?.version}") + } + + // Import them into the crypto store + val progressListener = if (stepProgressListener != null) { + object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + // Note: no need to post to UI thread, importMegolmSessionsData() will do it + stepProgressListener.onStepProgress(StepProgressListener.Step.ImportingKey(progress, total)) + } + } + } else { + null + } + + val result = megolmSessionDataImporter.handle(sessionsData, !backUp, uiHandler, progressListener) + + // Do not back up the key if it comes from a backup recovery + if (backUp) { + maybeBackupKeys() + } + + result + } + }.foldToCallback(callback) } } @@ -717,7 +704,7 @@ internal class KeysBackup @Inject constructor( Timber.v("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}") GlobalScope.launch(coroutineDispatchers.main) { - withContext(coroutineDispatchers.crypto) { + runCatching { val progressListener = if (stepProgressListener != null) { object : ProgressListener { override fun onProgress(progress: Int, total: Int) { @@ -730,22 +717,18 @@ internal class KeysBackup @Inject constructor( null } - Try { + val recoveryKey = withContext(coroutineDispatchers.crypto) { recoveryKeyFromPassword(password, keysBackupVersion, progressListener) } - }.fold( - { - callback.onFailure(it) - }, - { recoveryKey -> - if (recoveryKey == null) { - Timber.v("backupKeys: Invalid configuration") - callback.onFailure(IllegalStateException("Invalid configuration")) - } else { - restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, roomId, sessionId, stepProgressListener, callback) - } + if (recoveryKey == null) { + Timber.v("backupKeys: Invalid configuration") + throw IllegalStateException("Invalid configuration") + } else { + awaitCallback { + restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, roomId, sessionId, stepProgressListener, it) } - ) + } + }.foldToCallback(callback) } } @@ -753,60 +736,26 @@ internal class KeysBackup @Inject constructor( * Same method as [RoomKeysRestClient.getRoomKey] except that it accepts nullable * parameters and always returns a KeysBackupData object through the Callback */ - private fun getKeys(sessionId: String?, + private suspend fun getKeys(sessionId: String?, roomId: String?, - version: String, - callback: MatrixCallback) { - if (roomId != null && sessionId != null) { + version: String): KeysBackupData { + return if (roomId != null && sessionId != null) { // Get key for the room and for the session - getRoomSessionDataTask - .configureWith(GetRoomSessionDataTask.Params(roomId, sessionId, version)) { - this.callback = object : MatrixCallback { - override fun onSuccess(data: KeyBackupData) { - // Convert to KeysBackupData - val keysBackupData = KeysBackupData() - keysBackupData.roomIdToRoomKeysBackupData = HashMap() - val roomKeysBackupData = RoomKeysBackupData() - roomKeysBackupData.sessionIdToKeyBackupData = HashMap() - roomKeysBackupData.sessionIdToKeyBackupData[sessionId] = data - keysBackupData.roomIdToRoomKeysBackupData[roomId] = roomKeysBackupData - - callback.onSuccess(keysBackupData) - } - - override fun onFailure(failure: Throwable) { - callback.onFailure(failure) - } - } - } - .executeBy(taskExecutor) + val data = getRoomSessionDataTask.execute(GetRoomSessionDataTask.Params(roomId, sessionId, version)) + // Convert to KeysBackupData + KeysBackupData(mutableMapOf( + roomId to RoomKeysBackupData(mutableMapOf( + sessionId to data + )) + )) } else if (roomId != null) { // Get all keys for the room - getRoomSessionsDataTask - .configureWith(GetRoomSessionsDataTask.Params(roomId, version)) { - this.callback = object : MatrixCallback { - override fun onSuccess(data: RoomKeysBackupData) { - // Convert to KeysBackupData - val keysBackupData = KeysBackupData() - keysBackupData.roomIdToRoomKeysBackupData = HashMap() - keysBackupData.roomIdToRoomKeysBackupData[roomId] = data - - callback.onSuccess(keysBackupData) - } - - override fun onFailure(failure: Throwable) { - callback.onFailure(failure) - } - } - } - .executeBy(taskExecutor) + val data = getRoomSessionsDataTask.execute(GetRoomSessionsDataTask.Params(roomId, version)) + // Convert to KeysBackupData + KeysBackupData(mutableMapOf(roomId to data)) } else { // Get all keys - getSessionsDataTask - .configureWith(GetSessionsDataTask.Params(version)) { - this.callback = callback - } - .executeBy(taskExecutor) + getSessionsDataTask.execute(GetSessionsDataTask.Params(version)) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/OlmInboundGroupSessionWrapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/OlmInboundGroupSessionWrapper.kt index 69fc314796..361b8bc205 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/OlmInboundGroupSessionWrapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/OlmInboundGroupSessionWrapper.kt @@ -17,13 +17,11 @@ package im.vector.matrix.android.internal.crypto.model -import android.text.TextUtils import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import im.vector.matrix.android.internal.crypto.MegolmSessionData import org.matrix.olm.OlmInboundGroupSession import timber.log.Timber import java.io.Serializable -import java.util.* /** * This class adds more context to a OlmInboundGroupSession object. @@ -91,7 +89,7 @@ class OlmInboundGroupSessionWrapper : Serializable { try { olmInboundGroupSession = OlmInboundGroupSession.importSession(megolmSessionData.sessionKey!!) - if (!TextUtils.equals(olmInboundGroupSession!!.sessionIdentifier(), megolmSessionData.sessionId)) { + if (olmInboundGroupSession!!.sessionIdentifier() != megolmSessionData.sessionId) { throw Exception("Mismatched group session Id") } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt index 01826cee71..d88a84de9e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.crypto.store.db -import android.text.TextUtils import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest import im.vector.matrix.android.internal.crypto.NewSessionListener @@ -101,8 +100,8 @@ internal class RealmCryptoStore(private val realmConfiguration: RealmConfigurati // Check credentials // The device id may not have been provided in credentials. // Check it only if provided, else trust the stored one. - if (!TextUtils.equals(currentMetadata.userId, credentials.userId) - || (credentials.deviceId != null && !TextUtils.equals(credentials.deviceId, currentMetadata.deviceId))) { + if (currentMetadata.userId != credentials.userId + || (credentials.deviceId != null && credentials.deviceId != currentMetadata.deviceId)) { Timber.w("## open() : Credentials do not match, close this store and delete data") deleteAll = true currentMetadata = null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/ClaimOneTimeKeysForUsersDeviceTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/ClaimOneTimeKeysForUsersDeviceTask.kt index 23b0f958bf..80f326e083 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/ClaimOneTimeKeysForUsersDeviceTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/ClaimOneTimeKeysForUsersDeviceTask.kt @@ -44,18 +44,14 @@ internal class DefaultClaimOneTimeKeysForUsersDevice @Inject constructor(private } val map = MXUsersDevicesMap() keysClaimResponse.oneTimeKeys?.let { oneTimeKeys -> - for (userId in oneTimeKeys.keys) { - val mapByUserId = oneTimeKeys[userId] + for ((userId, mapByUserId) in oneTimeKeys) { + for ((deviceId, deviceKey) in mapByUserId) { + val mxKey = MXKey.from(deviceKey) - if (mapByUserId != null) { - for (deviceId in mapByUserId.keys) { - val mxKey = MXKey.from(mapByUserId[deviceId]) - - if (mxKey != null) { - map.setObject(userId, deviceId, mxKey) - } else { - Timber.e("## claimOneTimeKeysForUsersDevices : fail to create a MXKey") - } + if (mxKey != null) { + map.setObject(userId, deviceId, mxKey) + } else { + Timber.e("## claimOneTimeKeysForUsersDevices : fail to create a MXKey") } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DownloadKeysForUsersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DownloadKeysForUsersTask.kt index 92908071a1..230af3874e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DownloadKeysForUsersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DownloadKeysForUsersTask.kt @@ -16,13 +16,11 @@ package im.vector.matrix.android.internal.crypto.tasks -import android.text.TextUtils import im.vector.matrix.android.internal.crypto.api.CryptoApi import im.vector.matrix.android.internal.crypto.model.rest.KeysQueryBody import im.vector.matrix.android.internal.crypto.model.rest.KeysQueryResponse import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task -import java.util.* import javax.inject.Inject internal interface DownloadKeysForUsersTask : Task { @@ -37,19 +35,13 @@ internal class DefaultDownloadKeysForUsers @Inject constructor(private val crypt : DownloadKeysForUsersTask { override suspend fun execute(params: DownloadKeysForUsersTask.Params): KeysQueryResponse { - val downloadQuery = HashMap>() - - if (null != params.userIds) { - for (userId in params.userIds) { - downloadQuery[userId] = HashMap() - } - } + val downloadQuery = params.userIds?.associateWith { emptyMap() }.orEmpty() val body = KeysQueryBody( deviceKeys = downloadQuery ) - if (!TextUtils.isEmpty(params.token)) { + if (!params.token.isNullOrEmpty()) { body.token = params.token } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SetDeviceNameTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SetDeviceNameTask.kt index 1abef5763e..47f3050b88 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SetDeviceNameTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SetDeviceNameTask.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.crypto.tasks -import android.text.TextUtils import im.vector.matrix.android.internal.crypto.api.CryptoApi import im.vector.matrix.android.internal.crypto.model.rest.UpdateDeviceInfoBody import im.vector.matrix.android.internal.network.executeRequest @@ -37,7 +36,7 @@ internal class DefaultSetDeviceNameTask @Inject constructor(private val cryptoAp override suspend fun execute(params: SetDeviceNameTask.Params) { val body = UpdateDeviceInfoBody( - displayName = if (TextUtils.isEmpty(params.deviceName)) "" else params.deviceName + displayName = params.deviceName ) return executeRequest { apiCall = cryptoApi.updateDeviceInfo(params.deviceId, body) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/UserAgentHolder.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/UserAgentHolder.kt index 097ab50abc..89da02b275 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/UserAgentHolder.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/UserAgentHolder.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.internal.network import android.content.Context -import android.text.TextUtils import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.internal.di.MatrixScope import timber.log.Timber @@ -60,10 +59,10 @@ internal class UserAgentHolder @Inject constructor(private val context: Context) Timber.e(e, "## initUserAgent() : failed") } - var systemUserAgent = System.getProperty("http.agent") + val systemUserAgent = System.getProperty("http.agent") // cannot retrieve the application version - if (TextUtils.isEmpty(appName) || TextUtils.isEmpty(appVersion)) { + if (appName.isEmpty() || appVersion.isEmpty()) { if (null == systemUserAgent) { userAgent = "Java" + System.getProperty("java.version") } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUploadStateTracker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUploadStateTracker.kt index 5fcad0e30f..68f48d20db 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUploadStateTracker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUploadStateTracker.kt @@ -75,9 +75,7 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU private fun updateState(key: String, state: ContentUploadStateTracker.State) { states[key] = state mainHandler.post { - listeners[key]?.also { listeners -> - listeners.forEach { it.onUpdate(state) } - } + listeners[key]?.forEach { it.onUpdate(state) } } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGetGroupDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGetGroupDataTask.kt index 3479d6dcdc..40dc0a44c0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGetGroupDataTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGetGroupDataTask.kt @@ -65,13 +65,11 @@ internal class DefaultGetGroupDataTask @Inject constructor( groupSummaryEntity.displayName = if (name.isNullOrEmpty()) groupId else name groupSummaryEntity.shortDescription = groupSummary.profile?.shortDescription ?: "" - val roomIds = groupRooms.rooms.map { it.roomId } groupSummaryEntity.roomIds.clear() - groupSummaryEntity.roomIds.addAll(roomIds) + groupRooms.rooms.mapTo(groupSummaryEntity.roomIds) { it.roomId } - val userIds = groupUsers.users.map { it.userId } groupSummaryEntity.userIds.clear() - groupSummaryEntity.userIds.addAll(userIds) + groupUsers.users.mapTo(groupSummaryEntity.userIds) { it.userId } groupSummaryEntity.membership = when (groupSummary.user?.membership) { Membership.JOIN.value -> Membership.JOIN diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/ProcessEventForPushTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/ProcessEventForPushTask.kt index a48990d48f..6e47bdfeaa 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/ProcessEventForPushTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/ProcessEventForPushTask.kt @@ -50,19 +50,15 @@ internal class DefaultProcessEventForPushTask @Inject constructor( defaultPushRuleService.dispatchRoomJoined(it) } val newJoinEvents = params.syncResponse.join - .map { entries -> - entries.value.timeline?.events?.map { it.copy(roomId = entries.key) } + .mapNotNull { (key, value) -> + value.timeline?.events?.map { it.copy(roomId = key) } } - .fold(emptyList(), { acc, next -> - acc + (next ?: emptyList()) - }) + .flatten() val inviteEvents = params.syncResponse.invite - .map { entries -> - entries.value.inviteState?.events?.map { it.copy(roomId = entries.key) } + .mapNotNull { (key, value) -> + value.inviteState?.events?.map { it.copy(roomId = key) } } - .fold(emptyList(), { acc, next -> - acc + (next ?: emptyList()) - }) + .flatten() val allEvents = (newJoinEvents + inviteEvents).filter { event -> when (event.type) { EventType.MESSAGE, @@ -84,16 +80,12 @@ internal class DefaultProcessEventForPushTask @Inject constructor( } val allRedactedEvents = params.syncResponse.join - .map { entries -> - entries.value.timeline?.events?.filter { - it.type == EventType.REDACTION - } - .orEmpty() - .mapNotNull { it.redacts } - } - .fold(emptyList(), { acc, next -> - acc + next - }) + .asSequence() + .mapNotNull { (_, value) -> value.timeline?.events } + .flatten() + .filter { it.type == EventType.REDACTION } + .mapNotNull { it.redacts } + .toList() Timber.v("[PushRules] Found ${allRedactedEvents.size} redacted events") @@ -107,18 +99,11 @@ internal class DefaultProcessEventForPushTask @Inject constructor( private fun fulfilledBingRule(event: Event, rules: List): PushRule? { // TODO This should be injected val conditionResolver = DefaultConditionResolver(event, roomService, userId) - rules.filter { it.enabled }.forEach { rule -> - val isFullfilled = rule.conditions?.map { + return rules.firstOrNull { rule -> + // All conditions must hold true for an event in order to apply the action for the event. + rule.enabled && rule.conditions?.all { it.asExecutableCondition()?.isSatisfied(conditionResolver) ?: false - }?.fold(true/*A rule with no conditions always matches*/, { acc, next -> - // All conditions must hold true for an event in order to apply the action for the event. - acc && next - }) ?: false - - if (isFullfilled) { - return rule - } + } ?: false } - return null } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt index 74b8d3c3a7..b50424b343 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt @@ -132,8 +132,7 @@ internal class RoomMembers(private val realm: Realm, .findAll() .map { it.asDomain() } .associateBy { it.stateKey!! } - .mapValues { it.value.content.toModel()!! } - .filterValues { predicate(it) } + .filterValues { predicate(it.content.toModel()!!) } .keys .toList() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FindReactionEventForUndoTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FindReactionEventForUndoTask.kt index 997ed18492..5f9f9e83ea 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FindReactionEventForUndoTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FindReactionEventForUndoTask.kt @@ -48,23 +48,22 @@ internal class DefaultFindReactionEventForUndoTask @Inject constructor(private v } private fun getReactionToRedact(realm: Realm, reaction: String, eventId: String, userId: String): EventEntity? { - val summary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() - if (summary != null) { - summary.reactionsSummary.where() - .equalTo(ReactionAggregatedSummaryEntityFields.KEY, reaction) - .findFirst()?.let { - // want to find the event orignated by me! - it.sourceEvents.forEach { - // find source event - EventEntity.where(realm, it).findFirst()?.let { eventEntity -> - // is it mine? - if (eventEntity.sender == userId) { - return eventEntity - } - } - } - } - } - return null + val summary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() ?: return null + + val rase = summary.reactionsSummary.where() + .equalTo(ReactionAggregatedSummaryEntityFields.KEY, reaction) + .findFirst() ?: return null + + // want to find the event orignated by me! + return rase.sourceEvents + .asSequence() + .mapNotNull { + // find source event + EventEntity.where(realm, it).findFirst() + } + .firstOrNull { eventEntity -> + // is it mine? + eventEntity.sender == userId + } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt index 9562f57d63..555a19d842 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt @@ -39,11 +39,10 @@ internal class TimelineEventDecryptor( override fun onNewSession(roomId: String?, senderKey: String, sessionId: String) { synchronized(unknownSessionsFailure) { val toDecryptAgain = ArrayList() - unknownSessionsFailure[sessionId]?.let { eventIds -> - toDecryptAgain.addAll(eventIds) - } + val eventIds = unknownSessionsFailure[sessionId] + if (eventIds != null) toDecryptAgain.addAll(eventIds) if (toDecryptAgain.isNotEmpty()) { - unknownSessionsFailure[sessionId]?.clear() + eventIds?.clear() toDecryptAgain.forEach { requestDecryption(it) } @@ -72,16 +71,15 @@ internal class TimelineEventDecryptor( fun requestDecryption(eventId: String) { synchronized(existingRequests) { - if (existingRequests.contains(eventId)) { - return Unit.also { - Timber.d("Skip Decryption request for event $eventId, already requested") - } + if (eventId in existingRequests) { + Timber.d("Skip Decryption request for event $eventId, already requested") + return } existingRequests.add(eventId) } synchronized(unknownSessionsFailure) { unknownSessionsFailure.values.forEach { - if (it.contains(eventId)) return@synchronized Unit.also { + if (eventId in it) { Timber.d("Skip Decryption request for event $eventId, unknown session") } } @@ -116,10 +114,7 @@ internal class TimelineEventDecryptor( event.content?.toModel()?.let { content -> content.sessionId?.let { sessionId -> synchronized(unknownSessionsFailure) { - val list = unknownSessionsFailure[sessionId] - ?: ArrayList().also { - unknownSessionsFailure[sessionId] = it - } + val list = unknownSessionsFailure.getOrPut(sessionId) { ArrayList() } list.add(eventId) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/SecretStoringUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/SecretStoringUtils.kt index de1c7b0479..260f98d97f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/SecretStoringUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/SecretStoringUtils.kt @@ -461,9 +461,9 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte inputCipher.init(Cipher.ENCRYPT_MODE, privateKeyEntry.certificate.publicKey) val outputStream = ByteArrayOutputStream() - val cipherOutputStream = CipherOutputStream(outputStream, inputCipher) - cipherOutputStream.write(secret) - cipherOutputStream.close() + CipherOutputStream(outputStream, inputCipher).use { + it.write(secret) + } return outputStream.toByteArray() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/CryptoSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/CryptoSyncHandler.kt index b7ad577203..91397fae7e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/CryptoSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/CryptoSyncHandler.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.session.sync -import android.text.TextUtils import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType @@ -41,9 +40,9 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService: initialSyncProgressService?.reportProgress(((index / total.toFloat()) * 100).toInt()) // Decrypt event if necessary decryptEvent(event, null) - if (TextUtils.equals(event.getClearType(), EventType.MESSAGE) + if (event.getClearType() == EventType.MESSAGE && event.getClearContent()?.toModel()?.type == "m.bad.encrypted") { - Timber.e("## handleToDeviceEvent() : Warning: Unable to decrypt to-device event : " + event.content) + Timber.e("## handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}") } else { sasVerificationService.onToDeviceEvent(event) cryptoService.onToDeviceEvent(event) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt index c630f95b29..4e57aa5be1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt @@ -31,7 +31,8 @@ import im.vector.matrix.android.internal.task.TaskThread import im.vector.matrix.android.internal.task.configureWith import timber.log.Timber import java.net.SocketTimeoutException -import java.util.* +import java.util.Timer +import java.util.TimerTask /** * Can execute periodic sync task. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/DirectChatsHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/DirectChatsHelper.kt index cfb8a48038..b5f404019a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/DirectChatsHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/DirectChatsHelper.kt @@ -31,18 +31,10 @@ internal class DirectChatsHelper @Inject constructor(@SessionDatabase */ fun getLocalUserAccount(filterRoomId: String? = null): MutableMap> { return Realm.getInstance(realmConfiguration).use { realm -> - val currentDirectRooms = RoomSummaryEntity.getDirectRooms(realm) - val directChatsMap = mutableMapOf>() - for (directRoom in currentDirectRooms) { - if (directRoom.roomId == filterRoomId) continue - val directUserId = directRoom.directUserId ?: continue - directChatsMap - .getOrPut(directUserId, { arrayListOf() }) - .apply { - add(directRoom.roomId) - } - } - directChatsMap + RoomSummaryEntity.getDirectRooms(realm) + .asSequence() + .filter { it.roomId != filterRoomId && it.directUserId != null } + .groupByTo(mutableMapOf(), { it.directUserId!! }, { it.roomId }) } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/ConfigurableTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/ConfigurableTask.kt index 366f4e3d30..1bf939b91b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/ConfigurableTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/ConfigurableTask.kt @@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.task import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.util.Cancelable -import java.util.* +import java.util.UUID internal fun Task.configureWith(params: PARAMS, init: (ConfigurableTask.Builder.() -> Unit) = {} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CompatUtil.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CompatUtil.kt index 2e71d6b2fc..058a862bc8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CompatUtil.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CompatUtil.kt @@ -59,19 +59,11 @@ object CompatUtil { private const val SHARED_KEY_ANDROID_VERSION_WHEN_KEY_HAS_BEEN_GENERATED = "android_version_when_key_has_been_generated" private var sSecretKeyAndVersion: SecretKeyAndVersion? = null - private var sPrng: SecureRandom? = null /** * Returns the unique SecureRandom instance shared for all local storage encryption operations. */ - private val prng: SecureRandom - get() { - if (sPrng == null) { - sPrng = SecureRandom() - } - - return sPrng!! - } + private val prng: SecureRandom by lazy(LazyThreadSafetyMode.NONE) { SecureRandom() } /** * Create a GZIPOutputStream instance diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Hash.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Hash.kt index e57c289388..e748ad2778 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Hash.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Hash.kt @@ -24,12 +24,9 @@ import java.security.MessageDigest fun String.md5() = try { val digest = MessageDigest.getInstance("md5") digest.update(toByteArray()) - val bytes = digest.digest() - val sb = StringBuilder() - for (i in bytes.indices) { - sb.append(String.format("%02X", bytes[i])) - } - sb.toString().toLowerCase() + digest.digest() + .joinToString("") { String.format("%02X", it) } + .toLowerCase() } catch (exc: Exception) { // Should not happen, but just in case hashCode().toString() diff --git a/vector/src/gplay/java/im/vector/riotx/gplay/push/fcm/VectorFirebaseMessagingService.kt b/vector/src/gplay/java/im/vector/riotx/gplay/push/fcm/VectorFirebaseMessagingService.kt index ac5b5c882d..271d747fef 100755 --- a/vector/src/gplay/java/im/vector/riotx/gplay/push/fcm/VectorFirebaseMessagingService.kt +++ b/vector/src/gplay/java/im/vector/riotx/gplay/push/fcm/VectorFirebaseMessagingService.kt @@ -21,7 +21,6 @@ package im.vector.riotx.gplay.push.fcm import android.os.Handler import android.os.Looper -import android.text.TextUtils import androidx.lifecycle.Lifecycle import androidx.lifecycle.ProcessLifecycleOwner import com.google.firebase.messaging.FirebaseMessagingService @@ -214,10 +213,10 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { } } else { if (notifiableEvent is NotifiableMessageEvent) { - if (TextUtils.isEmpty(notifiableEvent.senderName)) { + if (notifiableEvent.senderName.isEmpty()) { notifiableEvent.senderName = data["sender_display_name"] ?: data["sender"] ?: "" } - if (TextUtils.isEmpty(notifiableEvent.roomName)) { + if (notifiableEvent.roomName.isEmpty()) { notifiableEvent.roomName = findRoomNameBestEffort(data, session) ?: "" } } diff --git a/vector/src/main/java/im/vector/riotx/core/dialogs/ExportKeysDialog.kt b/vector/src/main/java/im/vector/riotx/core/dialogs/ExportKeysDialog.kt index dc88dec406..fb320afded 100644 --- a/vector/src/main/java/im/vector/riotx/core/dialogs/ExportKeysDialog.kt +++ b/vector/src/main/java/im/vector/riotx/core/dialogs/ExportKeysDialog.kt @@ -18,7 +18,6 @@ package im.vector.riotx.core.dialogs import android.app.Activity import android.text.Editable -import android.text.TextUtils import android.widget.Button import android.widget.ImageView import androidx.appcompat.app.AlertDialog @@ -45,15 +44,15 @@ class ExportKeysDialog { val textWatcher = object : SimpleTextWatcher() { override fun afterTextChanged(s: Editable) { when { - TextUtils.isEmpty(passPhrase1EditText.text) -> { + passPhrase1EditText.text.isNullOrEmpty() -> { exportButton.isEnabled = false passPhrase2Til.error = null } - TextUtils.equals(passPhrase1EditText.text, passPhrase2EditText.text) -> { + passPhrase1EditText.text == passPhrase2EditText.text -> { exportButton.isEnabled = true passPhrase2Til.error = null } - else -> { + else -> { exportButton.isEnabled = false passPhrase2Til.error = activity.getString(R.string.passphrase_passphrase_does_not_match) } diff --git a/vector/src/main/java/im/vector/riotx/core/intent/ExternalIntentAnalyser.kt b/vector/src/main/java/im/vector/riotx/core/intent/ExternalIntentAnalyser.kt index d04290b7c1..11710d3e13 100644 --- a/vector/src/main/java/im/vector/riotx/core/intent/ExternalIntentAnalyser.kt +++ b/vector/src/main/java/im/vector/riotx/core/intent/ExternalIntentAnalyser.kt @@ -21,9 +21,7 @@ import android.content.ClipDescription import android.content.Intent import android.net.Uri import android.os.Build -import android.text.TextUtils import androidx.core.util.PatternsCompat.WEB_URL -import java.util.* /** * Inspired from Riot code: RoomMediaMessage.java @@ -69,34 +67,28 @@ fun analyseIntent(intent: Intent): List { // chrome adds many items when sharing an web page link // so, test first the type - if (TextUtils.equals(intent.type, ClipDescription.MIMETYPE_TEXT_PLAIN)) { + if (intent.type == ClipDescription.MIMETYPE_TEXT_PLAIN) { var message: String? = intent.getStringExtra(Intent.EXTRA_TEXT) - - if (null == message) { - val sequence = intent.getCharSequenceExtra(Intent.EXTRA_TEXT) - if (null != sequence) { - message = sequence.toString() - } - } + ?: intent.getCharSequenceExtra(Intent.EXTRA_TEXT)?.toString() val subject = intent.getStringExtra(Intent.EXTRA_SUBJECT) - if (!TextUtils.isEmpty(subject)) { - if (TextUtils.isEmpty(message)) { + if (!subject.isNullOrEmpty()) { + if (message.isNullOrEmpty()) { message = subject - } else if (WEB_URL.matcher(message!!).matches()) { + } else if (WEB_URL.matcher(message).matches()) { message = subject + "\n" + message } } - if (!TextUtils.isEmpty(message)) { - externalIntentDataList.add(ExternalIntentData.IntentDataText(message!!, null, intent.type)) + if (!message.isNullOrEmpty()) { + externalIntentDataList.add(ExternalIntentData.IntentDataText(message, null, intent.type)) return externalIntentDataList } } var clipData: ClipData? = null - var mimetypes: MutableList? = null + var mimeTypes: List? = null if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { clipData = intent.clipData @@ -106,41 +98,26 @@ fun analyseIntent(intent: Intent): List { if (null != clipData) { if (null != clipData.description) { if (0 != clipData.description.mimeTypeCount) { - mimetypes = ArrayList() - - for (i in 0 until clipData.description.mimeTypeCount) { - mimetypes.add(clipData.description.getMimeType(i)) + mimeTypes = with(clipData.description) { + List(mimeTypeCount) { getMimeType(it) } } // if the filter is "accept anything" the mimetype does not make sense - if (1 == mimetypes.size) { - if (mimetypes[0].endsWith("/*")) { - mimetypes = null + if (1 == mimeTypes.size) { + if (mimeTypes[0].endsWith("/*")) { + mimeTypes = null } } } } - val count = clipData.itemCount - - for (i in 0 until count) { + for (i in 0 until clipData.itemCount) { val item = clipData.getItemAt(i) - var mimetype: String? = null + val mimeType = mimeTypes?.getOrElse(i) { mimeTypes[0] } + // uris list is not a valid mimetype + .takeUnless { it == ClipDescription.MIMETYPE_TEXT_URILIST } - if (null != mimetypes) { - if (i < mimetypes.size) { - mimetype = mimetypes[i] - } else { - mimetype = mimetypes[0] - } - - // uris list is not a valid mimetype - if (TextUtils.equals(mimetype, ClipDescription.MIMETYPE_TEXT_URILIST)) { - mimetype = null - } - } - - externalIntentDataList.add(ExternalIntentData.IntentDataClipData(item, mimetype)) + externalIntentDataList.add(ExternalIntentData.IntentDataClipData(item, mimeType)) } } else if (null != intent.data) { externalIntentDataList.add(ExternalIntentData.IntentDataUri(intent.data!!)) diff --git a/vector/src/main/java/im/vector/riotx/core/preference/BingRulePreference.kt b/vector/src/main/java/im/vector/riotx/core/preference/BingRulePreference.kt index 5c0bc88765..76df61dd33 100755 --- a/vector/src/main/java/im/vector/riotx/core/preference/BingRulePreference.kt +++ b/vector/src/main/java/im/vector/riotx/core/preference/BingRulePreference.kt @@ -17,7 +17,6 @@ package im.vector.riotx.core.preference import android.content.Context -import android.text.TextUtils import android.util.AttributeSet import android.view.View import android.widget.RadioGroup @@ -84,7 +83,7 @@ class BingRulePreference : VectorPreference { val ruleStatusIndex: Int get() { if (null != rule) { - if (TextUtils.equals(rule!!.ruleId, BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS)) { + if (rule!!.ruleId == BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS) { if (rule!!.shouldNotNotify()) { return if (rule!!.isEnabled) { NOTIFICATION_OFF_INDEX @@ -143,7 +142,7 @@ class BingRulePreference : VectorPreference { if (null != this.rule && index != ruleStatusIndex) { rule = BingRule(this.rule!!) - if (TextUtils.equals(rule.ruleId, BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS)) { + if (rule.ruleId == BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS) { when (index) { NOTIFICATION_OFF_INDEX -> { rule.isEnabled = true @@ -164,8 +163,8 @@ class BingRulePreference : VectorPreference { } if (NOTIFICATION_OFF_INDEX == index) { - if (TextUtils.equals(this.rule!!.kind, BingRule.KIND_UNDERRIDE) - || TextUtils.equals(rule.ruleId, BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS)) { + if (this.rule!!.kind == BingRule.KIND_UNDERRIDE + || rule.ruleId == BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS) { rule.setNotify(false) } else { rule.isEnabled = false @@ -173,11 +172,11 @@ class BingRulePreference : VectorPreference { } else { rule.isEnabled = true rule.setNotify(true) - rule.setHighlight(!TextUtils.equals(this.rule!!.kind, BingRule.KIND_UNDERRIDE) - && !TextUtils.equals(rule.ruleId, BingRule.RULE_ID_INVITE_ME) + rule.setHighlight(this.rule!!.kind != BingRule.KIND_UNDERRIDE + && rule.ruleId != BingRule.RULE_ID_INVITE_ME && NOTIFICATION_NOISY_INDEX == index) if (NOTIFICATION_NOISY_INDEX == index) { - rule.notificationSound = if (TextUtils.equals(rule.ruleId, BingRule.RULE_ID_CALL)) { + rule.notificationSound = if (rule.ruleId == BingRule.RULE_ID_CALL) { BingRule.ACTION_VALUE_RING } else { BingRule.ACTION_VALUE_DEFAULT diff --git a/vector/src/main/java/im/vector/riotx/core/resources/ResourceUtils.kt b/vector/src/main/java/im/vector/riotx/core/resources/ResourceUtils.kt index dad95c97be..236caec081 100644 --- a/vector/src/main/java/im/vector/riotx/core/resources/ResourceUtils.kt +++ b/vector/src/main/java/im/vector/riotx/core/resources/ResourceUtils.kt @@ -18,7 +18,6 @@ package im.vector.riotx.core.resources import android.content.Context import android.net.Uri -import android.text.TextUtils import android.webkit.MimeTypeMap import im.vector.riotx.core.utils.getFileExtension import timber.log.Timber @@ -73,7 +72,7 @@ fun openResource(context: Context, uri: Uri, providedMimetype: String?): Resourc var mimetype = providedMimetype try { // if the mime type is not provided, try to find it out - if (TextUtils.isEmpty(mimetype)) { + if (mimetype.isNullOrEmpty()) { mimetype = context.contentResolver.getType(uri) // try to find the mimetype from the filename diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/NotificationAreaView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/NotificationAreaView.kt index b159b7b03b..145a26aed2 100644 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/NotificationAreaView.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/NotificationAreaView.kt @@ -20,7 +20,6 @@ import android.content.Context import android.graphics.Color import android.text.SpannableString import android.text.TextPaint -import android.text.TextUtils import android.text.method.LinkMovementMethod import android.text.style.ClickableSpan import android.util.AttributeSet @@ -168,7 +167,7 @@ class NotificationAreaView @JvmOverloads constructor( } else { imageView.setImageResource(R.drawable.scrolldown) messageView.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_room_notification_text_color)) - if (!TextUtils.isEmpty(state.message)) { + if (!state.message.isNullOrEmpty()) { messageView.text = SpannableString(state.message) } } diff --git a/vector/src/main/java/im/vector/riotx/core/utils/FileUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/FileUtils.kt index 4fd574f904..915a11b5de 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/FileUtils.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/FileUtils.kt @@ -17,7 +17,6 @@ package im.vector.riotx.core.utils import android.content.Context -import android.text.TextUtils import timber.log.Timber import java.io.File @@ -60,7 +59,7 @@ private fun logAction(file: File): Boolean { if (file.isDirectory) { Timber.v(file.toString()) } else { - Timber.v(file.toString() + " " + file.length() + " bytes") + Timber.v("$file ${file.length()} bytes") } return true } @@ -96,26 +95,19 @@ private fun recursiveActionOnFile(file: File, action: ActionOnFile): Boolean { fun getFileExtension(fileUri: String): String? { var reducedStr = fileUri - if (!TextUtils.isEmpty(reducedStr)) { + if (reducedStr.isNotEmpty()) { // Remove fragment - val fragment = fileUri.lastIndexOf('#') - if (fragment > 0) { - reducedStr = fileUri.substring(0, fragment) - } + reducedStr = reducedStr.substringBeforeLast('#') // Remove query - val query = reducedStr.lastIndexOf('?') - if (query > 0) { - reducedStr = reducedStr.substring(0, query) - } + reducedStr = reducedStr.substringBeforeLast('?') // Remove path - val filenamePos = reducedStr.lastIndexOf('/') - val filename = if (0 <= filenamePos) reducedStr.substring(filenamePos + 1) else reducedStr + val filename = reducedStr.substringAfterLast('/') // Contrary to method MimeTypeMap.getFileExtensionFromUrl, we do not check the pattern // See https://stackoverflow.com/questions/14320527/android-should-i-use-mimetypemap-getfileextensionfromurl-bugs - if (!filename.isEmpty()) { + if (filename.isNotEmpty()) { val dotPos = filename.lastIndexOf('.') if (0 <= dotPos) { val ext = filename.substring(dotPos + 1) @@ -135,14 +127,10 @@ fun getFileExtension(fileUri: String): String? { * ========================================================================================== */ fun getSizeOfFiles(context: Context, root: File): Int { - Timber.v("Get size of " + root.absolutePath) - return if (root.isDirectory) { - root.list() - .map { - getSizeOfFiles(context, File(root, it)) - } - .fold(0, { acc, other -> acc + other }) - } else { - root.length().toInt() - } + return root.walkTopDown() + .onEnter { + Timber.v("Get size of ${it.absolutePath}") + true + } + .sumBy { root.length().toInt() } } diff --git a/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt b/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt index b97614eaa5..346a4b07e9 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt @@ -21,7 +21,6 @@ import android.app.Activity import android.content.Context import android.content.pm.PackageManager import android.os.Build -import android.text.TextUtils import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.core.app.ActivityCompat @@ -29,7 +28,6 @@ import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import im.vector.riotx.R import timber.log.Timber -import java.util.* private const val LOG_TAG = "PermissionUtils" @@ -72,7 +70,7 @@ const val PERMISSION_REQUEST_CODE_DOWNLOAD_FILE = 575 */ fun logPermissionStatuses(context: Context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val permissions = Arrays.asList( + val permissions = listOf( Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO, Manifest.permission.WRITE_EXTERNAL_STORAGE, @@ -221,25 +219,25 @@ private fun checkPermissions(permissionsToBeGrantedBitMap: Int, permissionListAlreadyDenied.forEach { when (it) { Manifest.permission.CAMERA -> { - if (!TextUtils.isEmpty(explanationMessage)) { + if (explanationMessage.isNotEmpty()) { explanationMessage += "\n\n" } explanationMessage += activity.getString(R.string.permissions_rationale_msg_camera) } Manifest.permission.RECORD_AUDIO -> { - if (!TextUtils.isEmpty(explanationMessage)) { + if (explanationMessage.isNotEmpty()) { explanationMessage += "\n\n" } explanationMessage += activity.getString(R.string.permissions_rationale_msg_record_audio) } Manifest.permission.WRITE_EXTERNAL_STORAGE -> { - if (!TextUtils.isEmpty(explanationMessage)) { + if (explanationMessage.isNotEmpty()) { explanationMessage += "\n\n" } explanationMessage += activity.getString(R.string.permissions_rationale_msg_storage) } Manifest.permission.READ_CONTACTS -> { - if (!TextUtils.isEmpty(explanationMessage)) { + if (!explanationMessage.isEmpty()) { explanationMessage += "\n\n" } explanationMessage += activity.getString(R.string.permissions_rationale_msg_contacts) @@ -255,7 +253,7 @@ private fun checkPermissions(permissionsToBeGrantedBitMap: Int, .setMessage(explanationMessage) .setOnCancelListener { Toast.makeText(activity, R.string.missing_permissions_warning, Toast.LENGTH_SHORT).show() } .setPositiveButton(R.string.ok) { _, _ -> - if (!permissionsListToBeGranted.isEmpty()) { + if (permissionsListToBeGranted.isNotEmpty()) { fragment?.requestPermissions(permissionsListToBeGranted.toTypedArray(), requestCode) ?: run { ActivityCompat.requestPermissions(activity, permissionsListToBeGranted.toTypedArray(), requestCode) diff --git a/vector/src/main/java/im/vector/riotx/core/utils/TextUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/TextUtils.kt index f299589119..0b5df0d2e0 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/TextUtils.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/TextUtils.kt @@ -24,9 +24,9 @@ import java.util.* object TextUtils { private val suffixes = TreeMap().also { - it.put(1000, "k") - it.put(1000000, "M") - it.put(1000000000, "G") + it[1000] = "k" + it[1000000] = "M" + it[1000000000] = "G" } fun formatCountToShortDecimal(value: Int): String { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt index b87972b28c..f4717fa7b3 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt @@ -17,7 +17,6 @@ package im.vector.riotx.features.crypto.keysbackup.setup import android.os.AsyncTask import android.os.Bundle -import android.text.TextUtils import android.view.ViewGroup import android.view.inputmethod.EditorInfo import android.widget.EditText @@ -122,7 +121,7 @@ class KeysBackupSetupStep2Fragment : VectorBaseFragment() { }) viewModel.passphrase.observe(this, Observer { newValue -> - if (TextUtils.isEmpty(newValue)) { + if (newValue.isEmpty()) { viewModel.passwordStrength.value = null } else { AsyncTask.execute { @@ -172,7 +171,7 @@ class KeysBackupSetupStep2Fragment : VectorBaseFragment() { @OnClick(R.id.keys_backup_setup_step2_button) fun doNext() { when { - TextUtils.isEmpty(viewModel.passphrase.value) -> { + viewModel.passphrase.value.isNullOrEmpty() -> { viewModel.passphraseError.value = context?.getString(R.string.passphrase_empty_error_message) } viewModel.passphrase.value != viewModel.confirmPassphrase.value -> { @@ -192,7 +191,7 @@ class KeysBackupSetupStep2Fragment : VectorBaseFragment() { @OnClick(R.id.keys_backup_setup_step2_skip_button) fun skipPassphrase() { when { - TextUtils.isEmpty(viewModel.passphrase.value) -> { + viewModel.passphrase.value.isNullOrEmpty() -> { // Generate a recovery key for the user viewModel.megolmBackupCreationInfo = null diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt index 8e9ae8c08f..f5db16c8ee 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt @@ -20,7 +20,6 @@ package im.vector.riotx.features.crypto.keysrequest import android.content.Context -import android.text.TextUtils import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener @@ -39,7 +38,8 @@ import im.vector.riotx.features.popup.PopupAlertManager import timber.log.Timber import java.text.DateFormat import java.text.SimpleDateFormat -import java.util.* +import java.util.Locale +import java.util.Date import javax.inject.Inject import javax.inject.Singleton import kotlin.collections.ArrayList @@ -100,7 +100,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context) alertsToRequests[mappingKey] = ArrayList().apply { this.add(request) } // Add a notification for every incoming request - session?.downloadKeys(Arrays.asList(userId), false, object : MatrixCallback> { + session?.downloadKeys(listOf(userId), false, object : MatrixCallback> { override fun onSuccess(data: MXUsersDevicesMap) { val deviceInfo = data.getObject(userId, deviceId) @@ -147,7 +147,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context) wasNewDevice: Boolean, deviceInfo: MXDeviceInfo?, moreInfo: DeviceInfo? = null) { - val deviceName = if (TextUtils.isEmpty(deviceInfo!!.displayName())) deviceInfo.deviceId else deviceInfo.displayName() + val deviceName = if (deviceInfo!!.displayName().isNullOrEmpty()) deviceInfo.deviceId else deviceInfo.displayName() val dialogText: String? if (moreInfo != null) { @@ -244,12 +244,12 @@ class KeyRequestHandler @Inject constructor(private val context: Context) val deviceId = request.deviceId val requestId = request.requestId - if (TextUtils.isEmpty(userId) || TextUtils.isEmpty(deviceId) || TextUtils.isEmpty(requestId)) { + if (userId.isNullOrEmpty() || deviceId.isNullOrEmpty() || requestId.isNullOrEmpty()) { Timber.e("## handleKeyRequestCancellation() : invalid parameters") return } - val alertMgrUniqueKey = alertManagerId(deviceId!!, userId!!) + val alertMgrUniqueKey = alertManagerId(deviceId, userId) alertsToRequests[alertMgrUniqueKey]?.removeAll { it.deviceId == request.deviceId && it.userId == request.userId diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 7db9fc41d5..a9e13df859 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -17,7 +17,6 @@ package im.vector.riotx.features.home.room.detail import android.net.Uri -import android.text.TextUtils import androidx.annotation.IdRes import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -376,7 +375,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro val document = parser.parse(finalText) val renderer = HtmlRenderer.builder().build() val htmlText = renderer.render(document) - if (TextUtils.equals(finalText, htmlText)) { + if (finalText == htmlText) { room.sendTextMessage(finalText) } else { room.sendFormattedTextMessage(finalText, htmlText) @@ -401,19 +400,22 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private fun legacyRiotQuoteText(quotedText: String?, myText: String): String { val messageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray() - val quotedTextMsg = StringBuilder() - if (messageParagraphs != null) { - for (i in messageParagraphs.indices) { - if (messageParagraphs[i].trim() != "") { - quotedTextMsg.append("> ").append(messageParagraphs[i]) - } + return buildString { + if (messageParagraphs != null) { + for (i in messageParagraphs.indices) { + if (messageParagraphs[i].isNotBlank()) { + append("> ") + append(messageParagraphs[i]) + } - if (i + 1 != messageParagraphs.size) { - quotedTextMsg.append("\n\n") + if (i != messageParagraphs.lastIndex) { + append("\n\n") + } } } + append("\n\n") + append(myText) } - return "$quotedTextMsg\n\n$myText" } private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt index 0edf3b72e0..7a654171d9 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt @@ -130,8 +130,8 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room setState { copy( - joiningRoomsIds = joiningRoomsIds.toMutableSet().apply { add(roomId) }, - rejectingErrorRoomsIds = rejectingErrorRoomsIds.toMutableSet().apply { remove(roomId) } + joiningRoomsIds = joiningRoomsIds + roomId, + rejectingErrorRoomsIds = rejectingErrorRoomsIds - roomId ) } @@ -147,8 +147,8 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room setState { copy( - joiningRoomsIds = joiningRoomsIds.toMutableSet().apply { remove(roomId) }, - joiningErrorRoomsIds = joiningErrorRoomsIds.toMutableSet().apply { add(roomId) } + joiningRoomsIds = joiningRoomsIds - roomId, + joiningErrorRoomsIds = joiningErrorRoomsIds - roomId ) } } @@ -166,8 +166,8 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room setState { copy( - rejectingRoomsIds = rejectingRoomsIds.toMutableSet().apply { add(roomId) }, - joiningErrorRoomsIds = joiningErrorRoomsIds.toMutableSet().apply { remove(roomId) } + rejectingRoomsIds = rejectingRoomsIds + roomId, + joiningErrorRoomsIds = joiningErrorRoomsIds - roomId ) } @@ -185,8 +185,8 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room setState { copy( - rejectingRoomsIds = rejectingRoomsIds.toMutableSet().apply { remove(roomId) }, - rejectingErrorRoomsIds = rejectingErrorRoomsIds.toMutableSet().apply { add(roomId) } + rejectingRoomsIds = rejectingRoomsIds - roomId, + rejectingErrorRoomsIds = rejectingErrorRoomsIds + roomId ) } } diff --git a/vector/src/main/java/im/vector/riotx/features/homeserver/ServerUrlsRepository.kt b/vector/src/main/java/im/vector/riotx/features/homeserver/ServerUrlsRepository.kt index 23d6c09ec2..9535499d70 100644 --- a/vector/src/main/java/im/vector/riotx/features/homeserver/ServerUrlsRepository.kt +++ b/vector/src/main/java/im/vector/riotx/features/homeserver/ServerUrlsRepository.kt @@ -17,7 +17,6 @@ package im.vector.riotx.features.homeserver import android.content.Context -import android.text.TextUtils import androidx.core.content.edit import androidx.preference.PreferenceManager import im.vector.riotx.R @@ -41,11 +40,11 @@ object ServerUrlsRepository { fun setDefaultUrlsFromReferrer(context: Context, homeServerUrl: String, identityServerUrl: String) { PreferenceManager.getDefaultSharedPreferences(context) .edit { - if (!TextUtils.isEmpty(homeServerUrl)) { + if (homeServerUrl.isNotEmpty()) { putString(DEFAULT_REFERRER_HOME_SERVER_URL_PREF, homeServerUrl) } - if (!TextUtils.isEmpty(identityServerUrl)) { + if (identityServerUrl.isNotEmpty()) { putString(DEFAULT_REFERRER_IDENTITY_SERVER_URL_PREF, identityServerUrl) } } diff --git a/vector/src/main/java/im/vector/riotx/features/rageshake/BugReportActivity.kt b/vector/src/main/java/im/vector/riotx/features/rageshake/BugReportActivity.kt index 3aaaa82b49..187566f660 100755 --- a/vector/src/main/java/im/vector/riotx/features/rageshake/BugReportActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/rageshake/BugReportActivity.kt @@ -16,7 +16,6 @@ package im.vector.riotx.features.rageshake -import android.text.TextUtils import android.view.Menu import android.view.MenuItem import android.widget.Toast @@ -122,7 +121,7 @@ class BugReportActivity : VectorBaseActivity() { object : BugReporter.IMXBugReportListener { override fun onUploadFailed(reason: String?) { try { - if (!TextUtils.isEmpty(reason)) { + if (!reason.isNullOrEmpty()) { if (forSuggestion) { Toast.makeText(this@BugReportActivity, getString(R.string.send_suggestion_failed, reason), Toast.LENGTH_LONG).show() diff --git a/vector/src/main/java/im/vector/riotx/features/rageshake/BugReporter.kt b/vector/src/main/java/im/vector/riotx/features/rageshake/BugReporter.kt index e358823316..9a7707d063 100755 --- a/vector/src/main/java/im/vector/riotx/features/rageshake/BugReporter.kt +++ b/vector/src/main/java/im/vector/riotx/features/rageshake/BugReporter.kt @@ -25,7 +25,6 @@ import android.content.Intent import android.graphics.Bitmap import android.os.AsyncTask import android.os.Build -import android.text.TextUtils import android.view.View import im.vector.matrix.android.api.Matrix import im.vector.riotx.BuildConfig @@ -166,14 +165,11 @@ class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSes if (withDevicesLogs) { val files = vectorFileLogger.getLogFiles() - - for (f in files) { + files.mapNotNullTo(gzippedFiles) { f -> if (!mIsCancelled) { - val gzippedFile = compressFile(f) - - if (null != gzippedFile) { - gzippedFiles.add(gzippedFile) - } + compressFile(f) + } else { + null } } } @@ -244,7 +240,7 @@ class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSes .addFormDataPart("theme", ThemeUtils.getApplicationTheme(context)) val buildNumber = context.getString(R.string.build_number) - if (!TextUtils.isEmpty(buildNumber) && buildNumber != "0") { + if (buildNumber.isNotEmpty() && buildNumber != "0") { builder.addFormDataPart("build_number", buildNumber) } @@ -266,10 +262,9 @@ class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSes } try { - val fos = FileOutputStream(logCatScreenshotFile) - bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos) - fos.flush() - fos.close() + logCatScreenshotFile.outputStream().use { + bitmap.compress(Bitmap.CompressFormat.PNG, 100, it) + } builder.addFormDataPart("file", logCatScreenshotFile.name, logCatScreenshotFile.asRequestBody("application/octet-stream".toMediaTypeOrNull())) @@ -303,16 +298,14 @@ class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSes // add a progress listener requestBody.setWriteListener { totalWritten, contentLength -> - val percentage: Int - - if (-1L != contentLength) { + val percentage = if (-1L != contentLength) { if (totalWritten > contentLength) { - percentage = 100 + 100 } else { - percentage = (totalWritten * 100 / contentLength).toInt() + (totalWritten * 100 / contentLength).toInt() } } else { - percentage = 0 + 0 } if (mIsCancelled && null != mBugReportCall) { @@ -350,19 +343,18 @@ class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSes } else if (null == response || null == response.body) { serverError = "Failed with error $responseCode" } else { - var inputStream: InputStream? = null - try { - inputStream = response.body!!.byteStream() + val inputStream = response.body!!.byteStream() - var ch = inputStream.read() - val b = StringBuilder() - while (ch != -1) { - b.append(ch.toChar()) - ch = inputStream.read() + serverError = inputStream.use { + buildString { + var ch = it.read() + while (ch != -1) { + append(ch.toChar()) + ch = it.read() + } + } } - serverError = b.toString() - inputStream.close() // check if the error message try { @@ -378,12 +370,6 @@ class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSes } } catch (e: Exception) { Timber.e(e, "## sendBugReport() : failed to parse error") - } finally { - try { - inputStream?.close() - } catch (e: Exception) { - Timber.e(e, "## sendBugReport() : failed to close the error stream") - } } } } @@ -481,15 +467,9 @@ class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSes crashFile.delete() } - if (!TextUtils.isEmpty(crashDescription)) { + if (crashDescription.isNotEmpty()) { try { - val fos = FileOutputStream(crashFile) - val osw = OutputStreamWriter(fos) - osw.write(crashDescription) - osw.close() - - fos.flush() - fos.close() + crashFile.writeText(crashDescription) } catch (e: Exception) { Timber.e(e, "## saveCrashReport() : fail to write $e") } @@ -503,25 +483,17 @@ class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSes * @return the crash description */ private fun getCrashDescription(context: Context): String? { - var crashDescription: String? = null val crashFile = getCrashFile(context) if (crashFile.exists()) { try { - val fis = FileInputStream(crashFile) - val isr = InputStreamReader(fis) - - val buffer = CharArray(fis.available()) - val len = isr.read(buffer, 0, fis.available()) - crashDescription = String(buffer, 0, len) - isr.close() - fis.close() + return crashFile.readText() } catch (e: Exception) { Timber.e(e, "## getCrashDescription() : fail to read $e") } } - return crashDescription + return null } // ============================================================================================================== @@ -589,13 +561,9 @@ class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSes } try { - val fos = FileOutputStream(logCatErrFile) - val osw = OutputStreamWriter(fos) - getLogCatError(osw, isErrorLogcat) - osw.close() - - fos.flush() - fos.close() + logCatErrFile.writer().use { + getLogCatError(it, isErrorLogcat) + } return compressFile(logCatErrFile) } catch (error: OutOfMemoryError) { @@ -622,26 +590,17 @@ class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSes return } - var reader: BufferedReader? = null try { val separator = System.getProperty("line.separator") - reader = BufferedReader(InputStreamReader(logcatProc.inputStream), BUFFER_SIZE) - var line = reader.readLine() - while (line != null) { - streamWriter.append(line) - streamWriter.append(separator) - line = reader.readLine() - } + logcatProc.inputStream + .reader() + .buffered(BUFFER_SIZE) + .forEachLine { line -> + streamWriter.append(line) + streamWriter.append(separator) + } } catch (e: IOException) { Timber.e(e, "getLog fails") - } finally { - if (reader != null) { - try { - reader.close() - } catch (e: IOException) { - Timber.e(e, "getLog fails with") - } - } } } @@ -658,45 +617,25 @@ class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSes private fun compressFile(fin: File): File? { Timber.v("## compressFile() : compress ${fin.name}") - val dstFile = File(fin.parent, fin.name + ".gz") + val dstFile = fin.resolveSibling(fin.name + ".gz") if (dstFile.exists()) { dstFile.delete() } - var fos: FileOutputStream? = null - var gos: GZIPOutputStream? = null - var inputStream: InputStream? = null try { - fos = FileOutputStream(dstFile) - gos = GZIPOutputStream(fos) - - inputStream = FileInputStream(fin) - - val buffer = ByteArray(2048) - var n = inputStream.read(buffer) - while (n != -1) { - gos.write(buffer, 0, n) - n = inputStream.read(buffer) + GZIPOutputStream(dstFile.outputStream()).use { gos -> + fin.inputStream().use { + it.copyTo(gos, 2048) + } } - gos.close() - inputStream.close() - Timber.v("## compressFile() : ${fin.length()} compressed to ${dstFile.length()} bytes") return dstFile } catch (e: Exception) { Timber.e(e, "## compressFile() failed") } catch (oom: OutOfMemoryError) { Timber.e(oom, "## compressFile() failed") - } finally { - try { - fos?.close() - gos?.close() - inputStream?.close() - } catch (e: Exception) { - Timber.e(e, "## compressFile() failed to close inputStream") - } } return null diff --git a/vector/src/main/java/im/vector/riotx/features/settings/FontScale.kt b/vector/src/main/java/im/vector/riotx/features/settings/FontScale.kt index c68edac6ce..a9e797ba7a 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/FontScale.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/FontScale.kt @@ -18,7 +18,6 @@ package im.vector.riotx.features.settings import android.content.Context import android.content.res.Configuration -import android.text.TextUtils import androidx.core.content.edit import androidx.preference.PreferenceManager import im.vector.riotx.R @@ -68,7 +67,7 @@ object FontScale { val preferences = PreferenceManager.getDefaultSharedPreferences(context) var scalePreferenceValue: String - if (!preferences.contains(APPLICATION_FONT_SCALE_KEY)) { + if (APPLICATION_FONT_SCALE_KEY !in preferences) { val fontScale = context.resources.configuration.fontScale scalePreferenceValue = FONT_SCALE_NORMAL @@ -96,9 +95,9 @@ object FontScale { val fontScale = getFontScalePrefValue(context) if (fontScaleToPrefValue.containsValue(fontScale)) { - for (entry in fontScaleToPrefValue) { - if (TextUtils.equals(entry.value, fontScale)) { - return entry.key + for ((key, value) in fontScaleToPrefValue) { + if (value == fontScale) { + return key } } } @@ -125,9 +124,9 @@ object FontScale { * @param fontScaleDescription the font scale description */ fun updateFontScale(context: Context, fontScaleDescription: String) { - for (entry in prefValueToNameResId) { - if (TextUtils.equals(context.getString(entry.value), fontScaleDescription)) { - saveFontScale(context, entry.key) + for ((key, value) in prefValueToNameResId) { + if (context.getString(value) == fontScaleDescription) { + saveFontScale(context, key) } } @@ -143,7 +142,7 @@ object FontScale { * @param scaleValue the text scale */ fun saveFontScale(context: Context, scaleValue: String) { - if (!TextUtils.isEmpty(scaleValue)) { + if (scaleValue.isNotEmpty()) { PreferenceManager.getDefaultSharedPreferences(context) .edit { putString(APPLICATION_FONT_SCALE_KEY, scaleValue) diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt index eae21cf5d4..7fef12cddf 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt @@ -20,15 +20,13 @@ import android.content.Context import android.content.res.Configuration import android.os.Build import android.preference.PreferenceManager -import android.text.TextUtils -import android.util.Pair import androidx.core.content.edit import im.vector.riotx.R import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import timber.log.Timber -import java.util.* +import java.util.Locale /** * Object to manage the Locale choice of the user @@ -68,7 +66,7 @@ object VectorLocale { // detect if the default language is used val defaultStringValue = getString(context, defaultLocale, R.string.resources_country_code) - if (TextUtils.equals(defaultStringValue, getString(context, applicationLocale, R.string.resources_country_code))) { + if (defaultStringValue == getString(context, applicationLocale, R.string.resources_country_code)) { applicationLocale = defaultLocale } @@ -89,21 +87,21 @@ object VectorLocale { PreferenceManager.getDefaultSharedPreferences(context).edit { val language = locale.language - if (TextUtils.isEmpty(language)) { + if (language.isEmpty()) { remove(APPLICATION_LOCALE_LANGUAGE_KEY) } else { putString(APPLICATION_LOCALE_LANGUAGE_KEY, language) } val country = locale.country - if (TextUtils.isEmpty(country)) { + if (country.isEmpty()) { remove(APPLICATION_LOCALE_COUNTRY_KEY) } else { putString(APPLICATION_LOCALE_COUNTRY_KEY, country) } val variant = locale.variant - if (TextUtils.isEmpty(variant)) { + if (variant.isEmpty()) { remove(APPLICATION_LOCALE_VARIANT_KEY) } else { putString(APPLICATION_LOCALE_VARIANT_KEY, variant) @@ -120,17 +118,17 @@ object VectorLocale { * @return the localized string */ private fun getString(context: Context, locale: Locale, resourceId: Int): String { - var result: String + val result: String if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { val config = Configuration(context.resources.configuration) config.setLocale(locale) - try { - result = context.createConfigurationContext(config).getText(resourceId).toString() + result = try { + context.createConfigurationContext(config).getText(resourceId).toString() } catch (e: Exception) { Timber.e(e, "## getString() failed") // use the default one - result = context.getString(resourceId) + context.getString(resourceId) } } else { val resources = context.resources @@ -177,8 +175,8 @@ object VectorLocale { supportedLocales.clear() - for (knownLocale in knownLocalesSet) { - supportedLocales.add(Locale(knownLocale.first, knownLocale.second)) + knownLocalesSet.mapTo(supportedLocales) { (language, country) -> + Locale(language, country) } // sort by human display names @@ -194,7 +192,7 @@ object VectorLocale { fun localeToLocalisedString(locale: Locale): String { var res = locale.getDisplayLanguage(locale) - if (!TextUtils.isEmpty(locale.getDisplayCountry(locale))) { + if (locale.getDisplayCountry(locale).isNotEmpty()) { res += " (" + locale.getDisplayCountry(locale) + ")" } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt index 5d08d7626d..8929b94771 100755 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt @@ -18,11 +18,9 @@ package im.vector.riotx.features.settings import android.content.Context -import android.database.Cursor import android.media.RingtoneManager import android.net.Uri import android.provider.MediaStore -import android.text.TextUtils import androidx.core.content.edit import androidx.preference.PreferenceManager import im.vector.riotx.R @@ -30,7 +28,6 @@ import im.vector.riotx.features.homeserver.ServerUrlsRepository import im.vector.riotx.features.themes.ThemeUtils import timber.log.Timber import java.io.File -import java.util.* import javax.inject.Inject class VectorPreferences @Inject constructor(private val context: Context) { @@ -173,7 +170,7 @@ class VectorPreferences @Inject constructor(private val context: Context) { private const val MEDIA_SAVING_FOREVER = 3 // some preferences keys must be kept after a logout - private val mKeysToKeepAfterLogout = Arrays.asList( + private val mKeysToKeepAfterLogout = listOf( SETTINGS_DEFAULT_MEDIA_COMPRESSION_KEY, SETTINGS_DEFAULT_MEDIA_SOURCE_KEY, SETTINGS_PLAY_SHUTTER_SOUND_KEY, @@ -409,7 +406,7 @@ class VectorPreferences @Inject constructor(private val context: Context) { val url = defaultPrefs.getString(SETTINGS_NOTIFICATION_RINGTONE_PREFERENCE_KEY, null) // the user selects "None" - if (TextUtils.equals(url, "")) { + if (url == "") { return null } @@ -441,29 +438,18 @@ class VectorPreferences @Inject constructor(private val context: Context) { fun getNotificationRingToneName(): String? { val toneUri = getNotificationRingTone() ?: return null - var name: String? = null - - var cursor: Cursor? = null - try { val proj = arrayOf(MediaStore.Audio.Media.DATA) - cursor = context.contentResolver.query(toneUri, proj, null, null, null) - val column_index = cursor!!.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA) - cursor.moveToFirst() - - val file = File(cursor.getString(column_index)) - name = file.name - - if (name!!.contains(".")) { - name = name.substring(0, name.lastIndexOf(".")) + return context.contentResolver.query(toneUri, proj, null, null, null)?.use { + val columnIndex = it.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA) + it.moveToFirst() + File(it.getString(columnIndex)).nameWithoutExtension } } catch (e: Exception) { Timber.e(e, "## getNotificationRingToneName() failed") - } finally { - cursor?.close() } - return name + return null } /** @@ -580,16 +566,13 @@ class VectorPreferences @Inject constructor(private val context: Context) { * @return the min last access time (in seconds) */ fun getMinMediasLastAccessTime(): Long { - val selection = getSelectedMediasSavingPeriod() - - when (selection) { - MEDIA_SAVING_3_DAYS -> return System.currentTimeMillis() / 1000 - 3 * 24 * 60 * 60 - MEDIA_SAVING_1_WEEK -> return System.currentTimeMillis() / 1000 - 7 * 24 * 60 * 60 - MEDIA_SAVING_1_MONTH -> return System.currentTimeMillis() / 1000 - 30 * 24 * 60 * 60 - MEDIA_SAVING_FOREVER -> return 0 + return when (getSelectedMediasSavingPeriod()) { + MEDIA_SAVING_3_DAYS -> System.currentTimeMillis() / 1000 - 3 * 24 * 60 * 60 + MEDIA_SAVING_1_WEEK -> System.currentTimeMillis() / 1000 - 7 * 24 * 60 * 60 + MEDIA_SAVING_1_MONTH -> System.currentTimeMillis() / 1000 - 30 * 24 * 60 * 60 + MEDIA_SAVING_FOREVER -> 0 + else -> 0 } - - return 0 } /** @@ -599,15 +582,13 @@ class VectorPreferences @Inject constructor(private val context: Context) { * @return the selected period */ fun getSelectedMediasSavingPeriodString(): String { - val selection = getSelectedMediasSavingPeriod() - - when (selection) { - MEDIA_SAVING_3_DAYS -> return context.getString(R.string.media_saving_period_3_days) - MEDIA_SAVING_1_WEEK -> return context.getString(R.string.media_saving_period_1_week) - MEDIA_SAVING_1_MONTH -> return context.getString(R.string.media_saving_period_1_month) - MEDIA_SAVING_FOREVER -> return context.getString(R.string.media_saving_period_forever) + return when (getSelectedMediasSavingPeriod()) { + MEDIA_SAVING_3_DAYS -> context.getString(R.string.media_saving_period_3_days) + MEDIA_SAVING_1_WEEK -> context.getString(R.string.media_saving_period_1_week) + MEDIA_SAVING_1_MONTH -> context.getString(R.string.media_saving_period_1_month) + MEDIA_SAVING_FOREVER -> context.getString(R.string.media_saving_period_forever) + else -> "?" } - return "?" } /** diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsPreferencesFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsPreferencesFragment.kt index 8c09faf73a..ab81e6937e 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsPreferencesFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsPreferencesFragment.kt @@ -19,7 +19,6 @@ package im.vector.riotx.features.settings import android.app.Activity import android.content.Context import android.content.Intent -import android.text.TextUtils import android.widget.CheckedTextView import android.widget.LinearLayout import androidx.appcompat.app.AlertDialog @@ -194,7 +193,7 @@ class VectorSettingsPreferencesFragment : VectorSettingsBaseFragment() { val v = linearLayout.getChildAt(i) if (v is CheckedTextView) { - v.isChecked = TextUtils.equals(v.text, scaleText) + v.isChecked = v.text == scaleText v.setOnClickListener { dialog.dismiss() diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt index dfc7004554..c89b8435f9 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt @@ -21,7 +21,6 @@ import android.app.Activity import android.content.DialogInterface import android.content.Intent import android.graphics.Typeface -import android.text.TextUtils import android.view.KeyEvent import android.widget.Button import android.widget.EditText @@ -69,7 +68,7 @@ class VectorSettingsSecurityPrivacyFragment : VectorSettingsBaseFragment() { private var mAccountPassword: String = "" // devices: device IDs and device names - private var mDevicesNameList: List = ArrayList() + private val mDevicesNameList: MutableList = mutableListOf() private var mMyDeviceInfo: DeviceInfo? = null @@ -308,7 +307,7 @@ class VectorSettingsSecurityPrivacyFragment : VectorSettingsBaseFragment() { passPhraseEditText.addTextChangedListener(object : SimpleTextWatcher() { override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { - importButton.isEnabled = !TextUtils.isEmpty(passPhraseEditText.text) + importButton.isEnabled = !passPhraseEditText.text.isNullOrEmpty() } }) @@ -393,20 +392,20 @@ class VectorSettingsSecurityPrivacyFragment : VectorSettingsBaseFragment() { } // crypto section: device ID - if (!TextUtils.isEmpty(deviceId)) { + if (!deviceId.isNullOrEmpty()) { cryptoInfoDeviceIdPreference.summary = deviceId cryptoInfoDeviceIdPreference.setOnPreferenceClickListener { - activity?.let { copyToClipboard(it, deviceId!!) } + activity?.let { copyToClipboard(it, deviceId) } true } } // crypto section: device key (fingerprint) - if (!TextUtils.isEmpty(deviceId) && !TextUtils.isEmpty(userId)) { + if (!deviceId.isNullOrEmpty() && userId.isNotEmpty()) { val deviceInfo = session.getDeviceInfo(userId, deviceId) - if (null != deviceInfo && !TextUtils.isEmpty(deviceInfo.fingerprint())) { + if (null != deviceInfo && !deviceInfo.fingerprint().isNullOrEmpty()) { cryptoInfoTextPreference.summary = deviceInfo.getFingerprintHumanReadable() cryptoInfoTextPreference.setOnPreferenceClickListener { @@ -446,7 +445,7 @@ class VectorSettingsSecurityPrivacyFragment : VectorSettingsBaseFragment() { * It can be any mobile device, as any browser. */ private fun refreshDevicesList() { - if (session.isCryptoEnabled() && !TextUtils.isEmpty(session.sessionParams.credentials.deviceId)) { + if (session.isCryptoEnabled() && !session.sessionParams.credentials.deviceId.isNullOrEmpty()) { // display a spinner while loading the devices list if (0 == mDevicesListSettingsCategory.preferenceCount) { activity?.let { @@ -502,7 +501,8 @@ class VectorSettingsSecurityPrivacyFragment : VectorSettingsBaseFragment() { if (isNewList) { var prefIndex = 0 - mDevicesNameList = aDeviceInfoList + mDevicesNameList.clear() + mDevicesNameList.addAll(aDeviceInfoList) // sort before display: most recent first mDevicesNameList.sortByLastSeen() @@ -570,7 +570,7 @@ class VectorSettingsSecurityPrivacyFragment : VectorSettingsBaseFragment() { // device name textView = layout.findViewById(R.id.device_name) - val displayName = if (TextUtils.isEmpty(aDeviceInfo.displayName)) LABEL_UNAVAILABLE_DATA else aDeviceInfo.displayName + val displayName = if (aDeviceInfo.displayName.isNullOrEmpty()) LABEL_UNAVAILABLE_DATA else aDeviceInfo.displayName textView.text = displayName // last seen info @@ -598,7 +598,7 @@ class VectorSettingsSecurityPrivacyFragment : VectorSettingsBaseFragment() { .setPositiveButton(R.string.rename) { _, _ -> displayDeviceRenameDialog(aDeviceInfo) } // disable the deletion for our own device - if (!TextUtils.equals(session.getMyDevice().deviceId, aDeviceInfo.deviceId)) { + if (session.getMyDevice().deviceId != aDeviceInfo.deviceId) { builder.setNegativeButton(R.string.delete) { _, _ -> deleteDevice(aDeviceInfo) } } @@ -645,13 +645,13 @@ class VectorSettingsSecurityPrivacyFragment : VectorSettingsBaseFragment() { for (i in 0 until count) { val pref = mDevicesListSettingsCategory.getPreference(i) - if (TextUtils.equals(aDeviceInfoToRename.deviceId, pref.title)) { + if (aDeviceInfoToRename.deviceId == pref.title) { pref.summary = newName } } // detect if the updated device is the current account one - if (TextUtils.equals(cryptoInfoDeviceIdPreference.summary, aDeviceInfoToRename.deviceId)) { + if (cryptoInfoDeviceIdPreference.summary == aDeviceInfoToRename.deviceId) { cryptoInfoDeviceNamePreference.summary = newName } @@ -716,7 +716,7 @@ class VectorSettingsSecurityPrivacyFragment : VectorSettingsBaseFragment() { * Show a dialog to ask for user password, or use a previously entered password. */ private fun maybeShowDeleteDeviceWithPasswordDialog(deviceId: String, authSession: String?) { - if (!TextUtils.isEmpty(mAccountPassword)) { + if (mAccountPassword.isNotEmpty()) { deleteDeviceWithPassword(deviceId, authSession, mAccountPassword) } else { activity?.let { @@ -729,7 +729,7 @@ class VectorSettingsSecurityPrivacyFragment : VectorSettingsBaseFragment() { .setTitle(R.string.devices_delete_dialog_title) .setView(layout) .setPositiveButton(R.string.devices_delete_submit_button_label, DialogInterface.OnClickListener { _, _ -> - if (TextUtils.isEmpty(passwordEditText.toString())) { + if (passwordEditText.toString().isEmpty()) { it.toast(R.string.error_empty_field_your_password) return@OnClickListener } From f7f97e2098577590430745f849499d6752a5b037 Mon Sep 17 00:00:00 2001 From: Dominic Fischer Date: Sat, 12 Oct 2019 16:04:47 +0100 Subject: [PATCH 134/197] Typos Signed-off-by: Dominic Fischer --- .../internal/crypto/DefaultCryptoService.kt | 1 - .../algorithms/megolm/MXMegolmEncryption.kt | 2 - .../features/settings/VectorPreferences.kt | 45 ------------------- 3 files changed, 48 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt index 853360d12f..599e5fef81 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt @@ -785,7 +785,6 @@ internal class DefaultCryptoService @Inject constructor( * * @param password the password * @param anIterationCount the encryption iteration count (0 means no encryption) - * @param callback the exported keys */ private suspend fun exportRoomKeys(password: String, anIterationCount: Int): ByteArray { return withContext(coroutineDispatchers.crypto) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt index 2f5e657376..77a8f2ec1c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt @@ -164,7 +164,6 @@ internal class MXMegolmEncryption( * * @param session the session info * @param devicesByUser the devices map - * @param callback the asynchronous callback */ private suspend fun shareUserDevicesKey(session: MXOutboundSessionInfo, devicesByUser: Map>) { @@ -272,7 +271,6 @@ internal class MXMegolmEncryption( * This method must be called in getDecryptingThreadHandler() thread. * * @param userIds the user ids whose devices must be checked. - * @param callback the asynchronous callback */ private suspend fun getDevicesInRoom(userIds: List): MXUsersDevicesMap { // We are happy to use a cached version here: we assume that if we already diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt index 5d08d7626d..4ad3f54e30 100755 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt @@ -215,8 +215,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Clear the preferences. - * - * @param context the context */ fun clearPreferences() { val keysToKeep = HashSet(mKeysToKeepAfterLogout) @@ -266,7 +264,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if we have already asked the user to disable battery optimisations on android >= M devices. * - * @param context the context * @return true if it was already requested */ fun didAskUserToIgnoreBatteryOptimizations(): Boolean { @@ -275,8 +272,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Mark as requested the question to disable battery optimisations. - * - * @param context the context */ fun setDidAskUserToIgnoreBatteryOptimizations() { defaultPrefs.edit { @@ -297,7 +292,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if the timestamp must be displayed in 12h format * - * @param context the context * @return true if the time must be displayed in 12h format */ fun displayTimeIn12hFormat(): Boolean { @@ -307,7 +301,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if the join and leave membership events should be shown in the messages list. * - * @param context the context * @return true if the join and leave membership events should be shown in the messages list */ fun showJoinLeaveMessages(): Boolean { @@ -317,7 +310,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if the avatar and display name events should be shown in the messages list. * - * @param context the context * @return true true if the avatar and display name events should be shown in the messages list. */ fun showAvatarDisplayNameChangeMessages(): Boolean { @@ -327,7 +319,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells the native camera to take a photo or record a video. * - * @param context the context * @return true to use the native camera app to record video or take photo. */ fun useNativeCamera(): Boolean { @@ -337,7 +328,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if the send voice feature is enabled. * - * @param context the context * @return true if the send voice feature is enabled. */ fun isSendVoiceFeatureEnabled(): Boolean { @@ -347,7 +337,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells which compression level to use by default * - * @param context the context * @return the selected compression level */ fun getSelectedDefaultMediaCompressionLevel(): Int { @@ -357,7 +346,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells which media source to use by default * - * @param context the context * @return the selected media source */ fun getSelectedDefaultMediaSource(): Int { @@ -367,7 +355,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells whether to use shutter sound. * - * @param context the context * @return true if shutter sound should play */ fun useShutterSound(): Boolean { @@ -377,7 +364,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Update the notification ringtone * - * @param context the context * @param uri the new notification ringtone, or null for no RingTone */ fun setNotificationRingTone(uri: Uri?) { @@ -402,7 +388,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Provides the selected notification ring tone * - * @param context the context * @return the selected ring tone or null for no RingTone */ fun getNotificationRingTone(): Uri? { @@ -435,7 +420,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Provide the notification ringtone filename * - * @param context the context * @return the filename or null if "None" is selected */ fun getNotificationRingToneName(): String? { @@ -469,7 +453,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Enable or disable the lazy loading * - * @param context the context * @param newValue true to enable lazy loading, false to disable it */ fun setUseLazyLoading(newValue: Boolean) { @@ -481,7 +464,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if the lazy loading is enabled * - * @param context the context * @return true if the lazy loading of room members is enabled */ fun useLazyLoading(): Boolean { @@ -491,7 +473,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * User explicitly refuses the lazy loading. * - * @param context the context */ fun setUserRefuseLazyLoading() { defaultPrefs.edit { @@ -502,7 +483,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if the user has explicitly refused the lazy loading * - * @param context the context * @return true if the user has explicitly refuse the lazy loading of room members */ fun hasUserRefusedLazyLoading(): Boolean { @@ -512,7 +492,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if the data save mode is enabled * - * @param context the context * @return true if the data save mode is enabled */ fun useDataSaveMode(): Boolean { @@ -522,7 +501,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if the conf calls must be done with Jitsi. * - * @param context the context * @return true if the conference call must be done with jitsi. */ fun useJitsiConfCall(): Boolean { @@ -532,7 +510,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if the application is started on boot * - * @param context the context * @return true if the application must be started on boot */ fun autoStartOnBoot(): Boolean { @@ -542,7 +519,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if the application is started on boot * - * @param context the context * @param value true to start the application on boot */ fun setAutoStartOnBoot(value: Boolean) { @@ -554,7 +530,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Provides the selected saving period. * - * @param context the context * @return the selected period */ fun getSelectedMediasSavingPeriod(): Int { @@ -564,7 +539,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Updates the selected saving period. * - * @param context the context * @param index the selected period index */ fun setSelectedMediasSavingPeriod(index: Int) { @@ -576,7 +550,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Provides the minimum last access time to keep a media file. * - * @param context the context * @return the min last access time (in seconds) */ fun getMinMediasLastAccessTime(): Long { @@ -595,7 +568,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Provides the selected saving period. * - * @param context the context * @return the selected period */ fun getSelectedMediasSavingPeriodString(): String { @@ -620,7 +592,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if the markdown is enabled * - * @param context the context * @return true if the markdown is enabled */ fun isMarkdownEnabled(): Boolean { @@ -630,7 +601,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Update the markdown enable status. * - * @param context the context * @param isEnabled true to enable the markdown */ fun setMarkdownEnabled(isEnabled: Boolean) { @@ -642,7 +612,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if the read receipts should be shown * - * @param context the context * @return true if the read receipts should be shown */ fun showReadReceipts(): Boolean { @@ -652,7 +621,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if the message timestamps must be always shown * - * @param context the context * @return true if the message timestamps must be always shown */ fun alwaysShowTimeStamps(): Boolean { @@ -662,7 +630,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if the typing notifications should be sent * - * @param context the context * @return true to send the typing notifs */ fun sendTypingNotifs(): Boolean { @@ -672,7 +639,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells of the missing notifications rooms must be displayed at left (home screen) * - * @param context the context * @return true to move the missed notifications to the left side */ fun pinMissedNotifications(): Boolean { @@ -682,7 +648,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells of the unread rooms must be displayed at left (home screen) * - * @param context the context * @return true to move the unread room to the left side */ fun pinUnreadMessages(): Boolean { @@ -692,7 +657,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if the phone must vibrate when mentioning * - * @param context the context * @return true */ fun vibrateWhenMentioning(): Boolean { @@ -702,7 +666,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if a dialog has been displayed to ask to use the analytics tracking (piwik, matomo, etc.). * - * @param context the context * @return true if a dialog has been displayed to ask to use the analytics tracking */ fun didAskToUseAnalytics(): Boolean { @@ -712,7 +675,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * To call if the user has been asked for analytics tracking. * - * @param context the context */ fun setDidAskToUseAnalytics() { defaultPrefs.edit { @@ -723,7 +685,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if the analytics tracking is authorized (piwik, matomo, etc.). * - * @param context the context * @return true if the analytics tracking is authorized */ fun useAnalytics(): Boolean { @@ -733,7 +694,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Enable or disable the analytics tracking. * - * @param context the context * @param useAnalytics true to enable the analytics tracking */ fun setUseAnalytics(useAnalytics: Boolean) { @@ -745,7 +705,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if media should be previewed before sending * - * @param context the context * @return true to preview media */ fun previewMediaWhenSending(): Boolean { @@ -755,7 +714,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if message should be send by pressing enter on the soft keyboard * - * @param context the context * @return true to send message with enter */ fun sendMessageWithEnter(): Boolean { @@ -765,7 +723,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if the rage shake is used. * - * @param context the context * @return true if the rage shake is used */ fun useRageshake(): Boolean { @@ -775,7 +732,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Update the rage shake status. * - * @param context the context * @param isEnabled true to enable the rage shake */ fun setUseRageshake(isEnabled: Boolean) { @@ -787,7 +743,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if all the events must be displayed ie even the redacted events. * - * @param context the context * @return true to display all the events even the redacted ones. */ fun displayAllEvents(): Boolean { From e4d0e0b0bf73e5085fc971d6f788d14592a65326 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 15 Oct 2019 11:02:16 +0200 Subject: [PATCH 135/197] Update after Ganfra's review --- .../detail/timeline/factory/MessageItemFactory.kt | 13 +++++++++---- .../timeline/helper/MessageItemAttributesFactory.kt | 3 --- .../room/detail/timeline/item/AbsMessageItem.kt | 2 -- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 4619eb4c8e..0bb5c3a1d8 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -53,6 +53,7 @@ import javax.inject.Inject class MessageItemFactory @Inject constructor( private val colorProvider: ColorProvider, + private val dimensionConverter: DimensionConverter, private val timelineMediaSizeProvider: TimelineMediaSizeProvider, private val htmlRenderer: Lazy, private val stringProvider: StringProvider, @@ -243,7 +244,7 @@ class MessageItemFactory @Inject constructor( return MessageTextItem_() .apply { if (informationData.hasBeenEdited) { - val spannable = annotateWithEdited(linkifiedBody, callback, attributes.dimensionConverter, informationData) + val spannable = annotateWithEdited(linkifiedBody, callback, informationData) message(spannable) } else { message(linkifiedBody) @@ -260,7 +261,6 @@ class MessageItemFactory @Inject constructor( private fun annotateWithEdited(linkifiedBody: CharSequence, callback: TimelineEventController.Callback?, - dimensionConverter: DimensionConverter, informationData: MessageInformationData): SpannableStringBuilder { val spannable = SpannableStringBuilder() spannable.append(linkifiedBody) @@ -276,7 +276,12 @@ class MessageItemFactory @Inject constructor( Spanned.SPAN_INCLUSIVE_EXCLUSIVE) // Note: text size is set to 14sp - spannable.setSpan(AbsoluteSizeSpan(dimensionConverter.spToPx(13)), editStart, editEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + spannable.setSpan( + AbsoluteSizeSpan(dimensionConverter.spToPx(13)), + editStart, + editEnd, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + spannable.setSpan(object : ClickableSpan() { override fun onClick(widget: View?) { callback?.onEditedDecorationClicked(informationData) @@ -326,7 +331,7 @@ class MessageItemFactory @Inject constructor( return MessageTextItem_() .apply { if (informationData.hasBeenEdited) { - val spannable = annotateWithEdited(message, callback, attributes.dimensionConverter, informationData) + val spannable = annotateWithEdited(message, callback, informationData) message(spannable) } else { message(message) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt index dddf507453..0e1229eeca 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt @@ -22,7 +22,6 @@ import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.riotx.EmojiCompatFontProvider import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.utils.DebouncedClickListener -import im.vector.riotx.core.utils.DimensionConverter import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.item.AbsMessageItem @@ -33,7 +32,6 @@ class MessageItemAttributesFactory @Inject constructor( private val avatarRenderer: AvatarRenderer, private val colorProvider: ColorProvider, private val avatarSizeProvider: AvatarSizeProvider, - private val dimensionConverter: DimensionConverter, private val emojiCompatFontProvider: EmojiCompatFontProvider) { fun create(messageContent: MessageContent?, @@ -44,7 +42,6 @@ class MessageItemAttributesFactory @Inject constructor( informationData = informationData, avatarRenderer = avatarRenderer, colorProvider = colorProvider, - dimensionConverter = dimensionConverter, itemLongClickListener = View.OnLongClickListener { view -> callback?.onEventLongClicked(informationData, messageContent, view) ?: false }, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt index 461028c3d0..bddee50861 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -29,7 +29,6 @@ import im.vector.riotx.R import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.ui.views.ReadMarkerView import im.vector.riotx.core.utils.DebouncedClickListener -import im.vector.riotx.core.utils.DimensionConverter import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.reactions.widget.ReactionButton @@ -173,7 +172,6 @@ abstract class AbsMessageItem : BaseEventItem() { val informationData: MessageInformationData, val avatarRenderer: AvatarRenderer, val colorProvider: ColorProvider, - val dimensionConverter: DimensionConverter, val itemLongClickListener: View.OnLongClickListener? = null, val itemClickListener: View.OnClickListener? = null, val memberClickListener: View.OnClickListener? = null, From 3c3c6aeac63dd7946adb3dd439701e7747450138 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 15 Oct 2019 11:24:20 +0200 Subject: [PATCH 136/197] Removes the RoomList handling from a viewmodel as it doesn't have a ViewState and should be provided globally (IE, from application state) --- ...ctivityViewModel.kt => AppStateHandler.kt} | 67 ++++++++++--------- .../java/im/vector/riotx/VectorApplication.kt | 4 +- .../riotx/features/home/HomeActivity.kt | 2 - 3 files changed, 37 insertions(+), 36 deletions(-) rename vector/src/main/java/im/vector/riotx/{features/home/HomeActivityViewModel.kt => AppStateHandler.kt} (59%) diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/riotx/AppStateHandler.kt similarity index 59% rename from vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewModel.kt rename to vector/src/main/java/im/vector/riotx/AppStateHandler.kt index 16d3f42824..4124f3c779 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/AppStateHandler.kt @@ -14,56 +14,57 @@ * limitations under the License. */ -package im.vector.riotx.features.home +package im.vector.riotx +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.OnLifecycleEvent import arrow.core.Option -import com.airbnb.mvrx.ActivityViewModelContext -import com.airbnb.mvrx.MvRxState -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject -import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.rx.rx -import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.di.ActiveSessionHolder +import im.vector.riotx.features.home.HomeRoomListObservableStore import im.vector.riotx.features.home.group.ALL_COMMUNITIES_GROUP_ID import im.vector.riotx.features.home.group.SelectedGroupStore import io.reactivex.Observable +import io.reactivex.disposables.CompositeDisposable import io.reactivex.functions.BiFunction +import io.reactivex.rxkotlin.addTo import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.inject.Singleton -data class EmptyState(val isEmpty: Boolean = true) : MvRxState -class HomeActivityViewModel @AssistedInject constructor(@Assisted initialState: EmptyState, - private val session: Session, - private val selectedGroupStore: SelectedGroupStore, - private val homeRoomListStore: HomeRoomListObservableStore -) : VectorViewModel(initialState) { +/** + * This class handles the main room list at the moment. It will dispatch the results to the store. + * It requires to be added with ProcessLifecycleOwner.get().lifecycle.addObserver + */ +@Singleton +class AppStateHandler @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, + private val homeRoomListStore: HomeRoomListObservableStore, + private val selectedGroupStore: SelectedGroupStore) : LifecycleObserver { - @AssistedInject.Factory - interface Factory { - fun create(initialState: EmptyState): HomeActivityViewModel + private val compositeDisposable = CompositeDisposable() + + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + fun entersForeground() { + observeRoomsAndGroup() } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: EmptyState): HomeActivityViewModel? { - val homeActivity: HomeActivity = (viewModelContext as ActivityViewModelContext).activity() - return homeActivity.homeActivityViewModelFactory.create(state) - } + @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) + fun entersBackground() { + compositeDisposable.clear() } - init { - observeRoomAndGroup() - } - - private fun observeRoomAndGroup() { + private fun observeRoomsAndGroup() { Observable .combineLatest, Option, List>( - session.rx().liveRoomSummaries().throttleLast(300, TimeUnit.MILLISECONDS), + activeSessionHolder.getActiveSession() + .rx() + .liveRoomSummaries() + .throttleLast(300, TimeUnit.MILLISECONDS), selectedGroupStore.observe(), BiFunction { rooms, selectedGroupOption -> val selectedGroup = selectedGroupOption.orNull() @@ -83,7 +84,7 @@ class HomeActivityViewModel @AssistedInject constructor(@Assisted initialState: .filter { !it.isDirect } .filter { selectedGroup?.groupId == ALL_COMMUNITIES_GROUP_ID - || selectedGroup?.roomIds?.contains(it.roomId) ?: true + || selectedGroup?.roomIds?.contains(it.roomId) ?: true } filteredDirectRooms + filteredGroupRooms } @@ -91,6 +92,6 @@ class HomeActivityViewModel @AssistedInject constructor(@Assisted initialState: .subscribe { homeRoomListStore.post(it) } - .disposeOnClear() + .addTo(compositeDisposable) } } diff --git a/vector/src/main/java/im/vector/riotx/VectorApplication.kt b/vector/src/main/java/im/vector/riotx/VectorApplication.kt index 081d1c69aa..3878420cc7 100644 --- a/vector/src/main/java/im/vector/riotx/VectorApplication.kt +++ b/vector/src/main/java/im/vector/riotx/VectorApplication.kt @@ -74,6 +74,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration. @Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var versionProvider: VersionProvider @Inject lateinit var notificationUtils: NotificationUtils + @Inject lateinit var appStateHandler: AppStateHandler lateinit var vectorComponent: VectorComponent private var fontThreadHandler: Handler? = null @@ -134,7 +135,8 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration. FcmHelper.onEnterBackground(appContext, vectorPreferences, activeSessionHolder) } }) - // This should be done as early as possible + ProcessLifecycleOwner.get().lifecycle.addObserver(appStateHandler) + //This should be done as early as possible initKnownEmojiHashSet(appContext) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt index 9071b51acf..1a9ca04d7c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt @@ -55,11 +55,9 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { object OpenGroup : Navigation() } - private val homeActivityViewModel: HomeActivityViewModel by viewModel() private lateinit var navigationViewModel: HomeNavigationViewModel @Inject lateinit var activeSessionHolder: ActiveSessionHolder - @Inject lateinit var homeActivityViewModelFactory: HomeActivityViewModel.Factory @Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler @Inject lateinit var pushManager: PushersManager @Inject lateinit var notificationDrawerManager: NotificationDrawerManager From 6cd04525aa3dff4b3624aa13f02a4711a7148e39 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 15 Oct 2019 11:37:22 +0200 Subject: [PATCH 137/197] Clean after Benoit's review --- vector/build.gradle | 1 - vector/src/main/AndroidManifest.xml | 4 ---- .../src/main/assets/open_source_licenses.html | 10 ++++---- .../riotx/core/utils/KeyboardStateUtils.kt | 17 +++++++++++++ .../features/attachments/AttachmentsHelper.kt | 5 +++- .../features/attachments/ContactAttachment.kt | 16 +++++++++++++ .../features/share/IncomingShareActivity.kt | 24 +++++++++++++++---- 7 files changed, 62 insertions(+), 15 deletions(-) diff --git a/vector/build.gradle b/vector/build.gradle index 7273cb6123..5ac7ac257c 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -316,7 +316,6 @@ dependencies { implementation 'me.leolin:ShortcutBadger:1.1.22@aar' // File picker - implementation 'com.github.jaiselrahman:FilePicker:1.2.2' implementation 'com.kbeanie:multipicker:1.6@aar' // DI diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index c56fc02eda..6c0f846997 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -85,16 +85,12 @@ android:screenOrientation="portrait"> - - - - diff --git a/vector/src/main/assets/open_source_licenses.html b/vector/src/main/assets/open_source_licenses.html index 837f63efe9..3cd500138a 100755 --- a/vector/src/main/assets/open_source_licenses.html +++ b/vector/src/main/assets/open_source_licenses.html @@ -339,11 +339,6 @@ SOFTWARE.
Copyright 2014 Leo Lin -
  • - FilePicker -
    - Copyright (c) 2018, Jaisel Rahman -
  • diff-match-patch
    @@ -359,6 +354,11 @@ SOFTWARE.
    Copyright 2017 Gabriel Ittner.
  • +
  • + Android-multipicker-library +
    + Copyright 2018 Kumar Bibek +
  •  Apache License
    diff --git a/vector/src/main/java/im/vector/riotx/core/utils/KeyboardStateUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/KeyboardStateUtils.kt
    index d1db11c21d..c525d588ae 100644
    --- a/vector/src/main/java/im/vector/riotx/core/utils/KeyboardStateUtils.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/utils/KeyboardStateUtils.kt
    @@ -1,3 +1,19 @@
    +/*
    + * Copyright 2019 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.riotx.core.utils
     
     import android.app.Activity
    @@ -11,6 +27,7 @@ class KeyboardStateUtils(activity: Activity) : ViewTreeObserver.OnGlobalLayoutLi
             it.viewTreeObserver.addOnGlobalLayoutListener(this)
         }
         var isKeyboardShowing: Boolean = false
    +        private set
     
         override fun onGlobalLayout() {
             val rect = Rect()
    diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt
    index dd0d6cd79c..1e2d59a4c3 100644
    --- a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt
    @@ -22,6 +22,7 @@ import androidx.fragment.app.Fragment
     import com.kbeanie.multipicker.api.Picker.*
     import com.kbeanie.multipicker.core.PickerManager
     import com.kbeanie.multipicker.utils.IntentUtils
    +import im.vector.matrix.android.BuildConfig
     import im.vector.matrix.android.api.session.content.ContentAttachmentData
     import im.vector.riotx.core.platform.Restorable
     import timber.log.Timber
    @@ -47,7 +48,9 @@ class AttachmentsHelper private constructor(private val pickerManagerFactory: Pi
     
         interface Callback {
             fun onContactAttachmentReady(contactAttachment: ContactAttachment) {
    -            Timber.v("On contact attachment ready: $contactAttachment")
    +            if (BuildConfig.LOG_PRIVATE_DATA) {
    +                Timber.v("On contact attachment ready: $contactAttachment")
    +            }
             }
     
             fun onContentAttachmentsReady(attachments: List)
    diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/ContactAttachment.kt b/vector/src/main/java/im/vector/riotx/features/attachments/ContactAttachment.kt
    index 51c60ee4f6..90340e2ef6 100644
    --- a/vector/src/main/java/im/vector/riotx/features/attachments/ContactAttachment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/attachments/ContactAttachment.kt
    @@ -1,3 +1,19 @@
    +/*
    + * Copyright 2019 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.riotx.features.attachments
     
     /**
    diff --git a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt
    index 73f20cf201..3f50ada057 100644
    --- a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt
    @@ -1,3 +1,19 @@
    +/*
    + * Copyright 2019 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.riotx.features.share
     
     import android.content.ClipDescription
    @@ -53,10 +69,10 @@ class IncomingShareActivity :
                     isShareManaged = handleTextShare(intent)
                 }
                 if (!isShareManaged) {
    -                cantManageShare()
    +                cannottManageShare()
                 }
             } else {
    -            cantManageShare()
    +            cannottManageShare()
             }
         }
     
    @@ -67,10 +83,10 @@ class IncomingShareActivity :
         }
     
         override fun onAttachmentsProcessFailed() {
    -        cantManageShare()
    +        cannottManageShare()
         }
     
    -    private fun cantManageShare() {
    +    private fun cannottManageShare() {
             Toast.makeText(this, R.string.error_handling_incoming_share, Toast.LENGTH_LONG).show()
             finish()
         }
    
    From 203da0f37ebbeb6e576b1a8a2ea11d8bf37c0639 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Tue, 15 Oct 2019 16:48:31 +0200
    Subject: [PATCH 138/197] Mark all as read: not for all Room list and look if
     there is unread rooms
    
    ---
     .../home/room/list/RoomListFragment.kt        | 22 +++++++++++++++++++
     .../home/room/list/RoomListViewModel.kt       |  1 -
     .../home/room/list/RoomListViewState.kt       |  8 +++++++
     3 files changed, 30 insertions(+), 1 deletion(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt
    index 75bf15efa7..6665500676 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt
    @@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.list
     
     import android.os.Bundle
     import android.os.Parcelable
    +import android.view.Menu
     import android.view.MenuItem
     import androidx.annotation.StringRes
     import androidx.core.content.ContextCompat
    @@ -79,6 +80,8 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
             injector.inject(this)
         }
     
    +    private var hasUnreadRooms = false
    +
         override fun getMenuRes() = R.menu.room_list
     
         override fun onOptionsItemSelected(item: MenuItem): Boolean {
    @@ -92,6 +95,11 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
             return super.onOptionsItemSelected(item)
         }
     
    +    override fun onPrepareOptionsMenu(menu: Menu) {
    +        menu.findItem(R.id.menu_home_mark_all_as_read).isVisible = hasUnreadRooms
    +        super.onPrepareOptionsMenu(menu)
    +    }
    +
         override fun onActivityCreated(savedInstanceState: Bundle?) {
             super.onActivityCreated(savedInstanceState)
             setupCreateRoomButton()
    @@ -194,6 +202,20 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
                 is Fail       -> renderFailure(state.asyncFilteredRooms.error)
             }
             roomController.update(state)
    +
    +        // Mark all as read menu
    +        when (roomListParams.displayMode) {
    +            DisplayMode.HOME,
    +            DisplayMode.PEOPLE,
    +            DisplayMode.ROOMS -> {
    +                val newValue = state.hasUnread
    +                if (hasUnreadRooms != newValue) {
    +                    hasUnreadRooms = newValue
    +                    requireActivity().invalidateOptionsMenu()
    +                }
    +            }
    +            else              -> Unit
    +        }
         }
     
         private fun renderSuccess(state: RoomListViewState) {
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt
    index 292e5405c4..695ab53812 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt
    @@ -203,7 +203,6 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room
                     ?.let { session.markAllAsRead(it, object : MatrixCallback {}) }
         }
     
    -
         private fun buildRoomSummaries(rooms: List): RoomSummaries {
             val invites = ArrayList()
             val favourites = ArrayList()
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewState.kt
    index 2f388b60d8..505554a8fb 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewState.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewState.kt
    @@ -20,6 +20,7 @@ import androidx.annotation.StringRes
     import com.airbnb.mvrx.Async
     import com.airbnb.mvrx.MvRxState
     import com.airbnb.mvrx.Uninitialized
    +import im.vector.matrix.android.api.session.room.model.Membership
     import im.vector.matrix.android.api.session.room.model.RoomSummary
     import im.vector.riotx.R
     
    @@ -67,6 +68,13 @@ data class RoomListViewState(
                 RoomCategory.SERVER_NOTICE -> copy(isServerNoticeRoomsExpanded = !isServerNoticeRoomsExpanded)
             }
         }
    +
    +    val hasUnread: Boolean
    +        get() = asyncFilteredRooms.invoke()
    +                ?.flatMap { it.value }
    +                ?.filter { it.membership == Membership.JOIN }
    +                ?.any { it.hasUnreadMessages }
    +                ?: false
     }
     
     typealias RoomSummaries = LinkedHashMap>
    
    From a1a71e2f1d75f9b4b1379a9caf86d3d8f4b4808a Mon Sep 17 00:00:00 2001
    From: ganfra 
    Date: Wed, 16 Oct 2019 10:04:11 +0200
    Subject: [PATCH 139/197] App state: fix session
    
    ---
     .../riotx/ActiveSessionObservableStore.kt      | 10 ++++++++++
     .../java/im/vector/riotx/AppStateHandler.kt    | 18 +++++++++++-------
     .../riotx/core/di/ActiveSessionHolder.kt       |  5 +++++
     3 files changed, 26 insertions(+), 7 deletions(-)
     create mode 100644 vector/src/main/java/im/vector/riotx/ActiveSessionObservableStore.kt
    
    diff --git a/vector/src/main/java/im/vector/riotx/ActiveSessionObservableStore.kt b/vector/src/main/java/im/vector/riotx/ActiveSessionObservableStore.kt
    new file mode 100644
    index 0000000000..c10a7f700c
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/ActiveSessionObservableStore.kt
    @@ -0,0 +1,10 @@
    +package im.vector.riotx
    +
    +import arrow.core.Option
    +import im.vector.matrix.android.api.session.Session
    +import im.vector.riotx.core.utils.RxStore
    +import javax.inject.Inject
    +import javax.inject.Singleton
    +
    +@Singleton
    +class ActiveSessionObservableStore @Inject constructor() : RxStore>()
    diff --git a/vector/src/main/java/im/vector/riotx/AppStateHandler.kt b/vector/src/main/java/im/vector/riotx/AppStateHandler.kt
    index 4124f3c779..a03e8e0948 100644
    --- a/vector/src/main/java/im/vector/riotx/AppStateHandler.kt
    +++ b/vector/src/main/java/im/vector/riotx/AppStateHandler.kt
    @@ -23,26 +23,27 @@ import arrow.core.Option
     import im.vector.matrix.android.api.session.group.model.GroupSummary
     import im.vector.matrix.android.api.session.room.model.RoomSummary
     import im.vector.matrix.rx.rx
    -import im.vector.riotx.core.di.ActiveSessionHolder
     import im.vector.riotx.features.home.HomeRoomListObservableStore
     import im.vector.riotx.features.home.group.ALL_COMMUNITIES_GROUP_ID
     import im.vector.riotx.features.home.group.SelectedGroupStore
     import io.reactivex.Observable
    +import io.reactivex.android.schedulers.AndroidSchedulers
     import io.reactivex.disposables.CompositeDisposable
     import io.reactivex.functions.BiFunction
     import io.reactivex.rxkotlin.addTo
    +import io.reactivex.schedulers.Schedulers
     import java.util.concurrent.TimeUnit
     import javax.inject.Inject
     import javax.inject.Singleton
     
     
     /**
    - * This class handles the main room list at the moment. It will dispatch the results to the store.
    - * It requires to be added with ProcessLifecycleOwner.get().lifecycle.addObserver
    + * This class handles the global app state. At the moment, it only manages room list.
    + * It requires to be added to ProcessLifecycleOwner.get().lifecycle
      */
     @Singleton
     class AppStateHandler @Inject constructor(
    -        private val activeSessionHolder: ActiveSessionHolder,
    +        private val sessionObservableStore: ActiveSessionObservableStore,
             private val homeRoomListStore: HomeRoomListObservableStore,
             private val selectedGroupStore: SelectedGroupStore) : LifecycleObserver {
     
    @@ -61,9 +62,12 @@ class AppStateHandler @Inject constructor(
         private fun observeRoomsAndGroup() {
             Observable
                     .combineLatest, Option, List>(
    -                        activeSessionHolder.getActiveSession()
    -                                .rx()
    -                                .liveRoomSummaries()
    +                        sessionObservableStore.observe()
    +                                .observeOn(AndroidSchedulers.mainThread())
    +                                .switchMap {
    +                                    it.orNull()?.rx()?.liveRoomSummaries()
    +                                    ?: Observable.just(emptyList())
    +                                }
                                     .throttleLast(300, TimeUnit.MILLISECONDS),
                             selectedGroupStore.observe(),
                             BiFunction { rooms, selectedGroupOption ->
    diff --git a/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt b/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt
    index 60255dbbdd..da3c041a1c 100644
    --- a/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt
    @@ -16,8 +16,10 @@
     
     package im.vector.riotx.core.di
     
    +import arrow.core.Option
     import im.vector.matrix.android.api.auth.Authenticator
     import im.vector.matrix.android.api.session.Session
    +import im.vector.riotx.ActiveSessionObservableStore
     import im.vector.riotx.features.crypto.keysrequest.KeyRequestHandler
     import im.vector.riotx.features.crypto.verification.IncomingVerificationRequestHandler
     import java.util.concurrent.atomic.AtomicReference
    @@ -26,6 +28,7 @@ import javax.inject.Singleton
     
     @Singleton
     class ActiveSessionHolder @Inject constructor(private val authenticator: Authenticator,
    +                                              private val sessionObservableStore: ActiveSessionObservableStore,
                                                   private val keyRequestHandler: KeyRequestHandler,
                                                   private val incomingVerificationRequestHandler: IncomingVerificationRequestHandler
     ) {
    @@ -34,12 +37,14 @@ class ActiveSessionHolder @Inject constructor(private val authenticator: Authent
     
         fun setActiveSession(session: Session) {
             activeSession.set(session)
    +        sessionObservableStore.post(Option.fromNullable(session))
             keyRequestHandler.start(session)
             incomingVerificationRequestHandler.start(session)
         }
     
         fun clearActiveSession() {
             activeSession.set(null)
    +        sessionObservableStore.post(Option.empty())
             keyRequestHandler.stop()
             incomingVerificationRequestHandler.stop()
         }
    
    From 05a069be045d03aebe8ed7d366196803d44781ab Mon Sep 17 00:00:00 2001
    From: ganfra 
    Date: Wed, 16 Oct 2019 10:04:33 +0200
    Subject: [PATCH 140/197] Attachments: fix themes for selection view
    
    ---
     .../attachments/AttachmentTypeSelectorView.kt | 31 ++++++++++---------
     .../drawable/bg_attachment_type_selector.xml  |  4 +--
     vector/src/main/res/values/colors_riotx.xml   | 10 ++++++
     vector/src/main/res/values/theme_black.xml    |  2 ++
     vector/src/main/res/values/theme_dark.xml     |  2 ++
     vector/src/main/res/values/theme_light.xml    |  2 ++
     6 files changed, 34 insertions(+), 17 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentTypeSelectorView.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentTypeSelectorView.kt
    index 6d71d88eb2..e27fa70aed 100644
    --- a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentTypeSelectorView.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentTypeSelectorView.kt
    @@ -124,8 +124,9 @@ class AttachmentTypeSelectorView(context: Context,
         }
     
         override fun dismiss() {
    -        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    -            animateWindowOutCircular(anchor, contentView)
    +        val capturedAnchor = anchor
    +        if (capturedAnchor != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    +            animateWindowOutCircular(capturedAnchor, contentView)
             } else {
                 animateWindowOutTranslate(contentView)
             }
    @@ -142,13 +143,13 @@ class AttachmentTypeSelectorView(context: Context,
         }
     
         @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    -    private fun animateWindowInCircular(anchor: View?, contentView: View) {
    +    private fun animateWindowInCircular(anchor: View, contentView: View) {
             val coordinates = getClickCoordinates(anchor, contentView)
             val animator = ViewAnimationUtils.createCircularReveal(contentView,
    -                coordinates.first,
    -                coordinates.second,
    -                0f,
    -                max(contentView.width, contentView.height).toFloat())
    +                                                               coordinates.first,
    +                                                               coordinates.second,
    +                                                               0f,
    +                                                               max(contentView.width, contentView.height).toFloat())
             animator.duration = ANIMATION_DURATION.toLong()
             animator.start()
         }
    @@ -160,13 +161,13 @@ class AttachmentTypeSelectorView(context: Context,
         }
     
         @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    -    private fun animateWindowOutCircular(anchor: View?, contentView: View) {
    +    private fun animateWindowOutCircular(anchor: View, contentView: View) {
             val coordinates = getClickCoordinates(anchor, contentView)
             val animator = ViewAnimationUtils.createCircularReveal(getContentView(),
    -                coordinates.first,
    -                coordinates.second,
    -                max(getContentView().width, getContentView().height).toFloat(),
    -                0f)
    +                                                               coordinates.first,
    +                                                               coordinates.second,
    +                                                               max(getContentView().width, getContentView().height).toFloat(),
    +                                                               0f)
     
             animator.duration = ANIMATION_DURATION.toLong()
             animator.addListener(object : AnimatorListenerAdapter() {
    @@ -193,12 +194,12 @@ class AttachmentTypeSelectorView(context: Context,
             getContentView().startAnimation(animation)
         }
     
    -    private fun getClickCoordinates(anchor: View?, contentView: View): Pair {
    +    private fun getClickCoordinates(anchor: View, contentView: View): Pair {
             val anchorCoordinates = IntArray(2)
    -        anchor?.getLocationOnScreen(anchorCoordinates)
    +        anchor.getLocationOnScreen(anchorCoordinates)
             val contentCoordinates = IntArray(2)
             contentView.getLocationOnScreen(contentCoordinates)
    -        val x = anchorCoordinates[0] - contentCoordinates[0]
    +        val x = anchorCoordinates[0] - contentCoordinates[0] + anchor.width / 2
             val y = anchorCoordinates[1] - contentCoordinates[1]
             return Pair(x, y)
         }
    diff --git a/vector/src/main/res/drawable/bg_attachment_type_selector.xml b/vector/src/main/res/drawable/bg_attachment_type_selector.xml
    index 53c7c1f1c9..12787f911a 100644
    --- a/vector/src/main/res/drawable/bg_attachment_type_selector.xml
    +++ b/vector/src/main/res/drawable/bg_attachment_type_selector.xml
    @@ -2,7 +2,7 @@
     
         
             
    -            
    +            
                 
             
         
    @@ -13,7 +13,7 @@
             android:right="1dp"
             android:top="1dp">
             
    -            
    +            
                 
     
             
    diff --git a/vector/src/main/res/values/colors_riotx.xml b/vector/src/main/res/values/colors_riotx.xml
    index 0e51d46cc2..aec2a4c535 100644
    --- a/vector/src/main/res/values/colors_riotx.xml
    +++ b/vector/src/main/res/values/colors_riotx.xml
    @@ -147,6 +147,16 @@
         #BF000000
         #BF000000
     
    +    
    +    #FFFFFFFF
    +    #FF22262E
    +    #FF090A0C
    +
    +    
    +    #FFE9EDF1
    +    #FF22262E
    +    #FF090A0C
    +
         
         
         #FFF8E3
    diff --git a/vector/src/main/res/values/theme_black.xml b/vector/src/main/res/values/theme_black.xml
    index 129a2e9b1c..7398a4bcb7 100644
    --- a/vector/src/main/res/values/theme_black.xml
    +++ b/vector/src/main/res/values/theme_black.xml
    @@ -30,6 +30,8 @@
             @color/riotx_fab_label_bg_black
             @color/riotx_fab_label_color_black
             @color/riotx_touch_guard_bg_black
    +        @color/riotx_attachment_selector_background_black
    +        @color/riotx_attachment_selector_border_black
     
             
             @drawable/highlighted_message_background_black
    diff --git a/vector/src/main/res/values/theme_dark.xml b/vector/src/main/res/values/theme_dark.xml
    index 4143229293..f09cb0c874 100644
    --- a/vector/src/main/res/values/theme_dark.xml
    +++ b/vector/src/main/res/values/theme_dark.xml
    @@ -28,6 +28,8 @@
             @color/riotx_fab_label_bg_dark
             @color/riotx_fab_label_color_dark
             @color/riotx_touch_guard_bg_dark
    +        @color/riotx_attachment_selector_background_dark
    +        @color/riotx_attachment_selector_border_dark
     
             @color/riotx_keys_backup_banner_accent_color_dark
     
    diff --git a/vector/src/main/res/values/theme_light.xml b/vector/src/main/res/values/theme_light.xml
    index 8d51486e76..1da010b8ff 100644
    --- a/vector/src/main/res/values/theme_light.xml
    +++ b/vector/src/main/res/values/theme_light.xml
    @@ -29,6 +29,8 @@
             @color/riotx_fab_label_color_light
             @color/riotx_touch_guard_bg_light
             @color/riotx_keys_backup_banner_accent_color_light
    +        @color/riotx_attachment_selector_background_light
    +        @color/riotx_attachment_selector_border_light
     
             
             @drawable/highlighted_message_background_light
    
    From 9e436483de4960c8f332494a05c5499e2b3dc2e4 Mon Sep 17 00:00:00 2001
    From: ganfra 
    Date: Wed, 16 Oct 2019 10:39:42 +0200
    Subject: [PATCH 141/197] Use klint
    
    ---
     vector/src/main/java/im/vector/riotx/AppStateHandler.kt         | 2 --
     vector/src/main/java/im/vector/riotx/VectorApplication.kt       | 2 +-
     .../src/main/java/im/vector/riotx/features/home/HomeActivity.kt | 1 -
     3 files changed, 1 insertion(+), 4 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/AppStateHandler.kt b/vector/src/main/java/im/vector/riotx/AppStateHandler.kt
    index a03e8e0948..d0301e2c9f 100644
    --- a/vector/src/main/java/im/vector/riotx/AppStateHandler.kt
    +++ b/vector/src/main/java/im/vector/riotx/AppStateHandler.kt
    @@ -31,12 +31,10 @@ import io.reactivex.android.schedulers.AndroidSchedulers
     import io.reactivex.disposables.CompositeDisposable
     import io.reactivex.functions.BiFunction
     import io.reactivex.rxkotlin.addTo
    -import io.reactivex.schedulers.Schedulers
     import java.util.concurrent.TimeUnit
     import javax.inject.Inject
     import javax.inject.Singleton
     
    -
     /**
      * This class handles the global app state. At the moment, it only manages room list.
      * It requires to be added to ProcessLifecycleOwner.get().lifecycle
    diff --git a/vector/src/main/java/im/vector/riotx/VectorApplication.kt b/vector/src/main/java/im/vector/riotx/VectorApplication.kt
    index 3878420cc7..b1fd6a8485 100644
    --- a/vector/src/main/java/im/vector/riotx/VectorApplication.kt
    +++ b/vector/src/main/java/im/vector/riotx/VectorApplication.kt
    @@ -136,7 +136,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
                 }
             })
             ProcessLifecycleOwner.get().lifecycle.addObserver(appStateHandler)
    -        //This should be done as early as possible
    +        // This should be done as early as possible
             initKnownEmojiHashSet(appContext)
         }
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt
    index 1a9ca04d7c..d5a4412834 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt
    @@ -27,7 +27,6 @@ import androidx.core.view.isVisible
     import androidx.drawerlayout.widget.DrawerLayout
     import androidx.lifecycle.Observer
     import androidx.lifecycle.ViewModelProviders
    -import com.airbnb.mvrx.viewModel
     import im.vector.riotx.R
     import im.vector.riotx.core.di.ActiveSessionHolder
     import im.vector.riotx.core.di.ScreenComponent
    
    From 3986839801283abd9278360404e7739c8d6989ee Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 18 Oct 2019 14:25:19 +0200
    Subject: [PATCH 142/197] Inject userId
    
    ---
     .../room/model/relation/RelationService.kt     |  6 ++----
     .../internal/crypto/keysbackup/KeysBackup.kt   | 18 ++++++++----------
     .../room/relation/DefaultRelationService.kt    |  9 ++++-----
     .../relation/FindReactionEventForUndoTask.kt   | 11 ++++++-----
     .../room/relation/UpdateQuickReactionTask.kt   | 15 ++++++++-------
     .../home/room/detail/RoomDetailViewModel.kt    |  4 ++--
     6 files changed, 30 insertions(+), 33 deletions(-)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt
    index c6a58eeec1..14f730c2ba 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt
    @@ -60,11 +60,9 @@ interface RelationService {
          * Undo a reaction (emoji) to the targetedEvent.
          * @param reaction the reaction (preferably emoji)
          * @param targetEventId the id of the event being reacted
    -     * @param myUserId used to know if a reaction event was made by the user
          */
         fun undoReaction(reaction: String,
    -                     targetEventId: String,
    -                     myUserId: String) // : Cancelable
    +                     targetEventId: String) : Cancelable
     
         /**
          * Edit a text message body. Limited to "m.text" contentType
    @@ -92,7 +90,7 @@ interface RelationService {
                       compatibilityBodyText: String = "* $newBodyText"): Cancelable
     
         /**
    -     * Get's the edit history of the given event
    +     * Get the edit history of the given event
          */
         fun fetchEditHistory(eventId: String, callback: MatrixCallback>)
     
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt
    index a66d50dfad..51c3527042 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt
    @@ -50,6 +50,7 @@ import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrap
     import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
     import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
     import im.vector.matrix.android.internal.di.MoshiProvider
    +import im.vector.matrix.android.internal.di.UserId
     import im.vector.matrix.android.internal.extensions.foldToCallback
     import im.vector.matrix.android.internal.session.SessionScope
     import im.vector.matrix.android.internal.task.Task
    @@ -77,6 +78,7 @@ import kotlin.random.Random
     
     @SessionScope
     internal class KeysBackup @Inject constructor(
    +        @UserId private val userId: String,
             private val credentials: Credentials,
             private val cryptoStore: IMXCryptoStore,
             private val olmDevice: MXOlmDevice,
    @@ -375,8 +377,6 @@ internal class KeysBackup @Inject constructor(
          */
         @WorkerThread
         private fun getKeysBackupTrustBg(keysBackupVersion: KeysVersionResult): KeysBackupVersionTrust {
    -        val myUserId = credentials.userId
    -
             val keysBackupVersionTrust = KeysBackupVersionTrust()
             val authData = keysBackupVersion.getAuthDataAsMegolmBackupAuthData()
     
    @@ -388,7 +388,7 @@ internal class KeysBackup @Inject constructor(
                 return keysBackupVersionTrust
             }
     
    -        val mySigs = authData.signatures?.get(myUserId)
    +        val mySigs = authData.signatures?.get(userId)
             if (mySigs.isNullOrEmpty()) {
                 Timber.v("getKeysBackupTrust: Ignoring key backup because it lacks any signatures from this user")
                 return keysBackupVersionTrust
    @@ -403,7 +403,7 @@ internal class KeysBackup @Inject constructor(
                 }
     
                 if (deviceId != null) {
    -                val device = cryptoStore.getUserDevice(deviceId, myUserId)
    +                val device = cryptoStore.getUserDevice(deviceId, userId)
                     var isSignatureValid = false
     
                     if (device == null) {
    @@ -450,10 +450,8 @@ internal class KeysBackup @Inject constructor(
             } else {
                 GlobalScope.launch(coroutineDispatchers.main) {
                     val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) {
    -                    val myUserId = credentials.userId
    -
                         // Get current signatures, or create an empty set
    -                    val myUserSignatures = authData.signatures?.get(myUserId)?.toMutableMap()
    +                    val myUserSignatures = authData.signatures?.get(userId)?.toMutableMap()
                                 ?: HashMap()
     
                         if (trust) {
    @@ -462,7 +460,7 @@ internal class KeysBackup @Inject constructor(
     
                             val deviceSignatures = objectSigner.signObject(canonicalJson)
     
    -                        deviceSignatures[myUserId]?.forEach { entry ->
    +                        deviceSignatures[userId]?.forEach { entry ->
                                 myUserSignatures[entry.key] = entry.value
                             }
                         } else {
    @@ -478,7 +476,7 @@ internal class KeysBackup @Inject constructor(
                         val newMegolmBackupAuthData = authData.copy()
     
                         val newSignatures = newMegolmBackupAuthData.signatures!!.toMutableMap()
    -                    newSignatures[myUserId] = myUserSignatures
    +                    newSignatures[userId] = myUserSignatures
     
                         newMegolmBackupAuthData.signatures = newSignatures
     
    @@ -1411,5 +1409,5 @@ internal class KeysBackup @Inject constructor(
      * DEBUG INFO
      * ========================================================================================== */
     
    -    override fun toString() = "KeysBackup for ${credentials.userId}"
    +    override fun toString() = "KeysBackup for $userId"
     }
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt
    index 6abc7ed60e..9f6500ce51 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt
    @@ -75,13 +75,13 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
             return CancelableWork(context, sendRelationWork.id)
         }
     
    -    override fun undoReaction(reaction: String, targetEventId: String, myUserId: String)/*: Cancelable*/ {
    +    override fun undoReaction(reaction: String, targetEventId: String): Cancelable {
             val params = FindReactionEventForUndoTask.Params(
                     roomId,
                     targetEventId,
    -                reaction,
    -                myUserId
    +                reaction
             )
    +        // TODO We should avoid using MatrixCallback internally
             val callback = object : MatrixCallback {
                 override fun onSuccess(data: FindReactionEventForUndoTask.Result) {
                     if (data.redactEventId == null) {
    @@ -89,7 +89,6 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
                         // TODO?
                     }
                     data.redactEventId?.let { toRedact ->
    -
                         val redactEvent = eventFactory.createRedactEvent(roomId, toRedact, null).also {
                             saveLocalEcho(it)
                         }
    @@ -99,7 +98,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
                     }
                 }
             }
    -        findReactionEventForUndoTask
    +        return findReactionEventForUndoTask
                     .configureWith(params) {
                         this.retryCount = Int.MAX_VALUE
                         this.callback = callback
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FindReactionEventForUndoTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FindReactionEventForUndoTask.kt
    index 997ed18492..d1abf93c21 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FindReactionEventForUndoTask.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FindReactionEventForUndoTask.kt
    @@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryE
     import im.vector.matrix.android.internal.database.model.EventEntity
     import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntityFields
     import im.vector.matrix.android.internal.database.query.where
    +import im.vector.matrix.android.internal.di.UserId
     import im.vector.matrix.android.internal.task.Task
     import io.realm.Realm
     import javax.inject.Inject
    @@ -29,8 +30,7 @@ internal interface FindReactionEventForUndoTask : Task
    -            getReactionToRedact(realm, params.reaction, params.eventId, params.myUserId)?.eventId
    +            getReactionToRedact(realm, params.reaction, params.eventId)?.eventId
             }
             return FindReactionEventForUndoTask.Result(eventId)
         }
     
    -    private fun getReactionToRedact(realm: Realm, reaction: String, eventId: String, userId: String): EventEntity? {
    +    private fun getReactionToRedact(realm: Realm, reaction: String, eventId: String): EventEntity? {
             val summary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
             if (summary != null) {
                 summary.reactionsSummary.where()
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/UpdateQuickReactionTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/UpdateQuickReactionTask.kt
    index e4b71c0d42..6ec316e9a4 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/UpdateQuickReactionTask.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/UpdateQuickReactionTask.kt
    @@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryE
     import im.vector.matrix.android.internal.database.model.EventEntity
     import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntityFields
     import im.vector.matrix.android.internal.database.query.where
    +import im.vector.matrix.android.internal.di.UserId
     import im.vector.matrix.android.internal.task.Task
     import io.realm.Realm
     import javax.inject.Inject
    @@ -30,8 +31,7 @@ internal interface UpdateQuickReactionTask : Task?>? = null
             monarchy.doWithRealm { realm ->
    -            res = updateQuickReaction(realm, params.reaction, params.oppositeReaction, params.eventId, params.myUserId)
    +            res = updateQuickReaction(realm, params.reaction, params.oppositeReaction, params.eventId)
             }
             return UpdateQuickReactionTask.Result(res?.first, res?.second ?: emptyList())
         }
     
    -    private fun updateQuickReaction(realm: Realm, reaction: String, oppositeReaction: String, eventId: String, myUserId: String): Pair?> {
    +    private fun updateQuickReaction(realm: Realm, reaction: String, oppositeReaction: String, eventId: String): Pair?> {
             // the emoji reaction has been selected, we need to check if we have reacted it or not
             val existingSummary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
                     ?: return Pair(reaction, null)
    @@ -68,7 +69,7 @@ internal class DefaultUpdateQuickReactionTask @Inject constructor(private val mo
                 val toRedact = aggregationForOppositeReaction?.sourceEvents?.mapNotNull {
                     // find source event
                     val entity = EventEntity.where(realm, it).findFirst()
    -                if (entity?.sender == myUserId) entity.eventId else null
    +                if (entity?.sender == userId) entity.eventId else null
                 }
                 return Pair(reaction, toRedact)
             } else {
    @@ -77,7 +78,7 @@ internal class DefaultUpdateQuickReactionTask @Inject constructor(private val mo
                 val toRedact = aggregationForReaction.sourceEvents.mapNotNull {
                     // find source event
                     val entity = EventEntity.where(realm, it).findFirst()
    -                if (entity?.sender == myUserId) entity.eventId else null
    +                if (entity?.sender == userId) entity.eventId else null
                 }
                 return Pair(null, toRedact)
             }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    index 7db9fc41d5..9eb11d2019 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    @@ -454,14 +454,14 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
         }
     
         private fun handleUndoReact(action: RoomDetailActions.UndoReaction) {
    -        room.undoReaction(action.key, action.targetEventId, session.myUserId)
    +        room.undoReaction(action.key, action.targetEventId)
         }
     
         private fun handleUpdateQuickReaction(action: RoomDetailActions.UpdateQuickReactAction) {
             if (action.add) {
                 room.sendReaction(action.selectedReaction, action.targetEventId)
             } else {
    -            room.undoReaction(action.selectedReaction, action.targetEventId, session.myUserId)
    +            room.undoReaction(action.selectedReaction, action.targetEventId)
             }
         }
     
    
    From eff04be2473f1daa20efc64bab93a17cf8547275 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 18 Oct 2019 14:26:24 +0200
    Subject: [PATCH 143/197] Change order of class (no effect)
    
    ---
     .../vector/riotx/features/home/room/detail/RoomDetailActions.kt | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt
    index d42af3e50a..ac1e1df51a 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt
    @@ -31,8 +31,8 @@ sealed class RoomDetailActions {
         data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailActions()
         data class LoadMoreTimelineEvents(val direction: Timeline.Direction) : RoomDetailActions()
         data class SendReaction(val reaction: String, val targetEventId: String) : RoomDetailActions()
    -    data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailActions()
         data class UndoReaction(val targetEventId: String, val key: String, val reason: String? = "") : RoomDetailActions()
    +    data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailActions()
         data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailActions()
         data class NavigateToEvent(val eventId: String, val highlight: Boolean) : RoomDetailActions()
         data class SetReadMarkerAction(val eventId: String) : RoomDetailActions()
    
    From be94b2f90ae66b2e2e377aa203dc7721e40530fb Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 18 Oct 2019 14:28:12 +0200
    Subject: [PATCH 144/197] Change order of parameters (no effect)
    
    ---
     .../riotx/features/home/room/detail/RoomDetailActions.kt      | 2 +-
     .../riotx/features/home/room/detail/RoomDetailFragment.kt     | 4 ++--
     2 files changed, 3 insertions(+), 3 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt
    index ac1e1df51a..91614d5d74 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt
    @@ -30,7 +30,7 @@ sealed class RoomDetailActions {
         data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailActions()
         data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailActions()
         data class LoadMoreTimelineEvents(val direction: Timeline.Direction) : RoomDetailActions()
    -    data class SendReaction(val reaction: String, val targetEventId: String) : RoomDetailActions()
    +    data class SendReaction(val targetEventId: String, val reaction: String) : RoomDetailActions()
         data class UndoReaction(val targetEventId: String, val key: String, val reason: String? = "") : RoomDetailActions()
         data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailActions()
         data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailActions()
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    index 6907af9ff6..f09fbff09f 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    @@ -420,7 +420,7 @@ class RoomDetailFragment :
                         val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID) ?: return
                         val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT) ?: return
                         // TODO check if already reacted with that?
    -                    roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId))
    +                    roomDetailViewModel.process(RoomDetailActions.SendReaction(eventId, reaction))
                     }
                 }
             }
    @@ -909,7 +909,7 @@ class RoomDetailFragment :
         override fun onClickOnReactionPill(informationData: MessageInformationData, reaction: String, on: Boolean) {
             if (on) {
                 // we should test the current real state of reaction on this event
    -            roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, informationData.eventId))
    +            roomDetailViewModel.process(RoomDetailActions.SendReaction(informationData.eventId, reaction))
             } else {
                 // I need to redact a reaction
                 roomDetailViewModel.process(RoomDetailActions.UndoReaction(informationData.eventId, reaction))
    
    From 8078c39d6ed53d30b11e3de2c3588efc8dab65ab Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 18 Oct 2019 14:29:32 +0200
    Subject: [PATCH 145/197] Rename parameter
    
    ---
     .../vector/riotx/features/home/room/detail/RoomDetailActions.kt | 2 +-
     .../riotx/features/home/room/detail/RoomDetailViewModel.kt      | 2 +-
     2 files changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt
    index 91614d5d74..d032182994 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt
    @@ -31,7 +31,7 @@ sealed class RoomDetailActions {
         data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailActions()
         data class LoadMoreTimelineEvents(val direction: Timeline.Direction) : RoomDetailActions()
         data class SendReaction(val targetEventId: String, val reaction: String) : RoomDetailActions()
    -    data class UndoReaction(val targetEventId: String, val key: String, val reason: String? = "") : RoomDetailActions()
    +    data class UndoReaction(val targetEventId: String, val reaction: String, val reason: String? = "") : RoomDetailActions()
         data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailActions()
         data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailActions()
         data class NavigateToEvent(val eventId: String, val highlight: Boolean) : RoomDetailActions()
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    index 9eb11d2019..58cfb97b87 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    @@ -454,7 +454,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
         }
     
         private fun handleUndoReact(action: RoomDetailActions.UndoReaction) {
    -        room.undoReaction(action.key, action.targetEventId)
    +        room.undoReaction(action.reaction, action.targetEventId)
         }
     
         private fun handleUpdateQuickReaction(action: RoomDetailActions.UpdateQuickReactAction) {
    
    From 17636019e045cdc820d24a977e02b219df72b32d Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 18 Oct 2019 14:32:34 +0200
    Subject: [PATCH 146/197] Change order of parameters
    
    ---
     .../session/room/model/relation/RelationService.kt   | 12 ++++++------
     .../session/room/relation/DefaultRelationService.kt  |  4 ++--
     .../android/api/pushrules/PushrulesConditionTest.kt  |  2 +-
     .../features/home/room/detail/RoomDetailViewModel.kt |  8 ++++----
     4 files changed, 13 insertions(+), 13 deletions(-)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt
    index 14f730c2ba..5af5183dfa 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt
    @@ -50,19 +50,19 @@ interface RelationService {
     
         /**
          * Sends a reaction (emoji) to the targetedEvent.
    -     * @param reaction the reaction (preferably emoji)
          * @param targetEventId the id of the event being reacted
    +     * @param reaction the reaction (preferably emoji)
          */
    -    fun sendReaction(reaction: String,
    -                     targetEventId: String): Cancelable
    +    fun sendReaction(targetEventId: String,
    +                     reaction: String): Cancelable
     
         /**
          * Undo a reaction (emoji) to the targetedEvent.
    -     * @param reaction the reaction (preferably emoji)
          * @param targetEventId the id of the event being reacted
    +     * @param reaction the reaction (preferably emoji)
          */
    -    fun undoReaction(reaction: String,
    -                     targetEventId: String) : Cancelable
    +    fun undoReaction(targetEventId: String,
    +                     reaction: String): Cancelable
     
         /**
          * Edit a text message body. Limited to "m.text" contentType
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt
    index 9f6500ce51..68669171c7 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt
    @@ -65,7 +65,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
             fun create(roomId: String): RelationService
         }
     
    -    override fun sendReaction(reaction: String, targetEventId: String): Cancelable {
    +    override fun sendReaction(targetEventId: String, reaction: String): Cancelable {
             val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction)
                     .also {
                         saveLocalEcho(it)
    @@ -75,7 +75,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
             return CancelableWork(context, sendRelationWork.id)
         }
     
    -    override fun undoReaction(reaction: String, targetEventId: String): Cancelable {
    +    override fun undoReaction(targetEventId: String, reaction: String): Cancelable {
             val params = FindReactionEventForUndoTask.Params(
                     roomId,
                     targetEventId,
    diff --git a/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt b/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt
    index 36aded79ad..c7543c3a50 100644
    --- a/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt
    +++ b/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt
    @@ -330,7 +330,7 @@ class PushrulesConditionTest {
                 TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
             }
     
    -        override fun sendReaction(reaction: String, targetEventId: String): Cancelable {
    +        override fun sendReaction(targetEventId: String, reaction: String): Cancelable {
                 TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
             }
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    index 58cfb97b87..57a2c76f61 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    @@ -445,7 +445,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
         }
     
         private fun handleSendReaction(action: RoomDetailActions.SendReaction) {
    -        room.sendReaction(action.reaction, action.targetEventId)
    +        room.sendReaction(action.targetEventId, action.reaction)
         }
     
         private fun handleRedactEvent(action: RoomDetailActions.RedactAction) {
    @@ -454,14 +454,14 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
         }
     
         private fun handleUndoReact(action: RoomDetailActions.UndoReaction) {
    -        room.undoReaction(action.reaction, action.targetEventId)
    +        room.undoReaction(action.targetEventId, action.reaction)
         }
     
         private fun handleUpdateQuickReaction(action: RoomDetailActions.UpdateQuickReactAction) {
             if (action.add) {
    -            room.sendReaction(action.selectedReaction, action.targetEventId)
    +            room.sendReaction(action.targetEventId, action.selectedReaction)
             } else {
    -            room.undoReaction(action.selectedReaction, action.targetEventId)
    +            room.undoReaction(action.targetEventId, action.selectedReaction)
             }
         }
     
    
    From cd0a40c18dbb69d20978ce59db9f0634e67935b7 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 18 Oct 2019 14:34:44 +0200
    Subject: [PATCH 147/197] Fix compil test issue
    
    ---
     .../matrix/android/api/pushrules/PushrulesConditionTest.kt      | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt b/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt
    index c7543c3a50..b2e2bfbf5f 100644
    --- a/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt
    +++ b/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt
    @@ -334,7 +334,7 @@ class PushrulesConditionTest {
                 TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
             }
     
    -        override fun undoReaction(reaction: String, targetEventId: String, myUserId: String) {
    +        override fun undoReaction(targetEventId: String, reaction: String): Cancelable {
                 TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
             }
     
    
    From 1dacfa6744142e90a4df1eae73e0fd52a54ed782 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Thu, 10 Oct 2019 14:36:10 +0200
    Subject: [PATCH 148/197] Rework message menu bottom sheet: remove sub Fragment
     and use Epoxy - Also move some class to some dedicated package
    
    ---
     .../vector/riotx/core/di/ScreenComponent.kt   |   8 +-
     .../VectorBaseBottomSheetDialogFragment.kt    |   3 +-
     .../home/room/detail/RoomDetailFragment.kt    |   6 +-
     .../DisplayReadReceiptsBottomSheet.kt         |   2 +-
     .../timeline/action/BottomSheetItemAction.kt  |  55 ++++
     .../action/BottomSheetItemMessagePreview.kt   |  59 ++++
     .../action/BottomSheetItemQuickReactions.kt   |  80 +++++
     .../action/BottomSheetItemSendState.kt        |  52 +++
     .../action/BottomSheetItemSeparator.kt        |  27 ++
     .../action/MessageActionsBottomSheet.kt       |  98 +-----
     .../action/MessageActionsEpoxyController.kt   | 104 ++++++
     .../action/MessageActionsViewModel.kt         | 297 +++++++++++++++---
     .../timeline/action/MessageMenuFragment.kt    | 104 ------
     .../timeline/action/MessageMenuViewModel.kt   | 279 ----------------
     .../timeline/action/QuickReactionFragment.kt  |  89 ------
     .../timeline/action/QuickReactionViewModel.kt |  96 ------
     .../detail/timeline/action/SimpleAction.kt    |  43 +++
     .../ViewEditHistoryBottomSheet.kt             |   4 +-
     .../ViewEditHistoryEpoxyController.kt         |   4 +-
     .../ViewEditHistoryViewModel.kt               |   3 +-
     .../ReactionInfoSimpleItem.kt                 |   2 +-
     .../ViewReactionsBottomSheet.kt}              |  14 +-
     .../ViewReactionsEpoxyController.kt           |   2 +-
     .../ViewReactionsViewModel.kt}                |  19 +-
     .../bottom_sheet_epoxylist_with_title.xml     |   3 +-
     .../bottom_sheet_generic_recycler_epoxy.xml   |  22 ++
     .../layout/bottom_sheet_message_actions.xml   | 149 ---------
     .../main/res/layout/fragment_message_menu.xml |   5 -
     ...ction.xml => item_bottom_sheet_action.xml} |   2 +-
     .../res/layout/item_bottom_sheet_divider.xml  |   6 +
     .../item_bottom_sheet_message_preview.xml     |  78 +++++
     .../item_bottom_sheet_message_status.xml      |  33 ++
     ...l => item_bottom_sheet_quick_reaction.xml} |   0
     33 files changed, 872 insertions(+), 876 deletions(-)
     rename vector/src/main/java/im/vector/riotx/{features/home/room/detail/timeline/action => core/platform}/VectorBaseBottomSheetDialogFragment.kt (95%)
     create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemAction.kt
     create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemMessagePreview.kt
     create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemQuickReactions.kt
     create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemSendState.kt
     create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemSeparator.kt
     create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt
     delete mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuFragment.kt
     delete mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuViewModel.kt
     delete mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/QuickReactionFragment.kt
     delete mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/QuickReactionViewModel.kt
     create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/SimpleAction.kt
     rename vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/{action => edithistory}/ViewEditHistoryBottomSheet.kt (93%)
     rename vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/{action => edithistory}/ViewEditHistoryEpoxyController.kt (98%)
     rename vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/{action => edithistory}/ViewEditHistoryViewModel.kt (96%)
     rename vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/{action => reactions}/ReactionInfoSimpleItem.kt (96%)
     rename vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/{action/ViewReactionBottomSheet.kt => reactions/ViewReactionsBottomSheet.kt} (82%)
     rename vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/{action => reactions}/ViewReactionsEpoxyController.kt (96%)
     rename vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/{action/ViewReactionViewModel.kt => reactions/ViewReactionsViewModel.kt} (83%)
     create mode 100644 vector/src/main/res/layout/bottom_sheet_generic_recycler_epoxy.xml
     delete mode 100644 vector/src/main/res/layout/bottom_sheet_message_actions.xml
     delete mode 100644 vector/src/main/res/layout/fragment_message_menu.xml
     rename vector/src/main/res/layout/{adapter_item_action.xml => item_bottom_sheet_action.xml} (97%)
     create mode 100644 vector/src/main/res/layout/item_bottom_sheet_divider.xml
     create mode 100644 vector/src/main/res/layout/item_bottom_sheet_message_preview.xml
     create mode 100644 vector/src/main/res/layout/item_bottom_sheet_message_status.xml
     rename vector/src/main/res/layout/{adapter_item_action_quick_reaction.xml => item_bottom_sheet_quick_reaction.xml} (100%)
    
    diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt
    index 3b18d3042e..2cbc3d2a8b 100644
    --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt
    @@ -42,6 +42,8 @@ import im.vector.riotx.features.home.group.GroupListFragment
     import im.vector.riotx.features.home.room.detail.RoomDetailFragment
     import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
     import im.vector.riotx.features.home.room.detail.timeline.action.*
    +import im.vector.riotx.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet
    +import im.vector.riotx.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet
     import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity
     import im.vector.riotx.features.home.room.list.RoomListFragment
     import im.vector.riotx.features.invite.VectorInviteView
    @@ -103,12 +105,10 @@ interface ScreenComponent {
     
         fun inject(messageActionsBottomSheet: MessageActionsBottomSheet)
     
    -    fun inject(viewReactionBottomSheet: ViewReactionBottomSheet)
    +    fun inject(viewReactionsBottomSheet: ViewReactionsBottomSheet)
     
         fun inject(viewEditHistoryBottomSheet: ViewEditHistoryBottomSheet)
     
    -    fun inject(messageMenuFragment: MessageMenuFragment)
    -
         fun inject(vectorSettingsActivity: VectorSettingsActivity)
     
         fun inject(createRoomFragment: CreateRoomFragment)
    @@ -135,8 +135,6 @@ interface ScreenComponent {
     
         fun inject(sasVerificationIncomingFragment: SASVerificationIncomingFragment)
     
    -    fun inject(quickReactionFragment: QuickReactionFragment)
    -
         fun inject(emojiReactionPickerActivity: EmojiReactionPickerActivity)
     
         fun inject(loginActivity: LoginActivity)
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/VectorBaseBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt
    similarity index 95%
    rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/VectorBaseBottomSheetDialogFragment.kt
    rename to vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt
    index 026cb3ba1c..892f7b0daa 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/VectorBaseBottomSheetDialogFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt
    @@ -13,7 +13,7 @@
      * See the License for the specific language governing permissions and
      * limitations under the License.
      */
    -package im.vector.riotx.features.home.room.detail.timeline.action
    +package im.vector.riotx.core.platform
     
     import android.content.Context
     import android.os.Bundle
    @@ -24,7 +24,6 @@ import com.airbnb.mvrx.MvRxViewModelStore
     import com.google.android.material.bottomsheet.BottomSheetDialogFragment
     import im.vector.riotx.core.di.DaggerScreenComponent
     import im.vector.riotx.core.di.ScreenComponent
    -import im.vector.riotx.core.platform.VectorBaseActivity
     import java.util.*
     
     /**
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    index f09fbff09f..7447bf92e6 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    @@ -94,7 +94,9 @@ import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState
     import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
     import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
     import im.vector.riotx.features.home.room.detail.timeline.action.*
    +import im.vector.riotx.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet
     import im.vector.riotx.features.home.room.detail.timeline.item.*
    +import im.vector.riotx.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet
     import im.vector.riotx.features.html.EventHtmlRenderer
     import im.vector.riotx.features.html.PillImageSpan
     import im.vector.riotx.features.invite.VectorInviteView
    @@ -917,7 +919,7 @@ class RoomDetailFragment :
         }
     
         override fun onLongClickOnReactionPill(informationData: MessageInformationData, reaction: String) {
    -        ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, informationData)
    +        ViewReactionsBottomSheet.newInstance(roomDetailArgs.roomId, informationData)
                     .show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
         }
     
    @@ -970,7 +972,7 @@ class RoomDetailFragment :
                     startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), action.eventId), REACTION_SELECT_REQUEST_CODE)
                 }
                 is SimpleAction.ViewReactions       -> {
    -                ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData)
    +                ViewReactionsBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData)
                             .show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
                 }
                 is SimpleAction.Copy                -> {
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt
    index 510fc1e26f..80539d73e4 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt
    @@ -30,7 +30,7 @@ import com.airbnb.mvrx.MvRx
     import com.airbnb.mvrx.args
     import im.vector.riotx.R
     import im.vector.riotx.core.di.ScreenComponent
    -import im.vector.riotx.features.home.room.detail.timeline.action.VectorBaseBottomSheetDialogFragment
    +import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
     import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
     import kotlinx.android.parcel.Parcelize
     import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.*
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemAction.kt
    new file mode 100644
    index 0000000000..8ee7460d53
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemAction.kt
    @@ -0,0 +1,55 @@
    +/*
    + * Copyright 2019 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.riotx.features.home.room.detail.timeline.action
    +
    +import android.view.View
    +import android.widget.ImageView
    +import android.widget.TextView
    +import androidx.annotation.DrawableRes
    +import com.airbnb.epoxy.EpoxyAttribute
    +import com.airbnb.epoxy.EpoxyModelClass
    +import im.vector.riotx.R
    +import im.vector.riotx.core.epoxy.VectorEpoxyHolder
    +import im.vector.riotx.core.epoxy.VectorEpoxyModel
    +
    +/**
    + * A action for bottom sheet.
    + */
    +@EpoxyModelClass(layout = R.layout.item_bottom_sheet_action)
    +abstract class BottomSheetItemAction : VectorEpoxyModel() {
    +
    +    @EpoxyAttribute
    +    @DrawableRes
    +    var iconRes: Int = 0
    +    @EpoxyAttribute
    +    var textRes: Int = 0
    +    @EpoxyAttribute
    +    lateinit var listener: View.OnClickListener
    +
    +    override fun bind(holder: Holder) {
    +        holder.view.setOnClickListener {
    +            listener.onClick(it)
    +        }
    +
    +        holder.icon.setImageResource(iconRes)
    +        holder.text.setText(textRes)
    +    }
    +
    +    class Holder : VectorEpoxyHolder() {
    +        val icon by bind(R.id.action_icon)
    +        val text by bind(R.id.action_title)
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemMessagePreview.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemMessagePreview.kt
    new file mode 100644
    index 0000000000..d37aa43770
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemMessagePreview.kt
    @@ -0,0 +1,59 @@
    +/*
    + * Copyright 2019 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.riotx.features.home.room.detail.timeline.action
    +
    +import android.widget.ImageView
    +import android.widget.TextView
    +import com.airbnb.epoxy.EpoxyAttribute
    +import com.airbnb.epoxy.EpoxyModelClass
    +import im.vector.riotx.R
    +import im.vector.riotx.core.epoxy.VectorEpoxyHolder
    +import im.vector.riotx.core.epoxy.VectorEpoxyModel
    +import im.vector.riotx.core.extensions.setTextOrHide
    +import im.vector.riotx.features.home.AvatarRenderer
    +import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
    +
    +/**
    + * A message preview for bottom sheet.
    + */
    +@EpoxyModelClass(layout = R.layout.item_bottom_sheet_message_preview)
    +abstract class BottomSheetItemMessagePreview : VectorEpoxyModel() {
    +
    +    @EpoxyAttribute
    +    lateinit var avatarRenderer: AvatarRenderer
    +    @EpoxyAttribute
    +    lateinit var informationData: MessageInformationData
    +    @EpoxyAttribute
    +    var senderName: String? = null
    +    @EpoxyAttribute
    +    lateinit var body: CharSequence
    +    @EpoxyAttribute
    +    var time: CharSequence? = null
    +
    +    override fun bind(holder: Holder) {
    +        avatarRenderer.render(informationData.avatarUrl, informationData.senderId, senderName, holder.avatar)
    +        holder.sender.setTextOrHide(senderName)
    +        holder.body.text = body
    +        holder.timestamp.setTextOrHide(time)
    +    }
    +
    +    class Holder : VectorEpoxyHolder() {
    +        val avatar by bind(R.id.bottom_sheet_message_preview_avatar)
    +        val sender by bind(R.id.bottom_sheet_message_preview_sender)
    +        val body by bind(R.id.bottom_sheet_message_preview_body)
    +        val timestamp by bind(R.id.bottom_sheet_message_preview_timestamp)
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemQuickReactions.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemQuickReactions.kt
    new file mode 100644
    index 0000000000..3aafa7c974
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemQuickReactions.kt
    @@ -0,0 +1,80 @@
    +/*
    + * Copyright 2019 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.riotx.features.home.room.detail.timeline.action
    +
    +import android.graphics.Typeface
    +import android.widget.TextView
    +import com.airbnb.epoxy.EpoxyAttribute
    +import com.airbnb.epoxy.EpoxyModelClass
    +import im.vector.riotx.EmojiCompatFontProvider
    +import im.vector.riotx.R
    +import im.vector.riotx.core.epoxy.VectorEpoxyHolder
    +import im.vector.riotx.core.epoxy.VectorEpoxyModel
    +
    +/**
    + * A quick reaction list for bottom sheet.
    + */
    +@EpoxyModelClass(layout = R.layout.item_bottom_sheet_quick_reaction)
    +abstract class BottomSheetItemQuickReactions : VectorEpoxyModel() {
    +
    +    @EpoxyAttribute
    +    lateinit var fontProvider: EmojiCompatFontProvider
    +    @EpoxyAttribute
    +    lateinit var texts: List
    +    @EpoxyAttribute
    +    lateinit var selecteds: List
    +    @EpoxyAttribute
    +    var listener: Listener? = null
    +
    +    override fun bind(holder: Holder) {
    +        holder.textViews.forEachIndexed { index, textView ->
    +            textView.typeface = fontProvider.typeface ?: Typeface.DEFAULT
    +            textView.text = texts[index]
    +            textView.alpha = if (selecteds[index]) 0.2f else 1f
    +
    +            textView.setOnClickListener {
    +                listener?.didSelect(texts[index], !selecteds[index])
    +            }
    +        }
    +    }
    +
    +    class Holder : VectorEpoxyHolder() {
    +        private val quickReaction0 by bind(R.id.quickReaction0)
    +        private val quickReaction1 by bind(R.id.quickReaction1)
    +        private val quickReaction2 by bind(R.id.quickReaction2)
    +        private val quickReaction3 by bind(R.id.quickReaction3)
    +        private val quickReaction4 by bind(R.id.quickReaction4)
    +        private val quickReaction5 by bind(R.id.quickReaction5)
    +        private val quickReaction6 by bind(R.id.quickReaction6)
    +        private val quickReaction7 by bind(R.id.quickReaction7)
    +
    +        val textViews
    +            get() = listOf(
    +                    quickReaction0,
    +                    quickReaction1,
    +                    quickReaction2,
    +                    quickReaction3,
    +                    quickReaction4,
    +                    quickReaction5,
    +                    quickReaction6,
    +                    quickReaction7
    +            )
    +    }
    +
    +    interface Listener {
    +        fun didSelect(emoji: String, selected: Boolean)
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemSendState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemSendState.kt
    new file mode 100644
    index 0000000000..86a5512349
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemSendState.kt
    @@ -0,0 +1,52 @@
    +/*
    + * Copyright 2019 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.riotx.features.home.room.detail.timeline.action
    +
    +import android.view.View
    +import android.widget.TextView
    +import androidx.annotation.DrawableRes
    +import androidx.core.view.isVisible
    +import com.airbnb.epoxy.EpoxyAttribute
    +import com.airbnb.epoxy.EpoxyModelClass
    +import im.vector.riotx.R
    +import im.vector.riotx.core.epoxy.VectorEpoxyHolder
    +import im.vector.riotx.core.epoxy.VectorEpoxyModel
    +
    +/**
    + * A send state for bottom sheet.
    + */
    +@EpoxyModelClass(layout = R.layout.item_bottom_sheet_message_status)
    +abstract class BottomSheetItemSendState : VectorEpoxyModel() {
    +
    +    @EpoxyAttribute
    +    var showProgress: Boolean = false
    +    @EpoxyAttribute
    +    lateinit var text: CharSequence
    +    @EpoxyAttribute
    +    @DrawableRes
    +    var drawableStart: Int = 0
    +
    +    override fun bind(holder: Holder) {
    +        holder.progress.isVisible = showProgress
    +        holder.text.setCompoundDrawablesWithIntrinsicBounds(drawableStart, 0, 0, 0)
    +        holder.text.text = text
    +    }
    +
    +    class Holder : VectorEpoxyHolder() {
    +        val progress by bind(R.id.messageStatusProgress)
    +        val text by bind(R.id.messageStatusText)
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemSeparator.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemSeparator.kt
    new file mode 100644
    index 0000000000..f09f68b714
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemSeparator.kt
    @@ -0,0 +1,27 @@
    +/*
    + * Copyright 2019 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.riotx.features.home.room.detail.timeline.action
    +
    +import com.airbnb.epoxy.EpoxyModelClass
    +import im.vector.riotx.R
    +import im.vector.riotx.core.epoxy.VectorEpoxyHolder
    +import im.vector.riotx.core.epoxy.VectorEpoxyModel
    +
    +@EpoxyModelClass(layout = R.layout.item_bottom_sheet_divider)
    +abstract class BottomSheetItemSeparator : VectorEpoxyModel() {
    +
    +    class Holder : VectorEpoxyHolder()
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
    index 11f3207e32..39116b59b5 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
    @@ -21,92 +21,53 @@ import android.view.LayoutInflater
     import android.view.View
     import android.view.ViewGroup
     import android.widget.FrameLayout
    -import android.widget.ImageView
    -import android.widget.TextView
    -import androidx.core.view.isVisible
     import androidx.lifecycle.ViewModelProviders
    -import butterknife.BindView
    -import butterknife.ButterKnife
    -import com.airbnb.mvrx.MvRx
     import com.airbnb.mvrx.fragmentViewModel
     import com.airbnb.mvrx.withState
     import com.google.android.material.bottomsheet.BottomSheetBehavior
     import com.google.android.material.bottomsheet.BottomSheetDialog
    +import im.vector.riotx.EmojiCompatFontProvider
     import im.vector.riotx.R
     import im.vector.riotx.core.di.ScreenComponent
    +import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
     import im.vector.riotx.features.home.AvatarRenderer
     import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
    -import kotlinx.android.synthetic.main.bottom_sheet_message_actions.*
    +import kotlinx.android.synthetic.main.bottom_sheet_generic_recycler_epoxy.*
     import javax.inject.Inject
     
     /**
      * Bottom sheet fragment that shows a message preview with list of contextual actions
    - * (Includes fragments for quick reactions and list of actions)
      */
    -class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment() {
    +class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), MessageActionsEpoxyController.MessageActionsEpoxyControllerListener {
    +    @Inject lateinit var messageActionViewModelFactory: MessageActionsViewModel.Factory
    +    @Inject lateinit var avatarRenderer: AvatarRenderer
    +    @Inject lateinit var fontProvider: EmojiCompatFontProvider
     
    -    @Inject
    -    lateinit var messageActionViewModelFactory: MessageActionsViewModel.Factory
    -    @Inject
    -    lateinit var avatarRenderer: AvatarRenderer
         private val viewModel: MessageActionsViewModel by fragmentViewModel(MessageActionsViewModel::class)
     
    +    private lateinit var messageActionsEpoxyController: MessageActionsEpoxyController
         private lateinit var actionHandlerModel: ActionsHandler
     
    -    @BindView(R.id.bottom_sheet_message_preview_avatar)
    -    lateinit var senderAvatarImageView: ImageView
    -
    -    @BindView(R.id.bottom_sheet_message_preview_sender)
    -    lateinit var senderNameTextView: TextView
    -
    -    @BindView(R.id.bottom_sheet_message_preview_timestamp)
    -    lateinit var messageTimestampText: TextView
    -
    -    @BindView(R.id.bottom_sheet_message_preview_body)
    -    lateinit var messageBodyTextView: TextView
    -
         override fun injectWith(screenComponent: ScreenComponent) {
             screenComponent.inject(this)
         }
     
         override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    -        val view = inflater.inflate(R.layout.bottom_sheet_message_actions, container, false)
    -        ButterKnife.bind(this, view)
    -        return view
    +        return inflater.inflate(R.layout.bottom_sheet_generic_recycler_epoxy, container, false)
         }
     
         override fun onActivityCreated(savedInstanceState: Bundle?) {
             super.onActivityCreated(savedInstanceState)
             actionHandlerModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java)
     
    -        val cfm = childFragmentManager
    -        var menuActionFragment = cfm.findFragmentByTag("MenuActionFragment") as? MessageMenuFragment
    -        if (menuActionFragment == null) {
    -            menuActionFragment = MessageMenuFragment.newInstance(arguments!!.get(MvRx.KEY_ARG) as TimelineEventFragmentArgs)
    -            cfm.beginTransaction()
    -                    .replace(R.id.bottom_sheet_menu_container, menuActionFragment, "MenuActionFragment")
    -                    .commit()
    -        }
    -        menuActionFragment.interactionListener = object : MessageMenuFragment.InteractionListener {
    -            override fun didSelectMenuAction(simpleAction: SimpleAction) {
    -                actionHandlerModel.fireAction(simpleAction)
    -                dismiss()
    -            }
    -        }
    +        messageActionsEpoxyController = MessageActionsEpoxyController(requireContext(), avatarRenderer, fontProvider)
    +        bottomSheetEpoxyRecyclerView.setController(messageActionsEpoxyController)
    +        messageActionsEpoxyController.listener = this
    +    }
     
    -        var quickReactionFragment = cfm.findFragmentByTag("QuickReaction") as? QuickReactionFragment
    -        if (quickReactionFragment == null) {
    -            quickReactionFragment = QuickReactionFragment.newInstance(arguments!!.get(MvRx.KEY_ARG) as TimelineEventFragmentArgs)
    -            cfm.beginTransaction()
    -                    .replace(R.id.bottom_sheet_quick_reaction_container, quickReactionFragment, "QuickReaction")
    -                    .commit()
    -        }
    -        quickReactionFragment.interactionListener = object : QuickReactionFragment.InteractionListener {
    -            override fun didQuickReactWith(clickedOn: String, add: Boolean, eventId: String) {
    -                actionHandlerModel.fireAction(SimpleAction.QuickReact(eventId, clickedOn, add))
    -                dismiss()
    -            }
    -        }
    +    override fun didSelectMenuAction(simpleAction: SimpleAction) {
    +        actionHandlerModel.fireAction(simpleAction)
    +        dismiss()
         }
     
         override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
    @@ -124,32 +85,7 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment() {
         }
     
         override fun invalidate() = withState(viewModel) {
    -        val body = viewModel.resolveBody(it)
    -        if (body != null) {
    -            bottom_sheet_message_preview.isVisible = true
    -            senderNameTextView.text = it.senderName()
    -            messageBodyTextView.text = body
    -            messageTimestampText.text = it.time()
    -            avatarRenderer.render(it.informationData.avatarUrl, it.informationData.senderId, it.senderName(), senderAvatarImageView)
    -        } else {
    -            bottom_sheet_message_preview.isVisible = false
    -        }
    -        quickReactBottomDivider.isVisible = it.canReact()
    -        bottom_sheet_quick_reaction_container.isVisible = it.canReact()
    -        if (it.informationData.sendState.isSending()) {
    -            messageStatusInfo.isVisible = true
    -            messageStatusProgress.isVisible = true
    -            messageStatusText.text = getString(R.string.event_status_sending_message)
    -            messageStatusText.setCompoundDrawables(null, null, null, null)
    -        } else if (it.informationData.sendState.hasFailed()) {
    -            messageStatusInfo.isVisible = true
    -            messageStatusProgress.isVisible = false
    -            messageStatusText.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_warning_small, 0, 0, 0)
    -            messageStatusText.text = getString(R.string.unable_to_send_message)
    -        } else {
    -            messageStatusInfo.isVisible = false
    -        }
    -        return@withState
    +        messageActionsEpoxyController.setData(it)
         }
     
         companion object {
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt
    new file mode 100644
    index 0000000000..1de7302820
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt
    @@ -0,0 +1,104 @@
    +/*
    + * Copyright 2019 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.riotx.features.home.room.detail.timeline.action
    +
    +import android.content.Context
    +import android.view.View
    +import com.airbnb.epoxy.TypedEpoxyController
    +import com.airbnb.mvrx.Success
    +import im.vector.riotx.EmojiCompatFontProvider
    +import im.vector.riotx.R
    +import im.vector.riotx.features.home.AvatarRenderer
    +
    +/**
    + * Epoxy controller for message action list
    + */
    +class MessageActionsEpoxyController(private val context: Context,
    +                                    private val avatarRenderer: AvatarRenderer,
    +                                    private val fontProvider: EmojiCompatFontProvider) : TypedEpoxyController() {
    +
    +    var listener: MessageActionsEpoxyControllerListener? = null
    +
    +    override fun buildModels(state: MessageActionState) {
    +        // Message preview
    +        val body = state.messageBody
    +        if (body != null) {
    +            bottomSheetItemMessagePreview {
    +                id("preview")
    +                avatarRenderer(avatarRenderer)
    +                informationData(state.informationData)
    +                senderName(state.senderName())
    +                body(body)
    +                time(state.time())
    +            }
    +        }
    +
    +        // Send state
    +        if (state.informationData.sendState.isSending()) {
    +            bottomSheetItemSendState {
    +                id("send_state")
    +                showProgress(true)
    +                text(context.getString(R.string.event_status_sending_message))
    +            }
    +        } else if (state.informationData.sendState.hasFailed()) {
    +            bottomSheetItemSendState {
    +                id("send_state")
    +                showProgress(false)
    +                text(context.getString(R.string.unable_to_send_message))
    +                drawableStart(R.drawable.ic_warning_small)
    +            }
    +        }
    +
    +        // Quick reactions
    +        if (state.canReact() && state.quickStates is Success) {
    +            // Separator
    +            bottomSheetItemSeparator {
    +                id("reaction_separator")
    +            }
    +
    +            bottomSheetItemQuickReactions {
    +                id("quick_reaction")
    +                fontProvider(fontProvider)
    +                texts(state.quickStates()?.map { it.reaction }.orEmpty())
    +                selecteds(state.quickStates()?.map { it.isSelected }.orEmpty())
    +                listener(object : BottomSheetItemQuickReactions.Listener {
    +                    override fun didSelect(emoji: String, selected: Boolean) {
    +                        listener?.didSelectMenuAction(SimpleAction.QuickReact(state.eventId, emoji, selected))
    +                    }
    +                })
    +            }
    +        }
    +
    +        // Separator
    +        bottomSheetItemSeparator {
    +            id("actions_separator")
    +        }
    +
    +        // Action
    +        state.actions()?.forEachIndexed { index, action ->
    +            bottomSheetItemAction {
    +                id("action_$index")
    +                iconRes(action.iconResId)
    +                textRes(action.titleRes)
    +                listener(View.OnClickListener { listener?.didSelectMenuAction(action) })
    +            }
    +        }
    +    }
    +
    +    interface MessageActionsEpoxyControllerListener {
    +        fun didSelectMenuAction(simpleAction: SimpleAction)
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
    index c80c7c5f15..cc1237a555 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
    @@ -21,26 +21,47 @@ import com.squareup.inject.assisted.AssistedInject
     import dagger.Lazy
     import im.vector.matrix.android.api.session.Session
     import im.vector.matrix.android.api.session.events.model.EventType
    +import im.vector.matrix.android.api.session.events.model.isTextMessage
    +import im.vector.matrix.android.api.session.events.model.toModel
     import im.vector.matrix.android.api.session.room.model.message.MessageContent
    +import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
     import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
     import im.vector.matrix.android.api.session.room.model.message.MessageType
    +import im.vector.matrix.android.api.session.room.send.SendState
     import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
     import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
    +import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited
    +import im.vector.matrix.android.api.util.Optional
     import im.vector.matrix.rx.RxRoom
     import im.vector.matrix.rx.unwrap
    +import im.vector.riotx.R
     import im.vector.riotx.core.extensions.canReact
     import im.vector.riotx.core.platform.VectorViewModel
    +import im.vector.riotx.core.resources.StringProvider
     import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter
     import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
     import im.vector.riotx.features.html.EventHtmlRenderer
     import java.text.SimpleDateFormat
     import java.util.*
     
    +/**
    + * Quick reactions state
    + */
    +data class ToggleState(
    +        val reaction: String,
    +        val isSelected: Boolean
    +)
    +
     data class MessageActionState(
             val roomId: String,
             val eventId: String,
             val informationData: MessageInformationData,
    -        val timelineEvent: Async = Uninitialized
    +        val timelineEvent: Async = Uninitialized,
    +        val messageBody: CharSequence? = null,
    +        // For quick reactions
    +        val quickStates: Async> = Uninitialized,
    +        // For actions
    +        val actions: Async> = Uninitialized
     ) : MvRxState {
     
         constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData)
    @@ -49,18 +70,93 @@ data class MessageActionState(
     
         fun senderName(): String = informationData.memberName?.toString() ?: ""
     
    -    fun time(): String? = timelineEvent()?.root?.originServerTs?.let { dateFormat.format(Date(it)) }
    -                          ?: ""
    +    fun time(): String? = timelineEvent()?.root?.originServerTs?.let { dateFormat.format(Date(it)) } ?: ""
     
         fun canReact() = timelineEvent()?.canReact() == true
    +}
     
    -    fun messageBody(eventHtmlRenderer: EventHtmlRenderer?, noticeEventFormatter: NoticeEventFormatter?): CharSequence? {
    +/**
    + * Information related to an event and used to display preview in contextual bottomsheet.
    + */
    +class MessageActionsViewModel @AssistedInject constructor(@Assisted
    +                                                          initialState: MessageActionState,
    +                                                          private val eventHtmlRenderer: Lazy,
    +                                                          private val session: Session,
    +                                                          private val noticeEventFormatter: NoticeEventFormatter,
    +                                                          private val stringProvider: StringProvider
    +) : VectorViewModel(initialState) {
    +
    +    private val eventId = initialState.eventId
    +    private val informationData = initialState.informationData
    +    private val room = session.getRoom(initialState.roomId)
    +
    +    @AssistedInject.Factory
    +    interface Factory {
    +        fun create(initialState: MessageActionState): MessageActionsViewModel
    +    }
    +
    +    companion object : MvRxViewModelFactory {
    +
    +        val quickEmojis = listOf("👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀")
    +
    +        override fun create(viewModelContext: ViewModelContext, state: MessageActionState): MessageActionsViewModel? {
    +            val fragment: MessageActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
    +            return fragment.messageActionViewModelFactory.create(state)
    +        }
    +    }
    +
    +    init {
    +        observeEvent()
    +        observeReactions()
    +        observeEventAction()
    +    }
    +
    +    private fun observeEvent() {
    +        if (room == null) return
    +        RxRoom(room)
    +                .liveTimelineEvent(eventId)
    +                .unwrap()
    +                .execute {
    +                    copy(
    +                            timelineEvent = it,
    +                            messageBody = computeMessageBody(it)
    +                    )
    +                }
    +    }
    +
    +    private fun observeEventAction() {
    +        if (room == null) return
    +        RxRoom(room)
    +                .liveTimelineEvent(eventId)
    +                .map {
    +                    actionsForEvent(it)
    +                }
    +                .execute {
    +                    copy(actions = it)
    +                }
    +    }
    +
    +    private fun observeReactions() {
    +        if (room == null) return
    +        RxRoom(room)
    +                .liveAnnotationSummary(eventId)
    +                .map { annotations ->
    +                    quickEmojis.map { emoji ->
    +                        ToggleState(emoji, annotations.getOrNull()?.reactionsSummary?.firstOrNull { it.key == emoji }?.addedByMe ?: false)
    +                    }
    +                }
    +                .execute {
    +                    copy(quickStates = it)
    +                }
    +    }
    +
    +    private fun computeMessageBody(timelineEvent: Async): CharSequence? {
             return when (timelineEvent()?.root?.getClearType()) {
                 EventType.MESSAGE     -> {
                     val messageContent: MessageContent? = timelineEvent()?.getLastMessageContent()
                     if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
    -                    eventHtmlRenderer?.render(messageContent.formattedBody
    -                                              ?: messageContent.body)
    +                    eventHtmlRenderer.get().render(messageContent.formattedBody
    +                            ?: messageContent.body)
                     } else {
                         messageContent?.body
                     }
    @@ -72,54 +168,177 @@ data class MessageActionState(
                 EventType.CALL_INVITE,
                 EventType.CALL_HANGUP,
                 EventType.CALL_ANSWER -> {
    -                timelineEvent()?.let { noticeEventFormatter?.format(it) }
    +                timelineEvent()?.let { noticeEventFormatter.format(it) }
                 }
                 else                  -> null
             }
         }
    -}
     
    -/**
    - * Information related to an event and used to display preview in contextual bottomsheet.
    - */
    -class MessageActionsViewModel @AssistedInject constructor(@Assisted
    -                                                          initialState: MessageActionState,
    -                                                          private val eventHtmlRenderer: Lazy,
    -                                                          session: Session,
    -                                                          private val noticeEventFormatter: NoticeEventFormatter
    -) : VectorViewModel(initialState) {
    +    private fun actionsForEvent(optionalEvent: Optional): List {
    +        val event = optionalEvent.getOrNull() ?: return emptyList()
     
    -    private val eventId = initialState.eventId
    -    private val room = session.getRoom(initialState.roomId)
    +        val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent.toModel()
    +                ?: event.root.getClearContent().toModel()
    +        val type = messageContent?.type
     
    -    @AssistedInject.Factory
    -    interface Factory {
    -        fun create(initialState: MessageActionState): MessageActionsViewModel
    -    }
    +        return arrayListOf().apply {
    +            if (event.root.sendState.hasFailed()) {
    +                if (canRetry(event)) {
    +                    add(SimpleAction.Resend(eventId))
    +                }
    +                add(SimpleAction.Remove(eventId))
    +            } else if (event.root.sendState.isSending()) {
    +                // TODO is uploading attachment?
    +                if (canCancel(event)) {
    +                    add(SimpleAction.Cancel(eventId))
    +                }
    +            } else {
    +                if (!event.root.isRedacted()) {
    +                    if (canReply(event, messageContent)) {
    +                        add(SimpleAction.Reply(eventId))
    +                    }
     
    -    companion object : MvRxViewModelFactory {
    +                    if (canEdit(event, session.myUserId)) {
    +                        add(SimpleAction.Edit(eventId))
    +                    }
     
    -        override fun create(viewModelContext: ViewModelContext, state: MessageActionState): MessageActionsViewModel? {
    -            val fragment: MessageActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
    -            return fragment.messageActionViewModelFactory.create(state)
    +                    if (canRedact(event, session.myUserId)) {
    +                        add(SimpleAction.Delete(eventId))
    +                    }
    +
    +                    if (canCopy(type)) {
    +                        // TODO copy images? html? see ClipBoard
    +                        add(SimpleAction.Copy(messageContent!!.body))
    +                    }
    +
    +                    if (event.canReact()) {
    +                        add(SimpleAction.AddReaction(eventId))
    +                    }
    +
    +                    if (canQuote(event, messageContent)) {
    +                        add(SimpleAction.Quote(eventId))
    +                    }
    +
    +                    if (canViewReactions(event)) {
    +                        add(SimpleAction.ViewReactions(informationData))
    +                    }
    +
    +                    if (event.hasBeenEdited()) {
    +                        add(SimpleAction.ViewEditHistory(informationData))
    +                    }
    +
    +                    if (canShare(type)) {
    +                        if (messageContent is MessageImageContent) {
    +                            session.contentUrlResolver().resolveFullSize(messageContent.url)?.let { url ->
    +                                add(SimpleAction.Share(url))
    +                            }
    +                        }
    +                        // TODO
    +                    }
    +
    +                    if (event.root.sendState == SendState.SENT) {
    +                        // TODO Can be redacted
    +
    +                        // TODO sent by me or sufficient power level
    +                    }
    +                }
    +
    +                add(SimpleAction.ViewSource(event.root.toContentStringWithIndent()))
    +                if (event.isEncrypted()) {
    +                    val decryptedContent = event.root.toClearContentStringWithIndent()
    +                            ?: stringProvider.getString(R.string.encryption_information_decryption_error)
    +                    add(SimpleAction.ViewDecryptedSource(decryptedContent))
    +                }
    +                add(SimpleAction.CopyPermalink(eventId))
    +
    +                if (session.myUserId != event.root.senderId && event.root.getClearType() == EventType.MESSAGE) {
    +                    // not sent by me
    +                    add(SimpleAction.Flag(eventId))
    +                }
    +            }
             }
         }
     
    -    init {
    -        observeEvent()
    +    private fun canCancel(@Suppress("UNUSED_PARAMETER") event: TimelineEvent): Boolean {
    +        return false
         }
     
    -    private fun observeEvent() {
    -        if (room == null) return
    -        RxRoom(room)
    -                .liveTimelineEvent(eventId)
    -                .unwrap()
    -                .execute {
    -                    copy(timelineEvent = it)
    -                }
    +    private fun canReply(event: TimelineEvent, messageContent: MessageContent?): Boolean {
    +        // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
    +        if (event.root.getClearType() != EventType.MESSAGE) return false
    +        return when (messageContent?.type) {
    +            MessageType.MSGTYPE_TEXT,
    +            MessageType.MSGTYPE_NOTICE,
    +            MessageType.MSGTYPE_EMOTE,
    +            MessageType.MSGTYPE_IMAGE,
    +            MessageType.MSGTYPE_VIDEO,
    +            MessageType.MSGTYPE_AUDIO,
    +            MessageType.MSGTYPE_FILE -> true
    +            else                     -> false
    +        }
         }
     
    -    fun resolveBody(state: MessageActionState): CharSequence? {
    -        return state.messageBody(eventHtmlRenderer.get(), noticeEventFormatter)
    +    private fun canQuote(event: TimelineEvent, messageContent: MessageContent?): Boolean {
    +        // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
    +        if (event.root.getClearType() != EventType.MESSAGE) return false
    +        return when (messageContent?.type) {
    +            MessageType.MSGTYPE_TEXT,
    +            MessageType.MSGTYPE_NOTICE,
    +            MessageType.MSGTYPE_EMOTE,
    +            MessageType.FORMAT_MATRIX_HTML,
    +            MessageType.MSGTYPE_LOCATION -> {
    +                true
    +            }
    +            else                         -> false
    +        }
    +    }
    +
    +    private fun canRedact(event: TimelineEvent, myUserId: String): Boolean {
    +        // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
    +        if (event.root.getClearType() != EventType.MESSAGE) return false
    +        // TODO if user is admin or moderator
    +        return event.root.senderId == myUserId
    +    }
    +
    +    private fun canRetry(event: TimelineEvent): Boolean {
    +        return event.root.sendState.hasFailed() && event.root.isTextMessage()
    +    }
    +
    +    private fun canViewReactions(event: TimelineEvent): Boolean {
    +        // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
    +        if (event.root.getClearType() != EventType.MESSAGE) return false
    +        // TODO if user is admin or moderator
    +        return event.annotations?.reactionsSummary?.isNotEmpty() ?: false
    +    }
    +
    +    private fun canEdit(event: TimelineEvent, myUserId: String): Boolean {
    +        // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
    +        if (event.root.getClearType() != EventType.MESSAGE) return false
    +        // TODO if user is admin or moderator
    +        val messageContent = event.root.getClearContent().toModel()
    +        return event.root.senderId == myUserId && (
    +                messageContent?.type == MessageType.MSGTYPE_TEXT
    +                        || messageContent?.type == MessageType.MSGTYPE_EMOTE
    +                )
    +    }
    +
    +    private fun canCopy(type: String?): Boolean {
    +        return when (type) {
    +            MessageType.MSGTYPE_TEXT,
    +            MessageType.MSGTYPE_NOTICE,
    +            MessageType.MSGTYPE_EMOTE,
    +            MessageType.FORMAT_MATRIX_HTML,
    +            MessageType.MSGTYPE_LOCATION -> true
    +            else                         -> false
    +        }
    +    }
    +
    +    private fun canShare(type: String?): Boolean {
    +        return when (type) {
    +            MessageType.MSGTYPE_IMAGE,
    +            MessageType.MSGTYPE_AUDIO,
    +            MessageType.MSGTYPE_VIDEO -> true
    +            else                      -> false
    +        }
         }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuFragment.kt
    deleted file mode 100644
    index 2eec705eea..0000000000
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuFragment.kt
    +++ /dev/null
    @@ -1,104 +0,0 @@
    -/*
    - * Copyright 2019 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.riotx.features.home.room.detail.timeline.action
    -
    -import android.os.Bundle
    -import android.view.LayoutInflater
    -import android.view.View
    -import android.view.ViewGroup
    -import android.widget.FrameLayout
    -import android.widget.ImageView
    -import android.widget.LinearLayout
    -import android.widget.TextView
    -import com.airbnb.mvrx.MvRx
    -import com.airbnb.mvrx.fragmentViewModel
    -import com.airbnb.mvrx.withState
    -import im.vector.riotx.R
    -import im.vector.riotx.core.di.ScreenComponent
    -import im.vector.riotx.core.platform.VectorBaseFragment
    -import im.vector.riotx.features.themes.ThemeUtils
    -import javax.inject.Inject
    -
    -/**
    - * Fragment showing the list of available contextual action for a given message.
    - */
    -class MessageMenuFragment : VectorBaseFragment() {
    -
    -    @Inject lateinit var messageMenuViewModelFactory: MessageMenuViewModel.Factory
    -    private val viewModel: MessageMenuViewModel by fragmentViewModel(MessageMenuViewModel::class)
    -    private var addSeparators = false
    -    var interactionListener: InteractionListener? = null
    -
    -    override fun injectWith(injector: ScreenComponent) {
    -        injector.inject(this)
    -    }
    -
    -    override fun getLayoutResId() = R.layout.fragment_message_menu
    -
    -    override fun invalidate() = withState(viewModel) { state ->
    -
    -        val linearLayout = view as? LinearLayout
    -        if (linearLayout != null) {
    -            val inflater = LayoutInflater.from(linearLayout.context)
    -            linearLayout.removeAllViews()
    -            var insertIndex = 0
    -            val actions = state.actions()
    -            actions?.forEachIndexed { index, action ->
    -                inflateActionView(action, inflater, linearLayout)?.let {
    -                    it.setOnClickListener {
    -                        interactionListener?.didSelectMenuAction(action)
    -                    }
    -                    linearLayout.addView(it, insertIndex)
    -                    insertIndex++
    -                    if (addSeparators) {
    -                        if (index < actions.size - 1) {
    -                            linearLayout.addView(inflateSeparatorView(), insertIndex)
    -                            insertIndex++
    -                        }
    -                    }
    -                }
    -            }
    -        }
    -    }
    -
    -    private fun inflateActionView(action: SimpleAction, inflater: LayoutInflater, container: ViewGroup?): View? {
    -        return inflater.inflate(R.layout.adapter_item_action, container, false)?.apply {
    -            findViewById(R.id.action_icon)?.setImageResource(action.iconResId)
    -            findViewById(R.id.action_title)?.setText(action.titleRes)
    -        }
    -    }
    -
    -    private fun inflateSeparatorView(): View {
    -        val frame = FrameLayout(requireContext())
    -        frame.setBackgroundColor(ThemeUtils.getColor(requireContext(), R.attr.vctr_list_divider_color))
    -        frame.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, requireContext().resources.displayMetrics.density.toInt())
    -        return frame
    -    }
    -
    -    interface InteractionListener {
    -        fun didSelectMenuAction(simpleAction: SimpleAction)
    -    }
    -
    -    companion object {
    -        fun newInstance(pa: TimelineEventFragmentArgs): MessageMenuFragment {
    -            val args = Bundle()
    -            args.putParcelable(MvRx.KEY_ARG, pa)
    -            val fragment = MessageMenuFragment()
    -            fragment.arguments = args
    -            return fragment
    -        }
    -    }
    -}
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuViewModel.kt
    deleted file mode 100644
    index 14d730044a..0000000000
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuViewModel.kt
    +++ /dev/null
    @@ -1,279 +0,0 @@
    -/*
    - * Copyright 2019 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.riotx.features.home.room.detail.timeline.action
    -
    -import androidx.annotation.DrawableRes
    -import androidx.annotation.StringRes
    -import com.airbnb.mvrx.*
    -import com.squareup.inject.assisted.Assisted
    -import com.squareup.inject.assisted.AssistedInject
    -import im.vector.matrix.android.api.session.Session
    -import im.vector.matrix.android.api.session.events.model.EventType
    -import im.vector.matrix.android.api.session.events.model.isTextMessage
    -import im.vector.matrix.android.api.session.events.model.toModel
    -import im.vector.matrix.android.api.session.room.model.message.MessageContent
    -import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
    -import im.vector.matrix.android.api.session.room.model.message.MessageType
    -import im.vector.matrix.android.api.session.room.send.SendState
    -import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
    -import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited
    -import im.vector.matrix.android.api.util.Optional
    -import im.vector.matrix.rx.RxRoom
    -import im.vector.riotx.R
    -import im.vector.riotx.core.extensions.canReact
    -import im.vector.riotx.core.platform.VectorViewModel
    -import im.vector.riotx.core.resources.StringProvider
    -import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
    -
    -sealed class SimpleAction(@StringRes val titleRes: Int, @DrawableRes val iconResId: Int) {
    -    data class AddReaction(val eventId: String) : SimpleAction(R.string.message_add_reaction, R.drawable.ic_add_reaction)
    -    data class Copy(val content: String) : SimpleAction(R.string.copy, R.drawable.ic_copy)
    -    data class Edit(val eventId: String) : SimpleAction(R.string.edit, R.drawable.ic_edit)
    -    data class Quote(val eventId: String) : SimpleAction(R.string.quote, R.drawable.ic_quote)
    -    data class Reply(val eventId: String) : SimpleAction(R.string.reply, R.drawable.ic_reply)
    -    data class Share(val imageUrl: String) : SimpleAction(R.string.share, R.drawable.ic_share)
    -    data class Resend(val eventId: String) : SimpleAction(R.string.global_retry, R.drawable.ic_refresh_cw)
    -    data class Remove(val eventId: String) : SimpleAction(R.string.remove, R.drawable.ic_trash)
    -    data class Delete(val eventId: String) : SimpleAction(R.string.delete, R.drawable.ic_delete)
    -    data class Cancel(val eventId: String) : SimpleAction(R.string.cancel, R.drawable.ic_close_round)
    -    data class ViewSource(val content: String) : SimpleAction(R.string.view_source, R.drawable.ic_view_source)
    -    data class ViewDecryptedSource(val content: String) : SimpleAction(R.string.view_decrypted_source, R.drawable.ic_view_source)
    -    data class CopyPermalink(val eventId: String) : SimpleAction(R.string.permalink, R.drawable.ic_permalink)
    -    data class Flag(val eventId: String) : SimpleAction(R.string.report_content, R.drawable.ic_flag)
    -    data class QuickReact(val eventId: String, val clickedOn: String, val add: Boolean) : SimpleAction(0, 0)
    -    data class ViewReactions(val messageInformationData: MessageInformationData) : SimpleAction(R.string.message_view_reaction, R.drawable.ic_view_reactions)
    -    data class ViewEditHistory(val messageInformationData: MessageInformationData) :
    -            SimpleAction(R.string.message_view_edit_history, R.drawable.ic_view_edit_history)
    -}
    -
    -data class MessageMenuState(
    -        val roomId: String,
    -        val eventId: String,
    -        val informationData: MessageInformationData,
    -        val actions: Async> = Uninitialized
    -) : MvRxState {
    -
    -    constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData)
    -}
    -
    -/**
    - * Manages list actions for a given message (copy / paste / forward...)
    - */
    -class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: MessageMenuState,
    -                                                       private val session: Session,
    -                                                       private val stringProvider: StringProvider) : VectorViewModel(initialState) {
    -
    -    @AssistedInject.Factory
    -    interface Factory {
    -        fun create(initialState: MessageMenuState): MessageMenuViewModel
    -    }
    -
    -    private val room = session.getRoom(initialState.roomId)
    -            ?: throw IllegalStateException("Shouldn't use this ViewModel without a room")
    -
    -    private val eventId = initialState.eventId
    -    private val informationData: MessageInformationData = initialState.informationData
    -
    -    companion object : MvRxViewModelFactory {
    -        override fun create(viewModelContext: ViewModelContext, state: MessageMenuState): MessageMenuViewModel? {
    -            val fragment: MessageMenuFragment = (viewModelContext as FragmentViewModelContext).fragment()
    -            return fragment.messageMenuViewModelFactory.create(state)
    -        }
    -    }
    -
    -    init {
    -        observeEvent()
    -    }
    -
    -    private fun observeEvent() {
    -        RxRoom(room)
    -                .liveTimelineEvent(eventId)
    -                .map {
    -                    actionsForEvent(it)
    -                }
    -                .execute {
    -                    copy(actions = it)
    -                }
    -    }
    -
    -    private fun actionsForEvent(optionalEvent: Optional): List {
    -        val event = optionalEvent.getOrNull() ?: return emptyList()
    -
    -        val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent.toModel()
    -                ?: event.root.getClearContent().toModel()
    -        val type = messageContent?.type
    -
    -        return arrayListOf().apply {
    -            if (event.root.sendState.hasFailed()) {
    -                if (canRetry(event)) {
    -                    add(SimpleAction.Resend(eventId))
    -                }
    -                add(SimpleAction.Remove(eventId))
    -            } else if (event.root.sendState.isSending()) {
    -                // TODO is uploading attachment?
    -                if (canCancel(event)) {
    -                    add(SimpleAction.Cancel(eventId))
    -                }
    -            } else {
    -                if (!event.root.isRedacted()) {
    -                    if (canReply(event, messageContent)) {
    -                        add(SimpleAction.Reply(eventId))
    -                    }
    -
    -                    if (canEdit(event, session.myUserId)) {
    -                        add(SimpleAction.Edit(eventId))
    -                    }
    -
    -                    if (canRedact(event, session.myUserId)) {
    -                        add(SimpleAction.Delete(eventId))
    -                    }
    -
    -                    if (canCopy(type)) {
    -                        // TODO copy images? html? see ClipBoard
    -                        add(SimpleAction.Copy(messageContent!!.body))
    -                    }
    -
    -                    if (event.canReact()) {
    -                        add(SimpleAction.AddReaction(eventId))
    -                    }
    -
    -                    if (canQuote(event, messageContent)) {
    -                        add(SimpleAction.Quote(eventId))
    -                    }
    -
    -                    if (canViewReactions(event)) {
    -                        add(SimpleAction.ViewReactions(informationData))
    -                    }
    -
    -                    if (event.hasBeenEdited()) {
    -                        add(SimpleAction.ViewEditHistory(informationData))
    -                    }
    -
    -                    if (canShare(type)) {
    -                        if (messageContent is MessageImageContent) {
    -                            session.contentUrlResolver().resolveFullSize(messageContent.url)?.let { url ->
    -                                add(SimpleAction.Share(url))
    -                            }
    -                        }
    -                        // TODO
    -                    }
    -
    -                    if (event.root.sendState == SendState.SENT) {
    -                        // TODO Can be redacted
    -
    -                        // TODO sent by me or sufficient power level
    -                    }
    -                }
    -
    -                add(SimpleAction.ViewSource(event.root.toContentStringWithIndent()))
    -                if (event.isEncrypted()) {
    -                    val decryptedContent = event.root.toClearContentStringWithIndent()
    -                            ?: stringProvider.getString(R.string.encryption_information_decryption_error)
    -                    add(SimpleAction.ViewDecryptedSource(decryptedContent))
    -                }
    -                add(SimpleAction.CopyPermalink(eventId))
    -
    -                if (session.myUserId != event.root.senderId && event.root.getClearType() == EventType.MESSAGE) {
    -                    // not sent by me
    -                    add(SimpleAction.Flag(eventId))
    -                }
    -            }
    -        }
    -    }
    -
    -    private fun canCancel(@Suppress("UNUSED_PARAMETER") event: TimelineEvent): Boolean {
    -        return false
    -    }
    -
    -    private fun canReply(event: TimelineEvent, messageContent: MessageContent?): Boolean {
    -        // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
    -        if (event.root.getClearType() != EventType.MESSAGE) return false
    -        return when (messageContent?.type) {
    -            MessageType.MSGTYPE_TEXT,
    -            MessageType.MSGTYPE_NOTICE,
    -            MessageType.MSGTYPE_EMOTE,
    -            MessageType.MSGTYPE_IMAGE,
    -            MessageType.MSGTYPE_VIDEO,
    -            MessageType.MSGTYPE_AUDIO,
    -            MessageType.MSGTYPE_FILE -> true
    -            else                     -> false
    -        }
    -    }
    -
    -    private fun canQuote(event: TimelineEvent, messageContent: MessageContent?): Boolean {
    -        // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
    -        if (event.root.getClearType() != EventType.MESSAGE) return false
    -        return when (messageContent?.type) {
    -            MessageType.MSGTYPE_TEXT,
    -            MessageType.MSGTYPE_NOTICE,
    -            MessageType.MSGTYPE_EMOTE,
    -            MessageType.FORMAT_MATRIX_HTML,
    -            MessageType.MSGTYPE_LOCATION -> {
    -                true
    -            }
    -            else                         -> false
    -        }
    -    }
    -
    -    private fun canRedact(event: TimelineEvent, myUserId: String): Boolean {
    -        // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
    -        if (event.root.getClearType() != EventType.MESSAGE) return false
    -        // TODO if user is admin or moderator
    -        return event.root.senderId == myUserId
    -    }
    -
    -    private fun canRetry(event: TimelineEvent): Boolean {
    -        return event.root.sendState.hasFailed() && event.root.isTextMessage()
    -    }
    -
    -    private fun canViewReactions(event: TimelineEvent): Boolean {
    -        // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
    -        if (event.root.getClearType() != EventType.MESSAGE) return false
    -        // TODO if user is admin or moderator
    -        return event.annotations?.reactionsSummary?.isNotEmpty() ?: false
    -    }
    -
    -    private fun canEdit(event: TimelineEvent, myUserId: String): Boolean {
    -        // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
    -        if (event.root.getClearType() != EventType.MESSAGE) return false
    -        // TODO if user is admin or moderator
    -        val messageContent = event.root.getClearContent().toModel()
    -        return event.root.senderId == myUserId && (
    -                messageContent?.type == MessageType.MSGTYPE_TEXT
    -                        || messageContent?.type == MessageType.MSGTYPE_EMOTE
    -                )
    -    }
    -
    -    private fun canCopy(type: String?): Boolean {
    -        return when (type) {
    -            MessageType.MSGTYPE_TEXT,
    -            MessageType.MSGTYPE_NOTICE,
    -            MessageType.MSGTYPE_EMOTE,
    -            MessageType.FORMAT_MATRIX_HTML,
    -            MessageType.MSGTYPE_LOCATION -> true
    -            else                         -> false
    -        }
    -    }
    -
    -    private fun canShare(type: String?): Boolean {
    -        return when (type) {
    -            MessageType.MSGTYPE_IMAGE,
    -            MessageType.MSGTYPE_AUDIO,
    -            MessageType.MSGTYPE_VIDEO -> true
    -            else                      -> false
    -        }
    -    }
    -}
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/QuickReactionFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/QuickReactionFragment.kt
    deleted file mode 100644
    index cabb4c113f..0000000000
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/QuickReactionFragment.kt
    +++ /dev/null
    @@ -1,89 +0,0 @@
    -/*
    - * Copyright 2019 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.riotx.features.home.room.detail.timeline.action
    -
    -import android.graphics.Typeface
    -import android.os.Bundle
    -import android.view.View
    -import android.widget.TextView
    -import com.airbnb.mvrx.MvRx
    -import com.airbnb.mvrx.fragmentViewModel
    -import com.airbnb.mvrx.withState
    -import im.vector.riotx.EmojiCompatFontProvider
    -import im.vector.riotx.R
    -import im.vector.riotx.core.di.ScreenComponent
    -import im.vector.riotx.core.platform.VectorBaseFragment
    -import kotlinx.android.synthetic.main.adapter_item_action_quick_reaction.*
    -import javax.inject.Inject
    -
    -/**
    - * Quick Reaction Fragment (agree / like reactions)
    - */
    -class QuickReactionFragment : VectorBaseFragment() {
    -
    -    private val viewModel: QuickReactionViewModel by fragmentViewModel(QuickReactionViewModel::class)
    -
    -    var interactionListener: InteractionListener? = null
    -
    -    @Inject lateinit var fontProvider: EmojiCompatFontProvider
    -    @Inject lateinit var quickReactionViewModelFactory: QuickReactionViewModel.Factory
    -
    -    override fun getLayoutResId() = R.layout.adapter_item_action_quick_reaction
    -
    -    override fun injectWith(injector: ScreenComponent) {
    -        injector.inject(this)
    -    }
    -
    -    private lateinit var textViews: List
    -
    -    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    -        super.onViewCreated(view, savedInstanceState)
    -        textViews = listOf(quickReaction0, quickReaction1, quickReaction2, quickReaction3,
    -                quickReaction4, quickReaction5, quickReaction6, quickReaction7)
    -        textViews.forEachIndexed { index, textView ->
    -            textView.typeface = fontProvider.typeface ?: Typeface.DEFAULT
    -            textView.setOnClickListener {
    -                viewModel.didSelect(index)
    -            }
    -        }
    -    }
    -
    -    override fun invalidate() = withState(viewModel) {
    -        val quickReactionsStates = it.quickStates() ?: return@withState
    -        quickReactionsStates.forEachIndexed { index, qs ->
    -            textViews[index].text = qs.reaction
    -            textViews[index].alpha = if (qs.isSelected) 0.2f else 1f
    -        }
    -
    -        if (it.result != null) {
    -            interactionListener?.didQuickReactWith(it.result.reaction, it.result.isSelected, it.eventId)
    -        }
    -    }
    -
    -    interface InteractionListener {
    -        fun didQuickReactWith(clickedOn: String, add: Boolean, eventId: String)
    -    }
    -
    -    companion object {
    -        fun newInstance(pa: TimelineEventFragmentArgs): QuickReactionFragment {
    -            val args = Bundle()
    -            args.putParcelable(MvRx.KEY_ARG, pa)
    -            val fragment = QuickReactionFragment()
    -            fragment.arguments = args
    -            return fragment
    -        }
    -    }
    -}
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/QuickReactionViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/QuickReactionViewModel.kt
    deleted file mode 100644
    index edcfd8e28c..0000000000
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/QuickReactionViewModel.kt
    +++ /dev/null
    @@ -1,96 +0,0 @@
    -/*
    - * Copyright 2019 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.riotx.features.home.room.detail.timeline.action
    -
    -import com.airbnb.mvrx.*
    -import com.squareup.inject.assisted.Assisted
    -import com.squareup.inject.assisted.AssistedInject
    -import im.vector.matrix.android.api.session.Session
    -import im.vector.matrix.rx.RxRoom
    -import im.vector.riotx.core.platform.VectorViewModel
    -import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
    -
    -/**
    - * Quick reactions state, it's a toggle with 3rd state
    - */
    -data class ToggleState(
    -        val reaction: String,
    -        val isSelected: Boolean
    -)
    -
    -data class QuickReactionState(
    -        val roomId: String,
    -        val eventId: String,
    -        val informationData: MessageInformationData,
    -        val quickStates: Async> = Uninitialized,
    -        val result: ToggleState? = null
    -        /** Pair of 'clickedOn' and current toggles state*/
    -) : MvRxState {
    -
    -    constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData)
    -}
    -
    -/**
    - * Quick reaction view model
    - */
    -class QuickReactionViewModel @AssistedInject constructor(@Assisted initialState: QuickReactionState,
    -                                                         private val session: Session) : VectorViewModel(initialState) {
    -
    -    @AssistedInject.Factory
    -    interface Factory {
    -        fun create(initialState: QuickReactionState): QuickReactionViewModel
    -    }
    -
    -    private val room = session.getRoom(initialState.roomId)
    -    private val eventId = initialState.eventId
    -
    -    companion object : MvRxViewModelFactory {
    -
    -        val quickEmojis = listOf("👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀")
    -
    -        override fun create(viewModelContext: ViewModelContext, state: QuickReactionState): QuickReactionViewModel? {
    -            val fragment: QuickReactionFragment = (viewModelContext as FragmentViewModelContext).fragment()
    -            return fragment.quickReactionViewModelFactory.create(state)
    -        }
    -    }
    -
    -    init {
    -        observeReactions()
    -    }
    -
    -    private fun observeReactions() {
    -        if (room == null) return
    -        RxRoom(room)
    -                .liveAnnotationSummary(eventId)
    -                .map { annotations ->
    -                    quickEmojis.map { emoji ->
    -                        ToggleState(emoji, annotations.getOrNull()?.reactionsSummary?.firstOrNull { it.key == emoji }?.addedByMe
    -                                ?: false)
    -                    }
    -                }
    -                .execute {
    -                    copy(quickStates = it)
    -                }
    -    }
    -
    -    fun didSelect(index: Int) = withState {
    -        val selectedReaction = it.quickStates()?.get(index) ?: return@withState
    -        val isSelected = selectedReaction.isSelected
    -        setState {
    -            copy(result = ToggleState(selectedReaction.reaction, !isSelected))
    -        }
    -    }
    -}
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/SimpleAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/SimpleAction.kt
    new file mode 100644
    index 0000000000..9ba1bbb212
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/SimpleAction.kt
    @@ -0,0 +1,43 @@
    +/*
    + * Copyright 2019 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.riotx.features.home.room.detail.timeline.action
    +
    +import androidx.annotation.DrawableRes
    +import androidx.annotation.StringRes
    +import im.vector.riotx.R
    +import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
    +
    +sealed class SimpleAction(@StringRes val titleRes: Int, @DrawableRes val iconResId: Int) {
    +    data class AddReaction(val eventId: String) : SimpleAction(R.string.message_add_reaction, R.drawable.ic_add_reaction)
    +    data class Copy(val content: String) : SimpleAction(R.string.copy, R.drawable.ic_copy)
    +    data class Edit(val eventId: String) : SimpleAction(R.string.edit, R.drawable.ic_edit)
    +    data class Quote(val eventId: String) : SimpleAction(R.string.quote, R.drawable.ic_quote)
    +    data class Reply(val eventId: String) : SimpleAction(R.string.reply, R.drawable.ic_reply)
    +    data class Share(val imageUrl: String) : SimpleAction(R.string.share, R.drawable.ic_share)
    +    data class Resend(val eventId: String) : SimpleAction(R.string.global_retry, R.drawable.ic_refresh_cw)
    +    data class Remove(val eventId: String) : SimpleAction(R.string.remove, R.drawable.ic_trash)
    +    data class Delete(val eventId: String) : SimpleAction(R.string.delete, R.drawable.ic_delete)
    +    data class Cancel(val eventId: String) : SimpleAction(R.string.cancel, R.drawable.ic_close_round)
    +    data class ViewSource(val content: String) : SimpleAction(R.string.view_source, R.drawable.ic_view_source)
    +    data class ViewDecryptedSource(val content: String) : SimpleAction(R.string.view_decrypted_source, R.drawable.ic_view_source)
    +    data class CopyPermalink(val eventId: String) : SimpleAction(R.string.permalink, R.drawable.ic_permalink)
    +    data class Flag(val eventId: String) : SimpleAction(R.string.report_content, R.drawable.ic_flag)
    +    data class QuickReact(val eventId: String, val clickedOn: String, val add: Boolean) : SimpleAction(0, 0)
    +    data class ViewReactions(val messageInformationData: MessageInformationData) : SimpleAction(R.string.message_view_reaction, R.drawable.ic_view_reactions)
    +    data class ViewEditHistory(val messageInformationData: MessageInformationData) :
    +            SimpleAction(R.string.message_view_edit_history, R.drawable.ic_view_edit_history)
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt
    similarity index 93%
    rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryBottomSheet.kt
    rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt
    index 5fefb36e29..acdef2058d 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryBottomSheet.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt
    @@ -13,7 +13,7 @@
      * See the License for the specific language governing permissions and
      * limitations under the License.
      */
    -package im.vector.riotx.features.home.room.detail.timeline.action
    +package im.vector.riotx.features.home.room.detail.timeline.edithistory
     
     import android.os.Bundle
     import android.view.LayoutInflater
    @@ -29,6 +29,8 @@ import com.airbnb.mvrx.fragmentViewModel
     import com.airbnb.mvrx.withState
     import im.vector.riotx.R
     import im.vector.riotx.core.di.ScreenComponent
    +import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
    +import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs
     import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
     import im.vector.riotx.features.html.EventHtmlRenderer
     import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.*
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt
    similarity index 98%
    rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryEpoxyController.kt
    rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt
    index 288f001651..d36e98f67c 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryEpoxyController.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt
    @@ -13,7 +13,7 @@
      * See the License for the specific language governing permissions and
      * limitations under the License.
      */
    -package im.vector.riotx.features.home.room.detail.timeline.action
    +package im.vector.riotx.features.home.room.detail.timeline.edithistory
     
     import android.content.Context
     import android.text.Spannable
    @@ -41,7 +41,7 @@ import timber.log.Timber
     import java.util.*
     
     /**
    - * Epoxy controller for reaction event list
    + * Epoxy controller for edit history list
      */
     class ViewEditHistoryEpoxyController(private val context: Context,
                                          val dateFormatter: VectorDateFormatter,
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt
    similarity index 96%
    rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryViewModel.kt
    rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt
    index 890fbe60e5..e2b976b273 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt
    @@ -13,7 +13,7 @@
      * See the License for the specific language governing permissions and
      * limitations under the License.
      */
    -package im.vector.riotx.features.home.room.detail.timeline.action
    +package im.vector.riotx.features.home.room.detail.timeline.edithistory
     
     import com.airbnb.mvrx.*
     import com.squareup.inject.assisted.Assisted
    @@ -28,6 +28,7 @@ import im.vector.matrix.android.api.session.room.model.message.isReply
     import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
     import im.vector.riotx.core.platform.VectorViewModel
     import im.vector.riotx.core.date.VectorDateFormatter
    +import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs
     import timber.log.Timber
     import java.util.*
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ReactionInfoSimpleItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ReactionInfoSimpleItem.kt
    similarity index 96%
    rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ReactionInfoSimpleItem.kt
    rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ReactionInfoSimpleItem.kt
    index e1d03d93fc..39392324aa 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ReactionInfoSimpleItem.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ReactionInfoSimpleItem.kt
    @@ -14,7 +14,7 @@
      * limitations under the License.
      */
     
    -package im.vector.riotx.features.home.room.detail.timeline.action
    +package im.vector.riotx.features.home.room.detail.timeline.reactions
     
     import android.widget.TextView
     import androidx.core.view.isVisible
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsBottomSheet.kt
    similarity index 82%
    rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionBottomSheet.kt
    rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsBottomSheet.kt
    index b4eba4bbec..deb2a84818 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionBottomSheet.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsBottomSheet.kt
    @@ -14,7 +14,7 @@
      * limitations under the License.
      */
     
    -package im.vector.riotx.features.home.room.detail.timeline.action
    +package im.vector.riotx.features.home.room.detail.timeline.reactions
     
     import android.os.Bundle
     import android.view.LayoutInflater
    @@ -30,6 +30,8 @@ import com.airbnb.mvrx.fragmentViewModel
     import com.airbnb.mvrx.withState
     import im.vector.riotx.R
     import im.vector.riotx.core.di.ScreenComponent
    +import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
    +import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs
     import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
     import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.*
     import javax.inject.Inject
    @@ -37,11 +39,11 @@ import javax.inject.Inject
     /**
      * Bottom sheet displaying list of reactions for a given event ordered by timestamp
      */
    -class ViewReactionBottomSheet : VectorBaseBottomSheetDialogFragment() {
    +class ViewReactionsBottomSheet : VectorBaseBottomSheetDialogFragment() {
     
    -    private val viewModel: ViewReactionViewModel by fragmentViewModel(ViewReactionViewModel::class)
    +    private val viewModel: ViewReactionsViewModel by fragmentViewModel(ViewReactionsViewModel::class)
     
    -    @Inject lateinit var viewReactionViewModelFactory: ViewReactionViewModel.Factory
    +    @Inject lateinit var viewReactionsViewModelFactory: ViewReactionsViewModel.Factory
     
         @BindView(R.id.bottom_sheet_display_reactions_list)
         lateinit var epoxyRecyclerView: EpoxyRecyclerView
    @@ -72,7 +74,7 @@ class ViewReactionBottomSheet : VectorBaseBottomSheetDialogFragment() {
         }
     
         companion object {
    -        fun newInstance(roomId: String, informationData: MessageInformationData): ViewReactionBottomSheet {
    +        fun newInstance(roomId: String, informationData: MessageInformationData): ViewReactionsBottomSheet {
                 val args = Bundle()
                 val parcelableArgs = TimelineEventFragmentArgs(
                         informationData.eventId,
    @@ -80,7 +82,7 @@ class ViewReactionBottomSheet : VectorBaseBottomSheetDialogFragment() {
                         informationData
                 )
                 args.putParcelable(MvRx.KEY_ARG, parcelableArgs)
    -            return ViewReactionBottomSheet().apply { arguments = args }
    +            return ViewReactionsBottomSheet().apply { arguments = args }
             }
         }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionsEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsEpoxyController.kt
    similarity index 96%
    rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionsEpoxyController.kt
    rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsEpoxyController.kt
    index 6b1c099261..7fd2edcbfe 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionsEpoxyController.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsEpoxyController.kt
    @@ -14,7 +14,7 @@
      * limitations under the License.
      */
     
    -package im.vector.riotx.features.home.room.detail.timeline.action
    +package im.vector.riotx.features.home.room.detail.timeline.reactions
     
     import com.airbnb.epoxy.TypedEpoxyController
     import com.airbnb.mvrx.Fail
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt
    similarity index 83%
    rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionViewModel.kt
    rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt
    index a3611edd87..208e126022 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt
    @@ -14,7 +14,7 @@
      * limitations under the License.
      */
     
    -package im.vector.riotx.features.home.room.detail.timeline.action
    +package im.vector.riotx.features.home.room.detail.timeline.reactions
     
     import com.airbnb.mvrx.Async
     import com.airbnb.mvrx.FragmentViewModelContext
    @@ -30,6 +30,7 @@ import im.vector.matrix.rx.RxRoom
     import im.vector.matrix.rx.unwrap
     import im.vector.riotx.core.platform.VectorViewModel
     import im.vector.riotx.core.date.VectorDateFormatter
    +import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs
     import io.reactivex.Observable
     import io.reactivex.Single
     
    @@ -53,10 +54,10 @@ data class ReactionInfo(
     /**
      * Used to display the list of members that reacted to a given event
      */
    -class ViewReactionViewModel @AssistedInject constructor(@Assisted
    +class ViewReactionsViewModel @AssistedInject constructor(@Assisted
                                                             initialState: DisplayReactionsViewState,
    -                                                        private val session: Session,
    -                                                        private val dateFormatter: VectorDateFormatter
    +                                                         private val session: Session,
    +                                                         private val dateFormatter: VectorDateFormatter
     ) : VectorViewModel(initialState) {
     
         private val roomId = initialState.roomId
    @@ -66,14 +67,14 @@ class ViewReactionViewModel @AssistedInject constructor(@Assisted
     
         @AssistedInject.Factory
         interface Factory {
    -        fun create(initialState: DisplayReactionsViewState): ViewReactionViewModel
    +        fun create(initialState: DisplayReactionsViewState): ViewReactionsViewModel
         }
     
    -    companion object : MvRxViewModelFactory {
    +    companion object : MvRxViewModelFactory {
     
    -        override fun create(viewModelContext: ViewModelContext, state: DisplayReactionsViewState): ViewReactionViewModel? {
    -            val fragment: ViewReactionBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
    -            return fragment.viewReactionViewModelFactory.create(state)
    +        override fun create(viewModelContext: ViewModelContext, state: DisplayReactionsViewState): ViewReactionsViewModel? {
    +            val fragment: ViewReactionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
    +            return fragment.viewReactionsViewModelFactory.create(state)
             }
         }
     
    diff --git a/vector/src/main/res/layout/bottom_sheet_epoxylist_with_title.xml b/vector/src/main/res/layout/bottom_sheet_epoxylist_with_title.xml
    index 9b3ffb26a3..0cc2e16ed9 100644
    --- a/vector/src/main/res/layout/bottom_sheet_epoxylist_with_title.xml
    +++ b/vector/src/main/res/layout/bottom_sheet_epoxylist_with_title.xml
    @@ -25,7 +25,6 @@
             android:orientation="vertical"
             android:scrollbars="vertical"
             tools:itemCount="15"
    -        tools:listitem="@layout/item_simple_reaction_info">
    +        tools:listitem="@layout/item_simple_reaction_info" />
     
    -    
     
    \ No newline at end of file
    diff --git a/vector/src/main/res/layout/bottom_sheet_generic_recycler_epoxy.xml b/vector/src/main/res/layout/bottom_sheet_generic_recycler_epoxy.xml
    new file mode 100644
    index 0000000000..5ca952272f
    --- /dev/null
    +++ b/vector/src/main/res/layout/bottom_sheet_generic_recycler_epoxy.xml
    @@ -0,0 +1,22 @@
    +
    +
    +
    +
    +
    +    
    +
    +
    \ No newline at end of file
    diff --git a/vector/src/main/res/layout/bottom_sheet_message_actions.xml b/vector/src/main/res/layout/bottom_sheet_message_actions.xml
    deleted file mode 100644
    index c7d4f5ac8e..0000000000
    --- a/vector/src/main/res/layout/bottom_sheet_message_actions.xml
    +++ /dev/null
    @@ -1,149 +0,0 @@
    -
    -
    -
    -    
    -
    -        
    -
    -            
    -
    -            
    -
    -            
    -
    -            
    -        
    -
    -        
    -
    -            
    -
    -            
    -
    -        
    -
    -        
    -
    -        
    -
    -        
    -
    -        
    -
    -    
    -
    diff --git a/vector/src/main/res/layout/fragment_message_menu.xml b/vector/src/main/res/layout/fragment_message_menu.xml
    deleted file mode 100644
    index 4538ac935c..0000000000
    --- a/vector/src/main/res/layout/fragment_message_menu.xml
    +++ /dev/null
    @@ -1,5 +0,0 @@
    -
    -
    -
    \ No newline at end of file
    diff --git a/vector/src/main/res/layout/adapter_item_action.xml b/vector/src/main/res/layout/item_bottom_sheet_action.xml
    similarity index 97%
    rename from vector/src/main/res/layout/adapter_item_action.xml
    rename to vector/src/main/res/layout/item_bottom_sheet_action.xml
    index 03d2f81115..44145e7bf5 100644
    --- a/vector/src/main/res/layout/adapter_item_action.xml
    +++ b/vector/src/main/res/layout/item_bottom_sheet_action.xml
    @@ -2,7 +2,7 @@
     
    +
    diff --git a/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml b/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml
    new file mode 100644
    index 0000000000..a688f38d0f
    --- /dev/null
    +++ b/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml
    @@ -0,0 +1,78 @@
    +
    +
    +
    +    
    +
    +    
    +
    +    
    +
    +    
    +
    diff --git a/vector/src/main/res/layout/item_bottom_sheet_message_status.xml b/vector/src/main/res/layout/item_bottom_sheet_message_status.xml
    new file mode 100644
    index 0000000000..10c129cf58
    --- /dev/null
    +++ b/vector/src/main/res/layout/item_bottom_sheet_message_status.xml
    @@ -0,0 +1,33 @@
    +
    +
    +
    +    
    +
    +    
    +
    +
    diff --git a/vector/src/main/res/layout/adapter_item_action_quick_reaction.xml b/vector/src/main/res/layout/item_bottom_sheet_quick_reaction.xml
    similarity index 100%
    rename from vector/src/main/res/layout/adapter_item_action_quick_reaction.xml
    rename to vector/src/main/res/layout/item_bottom_sheet_quick_reaction.xml
    
    From 0a79b8b315cb202d3090b885d8f107d372768aa8 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Thu, 10 Oct 2019 15:29:31 +0200
    Subject: [PATCH 149/197] Cleanup
    
    ---
     .../action/MessageActionsBottomSheet.kt       | 25 +------------------
     .../action/MessageActionsEpoxyController.kt   | 15 +++++------
     .../bottom_sheet_epoxylist_with_title.xml     |  1 -
     .../bottom_sheet_generic_recycler_epoxy.xml   |  1 -
     .../res/layout/item_bottom_sheet_action.xml   |  3 +--
     5 files changed, 10 insertions(+), 35 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
    index 39116b59b5..53ac1bd430 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
    @@ -15,22 +15,16 @@
      */
     package im.vector.riotx.features.home.room.detail.timeline.action
     
    -import android.app.Dialog
     import android.os.Bundle
     import android.view.LayoutInflater
     import android.view.View
     import android.view.ViewGroup
    -import android.widget.FrameLayout
     import androidx.lifecycle.ViewModelProviders
     import com.airbnb.mvrx.fragmentViewModel
     import com.airbnb.mvrx.withState
    -import com.google.android.material.bottomsheet.BottomSheetBehavior
    -import com.google.android.material.bottomsheet.BottomSheetDialog
    -import im.vector.riotx.EmojiCompatFontProvider
     import im.vector.riotx.R
     import im.vector.riotx.core.di.ScreenComponent
     import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
    -import im.vector.riotx.features.home.AvatarRenderer
     import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
     import kotlinx.android.synthetic.main.bottom_sheet_generic_recycler_epoxy.*
     import javax.inject.Inject
    @@ -40,12 +34,10 @@ import javax.inject.Inject
      */
     class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), MessageActionsEpoxyController.MessageActionsEpoxyControllerListener {
         @Inject lateinit var messageActionViewModelFactory: MessageActionsViewModel.Factory
    -    @Inject lateinit var avatarRenderer: AvatarRenderer
    -    @Inject lateinit var fontProvider: EmojiCompatFontProvider
    +    @Inject lateinit var messageActionsEpoxyController: MessageActionsEpoxyController
     
         private val viewModel: MessageActionsViewModel by fragmentViewModel(MessageActionsViewModel::class)
     
    -    private lateinit var messageActionsEpoxyController: MessageActionsEpoxyController
         private lateinit var actionHandlerModel: ActionsHandler
     
         override fun injectWith(screenComponent: ScreenComponent) {
    @@ -60,7 +52,6 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), Message
             super.onActivityCreated(savedInstanceState)
             actionHandlerModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java)
     
    -        messageActionsEpoxyController = MessageActionsEpoxyController(requireContext(), avatarRenderer, fontProvider)
             bottomSheetEpoxyRecyclerView.setController(messageActionsEpoxyController)
             messageActionsEpoxyController.listener = this
         }
    @@ -70,20 +61,6 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), Message
             dismiss()
         }
     
    -    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
    -        val dialog = super.onCreateDialog(savedInstanceState)
    -        // We want to force the bottom sheet initial state to expanded
    -        (dialog as? BottomSheetDialog)?.let { bottomSheetDialog ->
    -            bottomSheetDialog.setOnShowListener { dialog ->
    -                val d = dialog as BottomSheetDialog
    -                (d.findViewById(com.google.android.material.R.id.design_bottom_sheet) as? FrameLayout)?.let {
    -                    BottomSheetBehavior.from(it).state = BottomSheetBehavior.STATE_COLLAPSED
    -                }
    -            }
    -        }
    -        return dialog
    -    }
    -
         override fun invalidate() = withState(viewModel) {
             messageActionsEpoxyController.setData(it)
         }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt
    index 1de7302820..66c1949631 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt
    @@ -15,20 +15,21 @@
      */
     package im.vector.riotx.features.home.room.detail.timeline.action
     
    -import android.content.Context
     import android.view.View
     import com.airbnb.epoxy.TypedEpoxyController
     import com.airbnb.mvrx.Success
     import im.vector.riotx.EmojiCompatFontProvider
     import im.vector.riotx.R
    +import im.vector.riotx.core.resources.StringProvider
     import im.vector.riotx.features.home.AvatarRenderer
    +import javax.inject.Inject
     
     /**
      * Epoxy controller for message action list
      */
    -class MessageActionsEpoxyController(private val context: Context,
    -                                    private val avatarRenderer: AvatarRenderer,
    -                                    private val fontProvider: EmojiCompatFontProvider) : TypedEpoxyController() {
    +class MessageActionsEpoxyController @Inject constructor(private val stringProvider: StringProvider,
    +                                                        private val avatarRenderer: AvatarRenderer,
    +                                                        private val fontProvider: EmojiCompatFontProvider) : TypedEpoxyController() {
     
         var listener: MessageActionsEpoxyControllerListener? = null
     
    @@ -51,13 +52,13 @@ class MessageActionsEpoxyController(private val context: Context,
                 bottomSheetItemSendState {
                     id("send_state")
                     showProgress(true)
    -                text(context.getString(R.string.event_status_sending_message))
    +                text(stringProvider.getString(R.string.event_status_sending_message))
                 }
             } else if (state.informationData.sendState.hasFailed()) {
                 bottomSheetItemSendState {
                     id("send_state")
                     showProgress(false)
    -                text(context.getString(R.string.unable_to_send_message))
    +                text(stringProvider.getString(R.string.unable_to_send_message))
                     drawableStart(R.drawable.ic_warning_small)
                 }
             }
    @@ -73,7 +74,7 @@ class MessageActionsEpoxyController(private val context: Context,
                     id("quick_reaction")
                     fontProvider(fontProvider)
                     texts(state.quickStates()?.map { it.reaction }.orEmpty())
    -                selecteds(state.quickStates()?.map { it.isSelected }.orEmpty())
    +                selecteds(state.quickStates.invoke().map { it.isSelected })
                     listener(object : BottomSheetItemQuickReactions.Listener {
                         override fun didSelect(emoji: String, selected: Boolean) {
                             listener?.didSelectMenuAction(SimpleAction.QuickReact(state.eventId, emoji, selected))
    diff --git a/vector/src/main/res/layout/bottom_sheet_epoxylist_with_title.xml b/vector/src/main/res/layout/bottom_sheet_epoxylist_with_title.xml
    index 0cc2e16ed9..a5e9c980a3 100644
    --- a/vector/src/main/res/layout/bottom_sheet_epoxylist_with_title.xml
    +++ b/vector/src/main/res/layout/bottom_sheet_epoxylist_with_title.xml
    @@ -22,7 +22,6 @@
             android:layout_height="0dp"
             android:layout_weight="1"
             android:fadeScrollbars="false"
    -        android:orientation="vertical"
             android:scrollbars="vertical"
             tools:itemCount="15"
             tools:listitem="@layout/item_simple_reaction_info" />
    diff --git a/vector/src/main/res/layout/bottom_sheet_generic_recycler_epoxy.xml b/vector/src/main/res/layout/bottom_sheet_generic_recycler_epoxy.xml
    index 5ca952272f..55d3492cf9 100644
    --- a/vector/src/main/res/layout/bottom_sheet_generic_recycler_epoxy.xml
    +++ b/vector/src/main/res/layout/bottom_sheet_generic_recycler_epoxy.xml
    @@ -14,7 +14,6 @@
             android:layout_height="0dp"
             android:layout_weight="1"
             android:fadeScrollbars="false"
    -        android:orientation="vertical"
             android:scrollbars="vertical"
             tools:itemCount="5"
             tools:listitem="@layout/item_bottom_sheet_action" />
    diff --git a/vector/src/main/res/layout/item_bottom_sheet_action.xml b/vector/src/main/res/layout/item_bottom_sheet_action.xml
    index 44145e7bf5..4f56e3c126 100644
    --- a/vector/src/main/res/layout/item_bottom_sheet_action.xml
    +++ b/vector/src/main/res/layout/item_bottom_sheet_action.xml
    @@ -11,8 +11,7 @@
         android:paddingLeft="@dimen/layout_horizontal_margin"
         android:paddingTop="8dp"
         android:paddingRight="@dimen/layout_horizontal_margin"
    -    android:paddingBottom="8dp"
    -    tools:layout_height="50dp">
    +    android:paddingBottom="8dp">
     
         
    Date: Thu, 10 Oct 2019 16:44:33 +0200
    Subject: [PATCH 150/197] Report content: UI menu
    
    ---
     .../timeline/action/BottomSheetItemAction.kt  | 14 ++++++++++++++
     .../action/MessageActionsBottomSheet.kt       |  9 +++++++--
     .../action/MessageActionsEpoxyController.kt   | 19 +++++++++++++++++++
     .../action/MessageActionsViewModel.kt         | 13 +++++++++++--
     .../detail/timeline/action/SimpleAction.kt    |  5 ++++-
     .../main/res/drawable/ic_report_custom.xml    |  8 ++++++++
     .../res/drawable/ic_report_inappropriate.xml  | 12 ++++++++++++
     .../src/main/res/drawable/ic_report_spam.xml  |  8 ++++++++
     .../res/layout/item_bottom_sheet_action.xml   | 19 +++++++++++++++++--
     vector/src/main/res/values/strings_riotX.xml  |  4 ++++
     10 files changed, 104 insertions(+), 7 deletions(-)
     create mode 100644 vector/src/main/res/drawable/ic_report_custom.xml
     create mode 100644 vector/src/main/res/drawable/ic_report_inappropriate.xml
     create mode 100644 vector/src/main/res/drawable/ic_report_spam.xml
    
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemAction.kt
    index 8ee7460d53..d0d5b1deea 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemAction.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemAction.kt
    @@ -19,6 +19,7 @@ import android.view.View
     import android.widget.ImageView
     import android.widget.TextView
     import androidx.annotation.DrawableRes
    +import androidx.core.view.isVisible
     import com.airbnb.epoxy.EpoxyAttribute
     import com.airbnb.epoxy.EpoxyModelClass
     import im.vector.riotx.R
    @@ -37,6 +38,12 @@ abstract class BottomSheetItemAction : VectorEpoxyModel(R.id.action_start_space)
             val icon by bind(R.id.action_icon)
             val text by bind(R.id.action_title)
    +        val expand by bind(R.id.action_expand)
         }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
    index 53ac1bd430..e1ff0ce628 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
    @@ -57,8 +57,13 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), Message
         }
     
         override fun didSelectMenuAction(simpleAction: SimpleAction) {
    -        actionHandlerModel.fireAction(simpleAction)
    -        dismiss()
    +        if (simpleAction is SimpleAction.ReportContent) {
    +            // Toggle report menu
    +            viewModel.toggleReportMenu()
    +        } else {
    +            actionHandlerModel.fireAction(simpleAction)
    +            dismiss()
    +        }
         }
     
         override fun invalidate() = withState(viewModel) {
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt
    index 66c1949631..d9119f08b3 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt
    @@ -94,8 +94,27 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid
                     id("action_$index")
                     iconRes(action.iconResId)
                     textRes(action.titleRes)
    +                showExpand(action is SimpleAction.ReportContent)
    +                expanded(state.expendedReportContentMenu)
                     listener(View.OnClickListener { listener?.didSelectMenuAction(action) })
                 }
    +
    +            if (action is SimpleAction.ReportContent && state.expendedReportContentMenu) {
    +                // Special case for report content menu: add the submenu
    +                listOf(
    +                        SimpleAction.ReportContentSpam(action.eventId),
    +                        SimpleAction.ReportContentInappropriate(action.eventId),
    +                        SimpleAction.ReportContentCustom(action.eventId)
    +                ).forEachIndexed { indexReport, actionReport ->
    +                    bottomSheetItemAction {
    +                        id("actionReport_$indexReport")
    +                        subMenuItem(true)
    +                        iconRes(actionReport.iconResId)
    +                        textRes(actionReport.titleRes)
    +                        listener(View.OnClickListener { listener?.didSelectMenuAction(actionReport) })
    +                    }
    +                }
    +            }
             }
         }
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
    index cc1237a555..3b25a9e908 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
    @@ -61,7 +61,8 @@ data class MessageActionState(
             // For quick reactions
             val quickStates: Async> = Uninitialized,
             // For actions
    -        val actions: Async> = Uninitialized
    +        val actions: Async> = Uninitialized,
    +        val expendedReportContentMenu: Boolean = false
     ) : MvRxState {
     
         constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData)
    @@ -111,6 +112,14 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
             observeEventAction()
         }
     
    +    fun toggleReportMenu() = withState {
    +        setState {
    +            copy(
    +                    expendedReportContentMenu = it.expendedReportContentMenu.not()
    +            )
    +        }
    +    }
    +
         private fun observeEvent() {
             if (room == null) return
             RxRoom(room)
    @@ -253,7 +262,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
     
                     if (session.myUserId != event.root.senderId && event.root.getClearType() == EventType.MESSAGE) {
                         // not sent by me
    -                    add(SimpleAction.Flag(eventId))
    +                    add(SimpleAction.ReportContent(eventId))
                     }
                 }
             }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/SimpleAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/SimpleAction.kt
    index 9ba1bbb212..5da589d862 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/SimpleAction.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/SimpleAction.kt
    @@ -35,7 +35,10 @@ sealed class SimpleAction(@StringRes val titleRes: Int, @DrawableRes val iconRes
         data class ViewSource(val content: String) : SimpleAction(R.string.view_source, R.drawable.ic_view_source)
         data class ViewDecryptedSource(val content: String) : SimpleAction(R.string.view_decrypted_source, R.drawable.ic_view_source)
         data class CopyPermalink(val eventId: String) : SimpleAction(R.string.permalink, R.drawable.ic_permalink)
    -    data class Flag(val eventId: String) : SimpleAction(R.string.report_content, R.drawable.ic_flag)
    +    data class ReportContent(val eventId: String) : SimpleAction(R.string.report_content, R.drawable.ic_flag)
    +    data class ReportContentSpam(val eventId: String) : SimpleAction(R.string.report_content_spam, R.drawable.ic_report_spam)
    +    data class ReportContentInappropriate(val eventId: String) : SimpleAction(R.string.report_content_inappropriate, R.drawable.ic_report_inappropriate)
    +    data class ReportContentCustom(val eventId: String) : SimpleAction(R.string.report_content_custom, R.drawable.ic_report_custom)
         data class QuickReact(val eventId: String, val clickedOn: String, val add: Boolean) : SimpleAction(0, 0)
         data class ViewReactions(val messageInformationData: MessageInformationData) : SimpleAction(R.string.message_view_reaction, R.drawable.ic_view_reactions)
         data class ViewEditHistory(val messageInformationData: MessageInformationData) :
    diff --git a/vector/src/main/res/drawable/ic_report_custom.xml b/vector/src/main/res/drawable/ic_report_custom.xml
    new file mode 100644
    index 0000000000..8e97c4bfb5
    --- /dev/null
    +++ b/vector/src/main/res/drawable/ic_report_custom.xml
    @@ -0,0 +1,8 @@
    +
    +    
    +
    diff --git a/vector/src/main/res/drawable/ic_report_inappropriate.xml b/vector/src/main/res/drawable/ic_report_inappropriate.xml
    new file mode 100644
    index 0000000000..47cc0591bd
    --- /dev/null
    +++ b/vector/src/main/res/drawable/ic_report_inappropriate.xml
    @@ -0,0 +1,12 @@
    +
    +    
    +    
    +
    diff --git a/vector/src/main/res/drawable/ic_report_spam.xml b/vector/src/main/res/drawable/ic_report_spam.xml
    new file mode 100644
    index 0000000000..bd5a46e00a
    --- /dev/null
    +++ b/vector/src/main/res/drawable/ic_report_spam.xml
    @@ -0,0 +1,8 @@
    +
    +    
    +
    diff --git a/vector/src/main/res/layout/item_bottom_sheet_action.xml b/vector/src/main/res/layout/item_bottom_sheet_action.xml
    index 4f56e3c126..131ee0e63c 100644
    --- a/vector/src/main/res/layout/item_bottom_sheet_action.xml
    +++ b/vector/src/main/res/layout/item_bottom_sheet_action.xml
    @@ -6,6 +6,7 @@
         android:clickable="true"
         android:focusable="true"
         android:foreground="?attr/selectableItemBackground"
    +    android:gravity="center_vertical"
         android:minHeight="50dp"
         android:orientation="horizontal"
         android:paddingLeft="@dimen/layout_horizontal_margin"
    @@ -13,11 +14,17 @@
         android:paddingRight="@dimen/layout_horizontal_margin"
         android:paddingBottom="8dp">
     
    +    
    +
         
     
    +    
    +
     
    diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml
    index a66858ae0e..6a2bcef5a7 100644
    --- a/vector/src/main/res/values/strings_riotX.xml
    +++ b/vector/src/main/res/values/strings_riotX.xml
    @@ -37,4 +37,8 @@
     
         "The file '%1$s' (%2$s) is too large to upload. The limit is %3$s."
     
    +
    +    "It's spam"
    +    "It's inappropriate"
    +    "Custom report"
     
    
    From a7a19dab1113fdca339fb3b7f3421a5d50f50316 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Thu, 10 Oct 2019 17:37:04 +0200
    Subject: [PATCH 151/197] Report content: Service and REST request
    
    ---
     .../matrix/android/api/session/room/Room.kt   |  2 +
     .../room/reporting/ReportingService.kt        | 33 +++++++++++++
     .../internal/session/room/DefaultRoom.kt      | 21 +++++----
     .../android/internal/session/room/RoomAPI.kt  | 13 ++++++
     .../internal/session/room/RoomFactory.kt      |  3 ++
     .../internal/session/room/RoomModule.kt       |  5 ++
     .../room/reporting/DefaultReportingService.kt | 46 +++++++++++++++++++
     .../room/reporting/ReportContentBody.kt       | 33 +++++++++++++
     .../room/reporting/ReportContentTask.kt       | 39 ++++++++++++++++
     .../home/room/detail/RoomDetailActions.kt     |  3 ++
     .../home/room/detail/RoomDetailFragment.kt    | 43 ++++++++++-------
     .../home/room/detail/RoomDetailViewModel.kt   | 32 +++++++++++--
     .../home/room/detail/RoomDetailViewState.kt   |  3 +-
     13 files changed, 245 insertions(+), 31 deletions(-)
     create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/reporting/ReportingService.kt
     create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/reporting/DefaultReportingService.kt
     create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/reporting/ReportContentBody.kt
     create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/reporting/ReportContentTask.kt
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt
    index 9f91e5b276..70c9c6e36c 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt
    @@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.room.crypto.RoomCryptoService
     import im.vector.matrix.android.api.session.room.members.MembershipService
     import im.vector.matrix.android.api.session.room.model.RoomSummary
     import im.vector.matrix.android.api.session.room.model.relation.RelationService
    +import im.vector.matrix.android.api.session.room.reporting.ReportingService
     import im.vector.matrix.android.api.session.room.read.ReadService
     import im.vector.matrix.android.api.session.room.send.DraftService
     import im.vector.matrix.android.api.session.room.send.SendService
    @@ -38,6 +39,7 @@ interface Room :
             ReadService,
             MembershipService,
             StateService,
    +        ReportingService,
             RelationService,
             RoomCryptoService {
     
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/reporting/ReportingService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/reporting/ReportingService.kt
    new file mode 100644
    index 0000000000..9dbc6d0e9e
    --- /dev/null
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/reporting/ReportingService.kt
    @@ -0,0 +1,33 @@
    +/*
    + * Copyright 2019 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.matrix.android.api.session.room.reporting
    +
    +import im.vector.matrix.android.api.MatrixCallback
    +import im.vector.matrix.android.api.util.Cancelable
    +
    +/**
    + * This interface defines methods to report content of an event.
    + */
    +interface ReportingService {
    +
    +    /**
    +     * Report content
    +     * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-rooms-roomid-report-eventid
    +     */
    +    fun reportContent(eventId: String, score: Int, reason: String, callback: MatrixCallback): Cancelable
    +
    +}
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt
    index 7d957ccdad..fea827fd25 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt
    @@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.room.Room
     import im.vector.matrix.android.api.session.room.members.MembershipService
     import im.vector.matrix.android.api.session.room.model.RoomSummary
     import im.vector.matrix.android.api.session.room.model.relation.RelationService
    +import im.vector.matrix.android.api.session.room.reporting.ReportingService
     import im.vector.matrix.android.api.session.room.read.ReadService
     import im.vector.matrix.android.api.session.room.send.DraftService
     import im.vector.matrix.android.api.session.room.send.SendService
    @@ -44,18 +45,20 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
                                                    private val sendService: SendService,
                                                    private val draftService: DraftService,
                                                    private val stateService: StateService,
    +                                               private val reportingService: ReportingService,
                                                    private val readService: ReadService,
                                                    private val cryptoService: CryptoService,
                                                    private val relationService: RelationService,
    -                                               private val roomMembersService: MembershipService
    -) : Room,
    -    TimelineService by timelineService,
    -    SendService by sendService,
    -    DraftService by draftService,
    -    StateService by stateService,
    -    ReadService by readService,
    -    RelationService by relationService,
    -    MembershipService by roomMembersService {
    +                                               private val roomMembersService: MembershipService) :
    +        Room,
    +        TimelineService by timelineService,
    +        SendService by sendService,
    +        DraftService by draftService,
    +        StateService by stateService,
    +        ReportingService by reportingService,
    +        ReadService by readService,
    +        RelationService by relationService,
    +        MembershipService by roomMembersService {
     
         override fun getRoomSummaryLive(): LiveData> {
             val liveData = monarchy.findAllMappedWithChanges(
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt
    index daee1c914c..797dbed31c 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt
    @@ -27,6 +27,7 @@ import im.vector.matrix.android.internal.network.NetworkConstants
     import im.vector.matrix.android.internal.session.room.membership.RoomMembersResponse
     import im.vector.matrix.android.internal.session.room.membership.joining.InviteBody
     import im.vector.matrix.android.internal.session.room.relation.RelationsResponse
    +import im.vector.matrix.android.internal.session.room.reporting.ReportContentBody
     import im.vector.matrix.android.internal.session.room.send.SendResponse
     import im.vector.matrix.android.internal.session.room.timeline.EventContextResponse
     import im.vector.matrix.android.internal.session.room.timeline.PaginationResponse
    @@ -245,4 +246,16 @@ internal interface RoomAPI {
                 @Path("eventId") parent_id: String,
                 @Body reason: Map
         ): Call
    +
    +    /**
    +     * Reports an event as inappropriate to the server, which may then notify the appropriate people.
    +     *
    +     * @param roomId  the room id
    +     * @param eventId the event to report content
    +     * @param body    body containing score and reason
    +     */
    +    @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/report/{eventId}")
    +    fun reportContent(@Path("roomId") roomId: String,
    +                      @Path("eventId") eventId: String,
    +                      @Body body: ReportContentBody): Call
     }
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt
    index 65a3624d2c..e2199782f4 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt
    @@ -24,6 +24,7 @@ import im.vector.matrix.android.internal.session.room.draft.DefaultDraftService
     import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService
     import im.vector.matrix.android.internal.session.room.read.DefaultReadService
     import im.vector.matrix.android.internal.session.room.relation.DefaultRelationService
    +import im.vector.matrix.android.internal.session.room.reporting.DefaultReportingService
     import im.vector.matrix.android.internal.session.room.send.DefaultSendService
     import im.vector.matrix.android.internal.session.room.state.DefaultStateService
     import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService
    @@ -40,6 +41,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona
                                                           private val sendServiceFactory: DefaultSendService.Factory,
                                                           private val draftServiceFactory: DefaultDraftService.Factory,
                                                           private val stateServiceFactory: DefaultStateService.Factory,
    +                                                      private val reportingServiceFactory: DefaultReportingService.Factory,
                                                           private val readServiceFactory: DefaultReadService.Factory,
                                                           private val relationServiceFactory: DefaultRelationService.Factory,
                                                           private val membershipServiceFactory: DefaultMembershipService.Factory) :
    @@ -54,6 +56,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona
                     sendServiceFactory.create(roomId),
                     draftServiceFactory.create(roomId),
                     stateServiceFactory.create(roomId),
    +                reportingServiceFactory.create(roomId),
                     readServiceFactory.create(roomId),
                     cryptoService,
                     relationServiceFactory.create(roomId),
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt
    index 5755d6b46e..1aca492b94 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt
    @@ -45,6 +45,8 @@ import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkers
     import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask
     import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask
     import im.vector.matrix.android.internal.session.room.relation.*
    +import im.vector.matrix.android.internal.session.room.reporting.DefaultReportContentTask
    +import im.vector.matrix.android.internal.session.room.reporting.ReportContentTask
     import im.vector.matrix.android.internal.session.room.state.DefaultSendStateTask
     import im.vector.matrix.android.internal.session.room.state.SendStateTask
     import im.vector.matrix.android.internal.session.room.timeline.*
    @@ -114,6 +116,9 @@ internal abstract class RoomModule {
         @Binds
         abstract fun bindSendStateTask(sendStateTask: DefaultSendStateTask): SendStateTask
     
    +    @Binds
    +    abstract fun bindReportContentTask(reportContentTask: DefaultReportContentTask): ReportContentTask
    +
         @Binds
         abstract fun bindGetContextOfEventTask(getContextOfEventTask: DefaultGetContextOfEventTask): GetContextOfEventTask
     
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/reporting/DefaultReportingService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/reporting/DefaultReportingService.kt
    new file mode 100644
    index 0000000000..3b64cce439
    --- /dev/null
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/reporting/DefaultReportingService.kt
    @@ -0,0 +1,46 @@
    +/*
    + * Copyright 2019 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.matrix.android.internal.session.room.reporting
    +
    +import com.squareup.inject.assisted.Assisted
    +import com.squareup.inject.assisted.AssistedInject
    +import im.vector.matrix.android.api.MatrixCallback
    +import im.vector.matrix.android.api.session.room.reporting.ReportingService
    +import im.vector.matrix.android.api.util.Cancelable
    +import im.vector.matrix.android.internal.task.TaskExecutor
    +import im.vector.matrix.android.internal.task.configureWith
    +
    +internal class DefaultReportingService @AssistedInject constructor(@Assisted private val roomId: String,
    +                                                                   private val taskExecutor: TaskExecutor,
    +                                                                   private val reportContentTask: ReportContentTask
    +) : ReportingService {
    +
    +    @AssistedInject.Factory
    +    interface Factory {
    +        fun create(roomId: String): ReportingService
    +    }
    +
    +    override fun reportContent(eventId: String, score: Int, reason: String, callback: MatrixCallback): Cancelable {
    +        val params = ReportContentTask.Params(roomId, eventId, score, reason)
    +
    +        return reportContentTask
    +                .configureWith(params) {
    +                    this.callback = callback
    +                }
    +                .executeBy(taskExecutor)
    +    }
    +}
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/reporting/ReportContentBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/reporting/ReportContentBody.kt
    new file mode 100644
    index 0000000000..2cf33551ce
    --- /dev/null
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/reporting/ReportContentBody.kt
    @@ -0,0 +1,33 @@
    +/*
    + * Copyright 2019 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.matrix.android.internal.session.room.reporting
    +
    +import com.squareup.moshi.Json
    +import com.squareup.moshi.JsonClass
    +
    +@JsonClass(generateAdapter = true)
    +internal data class ReportContentBody(
    +        /**
    +         * Required. The score to rate this content as where -100 is most offensive and 0 is inoffensive.
    +         */
    +        @Json(name = "score") val score: Int,
    +
    +        /**
    +         * Required. The reason the content is being reported. May be blank.
    +         */
    +        @Json(name = "reason") val reason: String
    +)
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/reporting/ReportContentTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/reporting/ReportContentTask.kt
    new file mode 100644
    index 0000000000..60c031158a
    --- /dev/null
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/reporting/ReportContentTask.kt
    @@ -0,0 +1,39 @@
    +/*
    + * Copyright 2019 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.matrix.android.internal.session.room.reporting
    +
    +import im.vector.matrix.android.internal.network.executeRequest
    +import im.vector.matrix.android.internal.session.room.RoomAPI
    +import im.vector.matrix.android.internal.task.Task
    +import javax.inject.Inject
    +
    +internal interface ReportContentTask : Task {
    +    data class Params(
    +            val roomId: String,
    +            val eventId: String,
    +            val score: Int,
    +            val reason: String
    +    )
    +}
    +
    +internal class DefaultReportContentTask @Inject constructor(private val roomAPI: RoomAPI) : ReportContentTask {
    +    override suspend fun execute(params: ReportContentTask.Params) {
    +        return executeRequest {
    +            apiCall = roomAPI.reportContent(params.roomId, params.eventId, ReportContentBody(params.score, params.reason))
    +        }
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt
    index d032182994..eec9eef5dc 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt
    @@ -49,6 +49,9 @@ sealed class RoomDetailActions {
     
         data class ResendMessage(val eventId: String) : RoomDetailActions()
         data class RemoveFailedEcho(val eventId: String) : RoomDetailActions()
    +
    +    data class ReportContent(val eventId: String, val reason: String) : RoomDetailActions()
    +
         object ClearSendQueue : RoomDetailActions()
         object ResendAll : RoomDetailActions()
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    index 7447bf92e6..d26dba44e1 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    @@ -93,7 +93,9 @@ import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel
     import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState
     import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
     import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
    -import im.vector.riotx.features.home.room.detail.timeline.action.*
    +import im.vector.riotx.features.home.room.detail.timeline.action.ActionsHandler
    +import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet
    +import im.vector.riotx.features.home.room.detail.timeline.action.SimpleAction
     import im.vector.riotx.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet
     import im.vector.riotx.features.home.room.detail.timeline.item.*
     import im.vector.riotx.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet
    @@ -968,23 +970,23 @@ class RoomDetailFragment :
     
         private fun handleActions(action: SimpleAction) {
             when (action) {
    -            is SimpleAction.AddReaction         -> {
    +            is SimpleAction.AddReaction                -> {
                     startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), action.eventId), REACTION_SELECT_REQUEST_CODE)
                 }
    -            is SimpleAction.ViewReactions       -> {
    +            is SimpleAction.ViewReactions              -> {
                     ViewReactionsBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData)
                             .show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
                 }
    -            is SimpleAction.Copy                -> {
    +            is SimpleAction.Copy                       -> {
                     // I need info about the current selected message :/
                     copyToClipboard(requireContext(), action.content, false)
                     val msg = requireContext().getString(R.string.copied_to_clipboard)
                     showSnackWithMessage(msg, Snackbar.LENGTH_SHORT)
                 }
    -            is SimpleAction.Delete              -> {
    +            is SimpleAction.Delete                     -> {
                     roomDetailViewModel.process(RoomDetailActions.RedactAction(action.eventId, context?.getString(R.string.event_redacted_by_user_reason)))
                 }
    -            is SimpleAction.Share               -> {
    +            is SimpleAction.Share                      -> {
                     // TODO current data communication is too limited
                     // Need to now the media type
                     // TODO bad, just POC
    @@ -1012,10 +1014,10 @@ class RoomDetailFragment :
                             }
                     )
                 }
    -            is SimpleAction.ViewEditHistory     -> {
    +            is SimpleAction.ViewEditHistory            -> {
                     onEditedDecorationClicked(action.messageInformationData)
                 }
    -            is SimpleAction.ViewSource          -> {
    +            is SimpleAction.ViewSource                 -> {
                     val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null)
                     view.findViewById(R.id.event_content_text_view)?.let {
                         it.text = action.content
    @@ -1026,7 +1028,7 @@ class RoomDetailFragment :
                             .setPositiveButton(R.string.ok, null)
                             .show()
                 }
    -            is SimpleAction.ViewDecryptedSource -> {
    +            is SimpleAction.ViewDecryptedSource        -> {
                     val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null)
                     view.findViewById(R.id.event_content_text_view)?.let {
                         it.text = action.content
    @@ -1037,31 +1039,38 @@ class RoomDetailFragment :
                             .setPositiveButton(R.string.ok, null)
                             .show()
                 }
    -            is SimpleAction.QuickReact          -> {
    +            is SimpleAction.QuickReact                 -> {
                     // eventId,ClickedOn,Add
                     roomDetailViewModel.process(RoomDetailActions.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add))
                 }
    -            is SimpleAction.Edit                -> {
    +            is SimpleAction.Edit                       -> {
                     roomDetailViewModel.process(RoomDetailActions.EnterEditMode(action.eventId, composerLayout.composerEditText.text.toString()))
                 }
    -            is SimpleAction.Quote               -> {
    +            is SimpleAction.Quote                      -> {
                     roomDetailViewModel.process(RoomDetailActions.EnterQuoteMode(action.eventId, composerLayout.composerEditText.text.toString()))
                 }
    -            is SimpleAction.Reply               -> {
    +            is SimpleAction.Reply                      -> {
                     roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(action.eventId, composerLayout.composerEditText.text.toString()))
                 }
    -            is SimpleAction.CopyPermalink       -> {
    +            is SimpleAction.CopyPermalink              -> {
                     val permalink = PermalinkFactory.createPermalink(roomDetailArgs.roomId, action.eventId)
                     copyToClipboard(requireContext(), permalink, false)
                     showSnackWithMessage(requireContext().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT)
                 }
    -            is SimpleAction.Resend              -> {
    +            is SimpleAction.Resend                     -> {
                     roomDetailViewModel.process(RoomDetailActions.ResendMessage(action.eventId))
                 }
    -            is SimpleAction.Remove              -> {
    +            is SimpleAction.Remove                     -> {
                     roomDetailViewModel.process(RoomDetailActions.RemoveFailedEcho(action.eventId))
                 }
    -            else                                -> {
    +            is SimpleAction.ReportContentSpam          -> {
    +                roomDetailViewModel.process(RoomDetailActions.ReportContent(action.eventId, "This message is spam"))
    +            }
    +            is SimpleAction.ReportContentInappropriate -> {
    +                roomDetailViewModel.process(RoomDetailActions.ReportContent(action.eventId, "This message is inappropriate"))
    +            }
    +            // TODO Custom
    +            else                                       -> {
                     Toast.makeText(context, "Action $action is not implemented yet", Toast.LENGTH_LONG).show()
                 }
             }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    index 57a2c76f61..ee60a89bc1 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    @@ -21,10 +21,7 @@ import android.text.TextUtils
     import androidx.annotation.IdRes
     import androidx.lifecycle.LiveData
     import androidx.lifecycle.MutableLiveData
    -import com.airbnb.mvrx.FragmentViewModelContext
    -import com.airbnb.mvrx.MvRxViewModelFactory
    -import com.airbnb.mvrx.Success
    -import com.airbnb.mvrx.ViewModelContext
    +import com.airbnb.mvrx.*
     import com.jakewharton.rxrelay2.BehaviorRelay
     import com.squareup.inject.assisted.Assisted
     import com.squareup.inject.assisted.AssistedInject
    @@ -155,6 +152,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
                 is RoomDetailActions.ResendAll                   -> handleResendAll()
                 is RoomDetailActions.SetReadMarkerAction         -> handleSetReadMarkerAction(action)
                 is RoomDetailActions.MarkAllAsRead               -> handleMarkAllAsRead()
    +            is RoomDetailActions.ReportContent               -> handleReportContent(action)
             }
         }
     
    @@ -708,6 +706,32 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
             room.markAllAsRead(object : MatrixCallback {})
         }
     
    +    private fun handleReportContent(action: RoomDetailActions.ReportContent) {
    +        setState {
    +            copy(
    +                    reportContentRequest = Loading()
    +            )
    +        }
    +
    +        room.reportContent(action.eventId, -100, action.reason, object : MatrixCallback {
    +            override fun onSuccess(data: Unit) {
    +                setState {
    +                    copy(
    +                            reportContentRequest = Success(Unit)
    +                    )
    +                }
    +            }
    +
    +            override fun onFailure(failure: Throwable) {
    +                setState {
    +                    copy(
    +                            reportContentRequest = Fail(failure)
    +                    )
    +                }
    +            }
    +        })
    +    }
    +
         private fun observeSyncState() {
             session.rx()
                     .liveSyncState()
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt
    index 03110858a1..ba20b9ed3f 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt
    @@ -52,7 +52,8 @@ data class RoomDetailViewState(
             val tombstoneEvent: Event? = null,
             val tombstoneEventHandling: Async = Uninitialized,
             val syncState: SyncState = SyncState.IDLE,
    -        val highlightedEventId: String? = null
    +        val highlightedEventId: String? = null,
    +        val reportContentRequest: Async = Uninitialized
     ) : MvRxState {
     
         constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId)
    
    From 4a6237b50eef50235661d93b326afc540787835a Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Thu, 10 Oct 2019 18:06:33 +0200
    Subject: [PATCH 152/197] Report content: confirmation dialogs
    
    ---
     .../home/room/detail/RoomDetailActions.kt     |  2 +-
     .../home/room/detail/RoomDetailFragment.kt    | 53 ++++++++++++++++++-
     .../home/room/detail/RoomDetailViewModel.kt   | 23 +++-----
     .../home/room/detail/RoomDetailViewState.kt   |  3 +-
     vector/src/main/res/values/strings_riotX.xml  | 11 ++++
     5 files changed, 71 insertions(+), 21 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt
    index eec9eef5dc..25b526fb8a 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt
    @@ -50,7 +50,7 @@ sealed class RoomDetailActions {
         data class ResendMessage(val eventId: String) : RoomDetailActions()
         data class RemoveFailedEcho(val eventId: String) : RoomDetailActions()
     
    -    data class ReportContent(val eventId: String, val reason: String) : RoomDetailActions()
    +    data class ReportContent(val eventId: String, val reason: String, val spam: Boolean = false, val inappropriate: Boolean = false) : RoomDetailActions()
     
         object ClearSendQueue : RoomDetailActions()
         object ResendAll : RoomDetailActions()
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    index d26dba44e1..1a6023f37d 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    @@ -268,6 +268,10 @@ class RoomDetailFragment :
             roomDetailViewModel.selectSubscribe(RoomDetailViewState::syncState) { syncState ->
                 syncStateView.render(syncState)
             }
    +
    +        roomDetailViewModel.requestLiveData.observeEvent(this) {
    +            displayRoomDetailActionResult(it)
    +        }
         }
     
         override fun onDestroy() {
    @@ -777,6 +781,51 @@ class RoomDetailFragment :
                     .show()
         }
     
    +    private fun displayRoomDetailActionResult(result: Async) {
    +        when (result) {
    +            is Fail    -> {
    +                AlertDialog.Builder(activity!!)
    +                        .setTitle(R.string.dialog_title_error)
    +                        .setMessage(errorFormatter.toHumanReadable(result.error))
    +                        .setPositiveButton(R.string.ok, null)
    +                        .show()
    +            }
    +            is Success -> {
    +                when (val data = result.invoke()) {
    +                    is RoomDetailActions.ReportContent -> {
    +                        when {
    +                            data.spam          -> {
    +                                AlertDialog.Builder(activity!!)
    +                                        .setTitle(R.string.content_reported_as_spam_title)
    +                                        .setMessage(R.string.content_reported_as_spam_content)
    +                                        .setPositiveButton(R.string.ok, null)
    +                                        .setNegativeButton(R.string.block_user) { _, _ -> vectorBaseActivity.notImplemented("block user") }
    +                                        .show()
    +                            }
    +                            data.inappropriate -> {
    +                                AlertDialog.Builder(activity!!)
    +                                        .setTitle(R.string.content_reported_as_inappropriate_title)
    +                                        .setMessage(R.string.content_reported_as_inappropriate_content)
    +                                        .setPositiveButton(R.string.ok, null)
    +                                        .setNegativeButton(R.string.block_user) { _, _ -> vectorBaseActivity.notImplemented("block user") }
    +                                        .show()
    +                            }
    +                            else               -> {
    +                                AlertDialog.Builder(activity!!)
    +                                        .setTitle(R.string.content_reported_title)
    +                                        .setMessage(R.string.content_reported_content)
    +                                        .setPositiveButton(R.string.ok, null)
    +                                        .setNegativeButton(R.string.block_user) { _, _ -> vectorBaseActivity.notImplemented("block user") }
    +                                        .show()
    +                            }
    +                        }
    +                    }
    +                }
    +            }
    +        }
    +    }
    +
    +
     // TimelineEventController.Callback ************************************************************
     
         override fun onUrlClicked(url: String): Boolean {
    @@ -1064,10 +1113,10 @@ class RoomDetailFragment :
                     roomDetailViewModel.process(RoomDetailActions.RemoveFailedEcho(action.eventId))
                 }
                 is SimpleAction.ReportContentSpam          -> {
    -                roomDetailViewModel.process(RoomDetailActions.ReportContent(action.eventId, "This message is spam"))
    +                roomDetailViewModel.process(RoomDetailActions.ReportContent(action.eventId, "This message is spam", spam = true))
                 }
                 is SimpleAction.ReportContentInappropriate -> {
    -                roomDetailViewModel.process(RoomDetailActions.ReportContent(action.eventId, "This message is inappropriate"))
    +                roomDetailViewModel.process(RoomDetailActions.ReportContent(action.eventId, "This message is inappropriate", inappropriate = true))
                 }
                 // TODO Custom
                 else                                       -> {
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    index ee60a89bc1..0774e48d27 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    @@ -94,6 +94,11 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
     
         private var timeline = room.createTimeline(eventId, timelineSettings)
     
    +    // Can be used for several actions, for a one shot result
    +    private val _requestLiveData = MutableLiveData>>()
    +    val requestLiveData: LiveData>>
    +        get() = _requestLiveData
    +
         // Slot to keep a pending action during permission request
         var pendingAction: RoomDetailActions? = null
     
    @@ -707,27 +712,13 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
         }
     
         private fun handleReportContent(action: RoomDetailActions.ReportContent) {
    -        setState {
    -            copy(
    -                    reportContentRequest = Loading()
    -            )
    -        }
    -
             room.reportContent(action.eventId, -100, action.reason, object : MatrixCallback {
                 override fun onSuccess(data: Unit) {
    -                setState {
    -                    copy(
    -                            reportContentRequest = Success(Unit)
    -                    )
    -                }
    +                _requestLiveData.postValue(LiveEvent(Success(action)))
                 }
     
                 override fun onFailure(failure: Throwable) {
    -                setState {
    -                    copy(
    -                            reportContentRequest = Fail(failure)
    -                    )
    -                }
    +                _requestLiveData.postValue(LiveEvent(Fail(failure)))
                 }
             })
         }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt
    index ba20b9ed3f..03110858a1 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt
    @@ -52,8 +52,7 @@ data class RoomDetailViewState(
             val tombstoneEvent: Event? = null,
             val tombstoneEventHandling: Async = Uninitialized,
             val syncState: SyncState = SyncState.IDLE,
    -        val highlightedEventId: String? = null,
    -        val reportContentRequest: Async = Uninitialized
    +        val highlightedEventId: String? = null
     ) : MvRxState {
     
         constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId)
    diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml
    index 6a2bcef5a7..30fae49f13 100644
    --- a/vector/src/main/res/values/strings_riotX.xml
    +++ b/vector/src/main/res/values/strings_riotX.xml
    @@ -41,4 +41,15 @@
         "It's spam"
         "It's inappropriate"
         "Custom report"
    +    "Report this content"
    +    "Reason for reporting this content"
    +    "REPORT"
    +    "BLOCK USER"
    +
    +    "Content reported"
    +    "This content was reported.\n\nIf you don't want to see any more content from this user, you can block him to hide his messages"
    +    "Reported as spam"
    +    "This content was reported as spam.\n\nIf you don't want to see any more content from this user, you can block him to hide his messages"
    +    "Reported as inappropriate"
    +    "This content was reported as inappropriate.\n\nIf you don't want to see any more content from this user, you can block him to hide his messages"
     
    
    From 6ad1932fe5c7819affa372aee53077275372c259 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Thu, 10 Oct 2019 18:28:26 +0200
    Subject: [PATCH 153/197] Report content: custom reason
    
    ---
     .../home/room/detail/RoomDetailFragment.kt    | 32 +++++++++++++++----
     .../main/res/layout/dialog_report_content.xml | 28 ++++++++++++++++
     2 files changed, 54 insertions(+), 6 deletions(-)
     create mode 100644 vector/src/main/res/layout/dialog_report_content.xml
    
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    index 1a6023f37d..264c0e98bc 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    @@ -50,6 +50,7 @@ import com.airbnb.mvrx.*
     import com.github.piasy.biv.BigImageViewer
     import com.github.piasy.biv.loader.ImageLoader
     import com.google.android.material.snackbar.Snackbar
    +import com.google.android.material.textfield.TextInputEditText
     import com.jaiselrahman.filepicker.activity.FilePickerActivity
     import com.jaiselrahman.filepicker.config.Configurations
     import com.jaiselrahman.filepicker.model.MediaFile
    @@ -774,17 +775,34 @@ class RoomDetailFragment :
         }
     
         private fun displayCommandError(message: String) {
    -        AlertDialog.Builder(activity!!)
    +        AlertDialog.Builder(requireActivity())
                     .setTitle(R.string.command_error)
                     .setMessage(message)
                     .setPositiveButton(R.string.ok, null)
                     .show()
         }
     
    +    private fun promptReasonToReportContent(action: SimpleAction.ReportContentCustom) {
    +        val inflater = requireActivity().layoutInflater
    +        val layout = inflater.inflate(R.layout.dialog_report_content, null)
    +
    +        val input = layout.findViewById(R.id.dialog_report_content_input)
    +
    +        AlertDialog.Builder(requireActivity())
    +                .setTitle(R.string.report_content_custom_title)
    +                .setView(layout)
    +                .setPositiveButton(R.string.report_content_custom_submit) { _, _ ->
    +                    val reason = input.text.toString()
    +                    roomDetailViewModel.process(RoomDetailActions.ReportContent(action.eventId, reason))
    +                }
    +                .setNegativeButton(R.string.cancel, null)
    +                .show()
    +    }
    +
         private fun displayRoomDetailActionResult(result: Async) {
             when (result) {
                 is Fail    -> {
    -                AlertDialog.Builder(activity!!)
    +                AlertDialog.Builder(requireActivity())
                             .setTitle(R.string.dialog_title_error)
                             .setMessage(errorFormatter.toHumanReadable(result.error))
                             .setPositiveButton(R.string.ok, null)
    @@ -795,7 +813,7 @@ class RoomDetailFragment :
                         is RoomDetailActions.ReportContent -> {
                             when {
                                 data.spam          -> {
    -                                AlertDialog.Builder(activity!!)
    +                                AlertDialog.Builder(requireActivity())
                                             .setTitle(R.string.content_reported_as_spam_title)
                                             .setMessage(R.string.content_reported_as_spam_content)
                                             .setPositiveButton(R.string.ok, null)
    @@ -803,7 +821,7 @@ class RoomDetailFragment :
                                             .show()
                                 }
                                 data.inappropriate -> {
    -                                AlertDialog.Builder(activity!!)
    +                                AlertDialog.Builder(requireActivity())
                                             .setTitle(R.string.content_reported_as_inappropriate_title)
                                             .setMessage(R.string.content_reported_as_inappropriate_content)
                                             .setPositiveButton(R.string.ok, null)
    @@ -811,7 +829,7 @@ class RoomDetailFragment :
                                             .show()
                                 }
                                 else               -> {
    -                                AlertDialog.Builder(activity!!)
    +                                AlertDialog.Builder(requireActivity())
                                             .setTitle(R.string.content_reported_title)
                                             .setMessage(R.string.content_reported_content)
                                             .setPositiveButton(R.string.ok, null)
    @@ -1118,7 +1136,9 @@ class RoomDetailFragment :
                 is SimpleAction.ReportContentInappropriate -> {
                     roomDetailViewModel.process(RoomDetailActions.ReportContent(action.eventId, "This message is inappropriate", inappropriate = true))
                 }
    -            // TODO Custom
    +            is SimpleAction.ReportContentCustom        -> {
    +                promptReasonToReportContent(action)
    +            }
                 else                                       -> {
                     Toast.makeText(context, "Action $action is not implemented yet", Toast.LENGTH_LONG).show()
                 }
    diff --git a/vector/src/main/res/layout/dialog_report_content.xml b/vector/src/main/res/layout/dialog_report_content.xml
    new file mode 100644
    index 0000000000..dda84fe02d
    --- /dev/null
    +++ b/vector/src/main/res/layout/dialog_report_content.xml
    @@ -0,0 +1,28 @@
    +
    +
    +
    +    
    +
    +        
    +
    +    
    +
    \ No newline at end of file
    
    From 36042ed14575247473365cbf994827a18b82aef8 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 11 Oct 2019 09:59:21 +0200
    Subject: [PATCH 154/197] Report content: red color for "block user" button
    
    ---
     .../vector/riotx/core/dialogs/Extensions.kt   | 27 +++++++++++++++++++
     .../home/room/detail/RoomDetailFragment.kt    |  5 ++++
     2 files changed, 32 insertions(+)
     create mode 100644 vector/src/main/java/im/vector/riotx/core/dialogs/Extensions.kt
    
    diff --git a/vector/src/main/java/im/vector/riotx/core/dialogs/Extensions.kt b/vector/src/main/java/im/vector/riotx/core/dialogs/Extensions.kt
    new file mode 100644
    index 0000000000..1b90e88864
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/core/dialogs/Extensions.kt
    @@ -0,0 +1,27 @@
    +/*
    + * Copyright 2019 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.riotx.core.dialogs
    +
    +import androidx.annotation.ColorRes
    +import androidx.appcompat.app.AlertDialog
    +import androidx.core.content.ContextCompat
    +import im.vector.riotx.R
    +
    +fun AlertDialog.withColoredButton(whichButton: Int, @ColorRes color: Int = R.color.vector_error_color): AlertDialog {
    +    getButton(whichButton)?.setTextColor(ContextCompat.getColor(context, color))
    +    return this
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    index 264c0e98bc..4149436166 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    @@ -19,6 +19,7 @@ package im.vector.riotx.features.home.room.detail
     import android.annotation.SuppressLint
     import android.app.Activity.RESULT_OK
     import android.content.Context
    +import android.content.DialogInterface
     import android.content.Intent
     import android.graphics.drawable.ColorDrawable
     import android.net.Uri
    @@ -69,6 +70,7 @@ import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
     import im.vector.matrix.android.api.session.user.model.User
     import im.vector.riotx.R
     import im.vector.riotx.core.di.ScreenComponent
    +import im.vector.riotx.core.dialogs.withColoredButton
     import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer
     import im.vector.riotx.core.error.ErrorFormatter
     import im.vector.riotx.core.extensions.hideKeyboard
    @@ -819,6 +821,7 @@ class RoomDetailFragment :
                                             .setPositiveButton(R.string.ok, null)
                                             .setNegativeButton(R.string.block_user) { _, _ -> vectorBaseActivity.notImplemented("block user") }
                                             .show()
    +                                        .withColoredButton(DialogInterface.BUTTON_NEGATIVE)
                                 }
                                 data.inappropriate -> {
                                     AlertDialog.Builder(requireActivity())
    @@ -827,6 +830,7 @@ class RoomDetailFragment :
                                             .setPositiveButton(R.string.ok, null)
                                             .setNegativeButton(R.string.block_user) { _, _ -> vectorBaseActivity.notImplemented("block user") }
                                             .show()
    +                                        .withColoredButton(DialogInterface.BUTTON_NEGATIVE)
                                 }
                                 else               -> {
                                     AlertDialog.Builder(requireActivity())
    @@ -835,6 +839,7 @@ class RoomDetailFragment :
                                             .setPositiveButton(R.string.ok, null)
                                             .setNegativeButton(R.string.block_user) { _, _ -> vectorBaseActivity.notImplemented("block user") }
                                             .show()
    +                                        .withColoredButton(DialogInterface.BUTTON_NEGATIVE)
                                 }
                             }
                         }
    
    From b2f6fb8c916868b823bfa132395d285b40734a77 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 11 Oct 2019 10:57:31 +0200
    Subject: [PATCH 155/197] Try to fix the bottom sheet showing expanded by
     default. The second time it's open, it's not expanded... With this fix, the
     bug appear only at the third time...
    
    ---
     .../VectorBaseBottomSheetDialogFragment.kt    | 20 +++++++++++++++++++
     .../action/MessageActionsBottomSheet.kt       |  2 ++
     2 files changed, 22 insertions(+)
    
    diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt
    index 892f7b0daa..168aaf87f0 100644
    --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt
    @@ -15,12 +15,17 @@
      */
     package im.vector.riotx.core.platform
     
    +import android.app.Dialog
     import android.content.Context
     import android.os.Bundle
     import android.os.Parcelable
    +import android.view.View
    +import android.widget.FrameLayout
     import com.airbnb.mvrx.MvRx
     import com.airbnb.mvrx.MvRxView
     import com.airbnb.mvrx.MvRxViewModelStore
    +import com.google.android.material.bottomsheet.BottomSheetBehavior
    +import com.google.android.material.bottomsheet.BottomSheetDialog
     import com.google.android.material.bottomsheet.BottomSheetDialogFragment
     import im.vector.riotx.core.di.DaggerScreenComponent
     import im.vector.riotx.core.di.ScreenComponent
    @@ -40,6 +45,8 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
             activity as VectorBaseActivity
         }
     
    +    open val showExpanded = false
    +
         override fun onAttach(context: Context) {
             screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity)
             super.onAttach(context)
    @@ -56,6 +63,19 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
             super.onCreate(savedInstanceState)
         }
     
    +    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
    +        return super.onCreateDialog(savedInstanceState).apply {
    +            if (showExpanded) {
    +                setOnShowListener { dialog ->
    +                    val d = dialog as BottomSheetDialog
    +
    +                    val bottomSheet = d.findViewById(com.google.android.material.R.id.design_bottom_sheet) as FrameLayout?
    +                    BottomSheetBehavior.from(bottomSheet!!).state = BottomSheetBehavior.STATE_EXPANDED
    +                }
    +            }
    +        }
    +    }
    +
         override fun onSaveInstanceState(outState: Bundle) {
             super.onSaveInstanceState(outState)
             mvrxViewModelStore.saveViewModels(outState)
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
    index e1ff0ce628..95b2170d68 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
    @@ -38,6 +38,8 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), Message
     
         private val viewModel: MessageActionsViewModel by fragmentViewModel(MessageActionsViewModel::class)
     
    +    override val showExpanded = true
    +
         private lateinit var actionHandlerModel: ActionsHandler
     
         override fun injectWith(screenComponent: ScreenComponent) {
    
    From 0bcc84cbd61ee2c6a2cc7f0d00e5f0c701e20ee9 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 11 Oct 2019 11:16:38 +0200
    Subject: [PATCH 156/197] Try to fix the bottom sheet showing expanded by
     default #2 Seems ok now
    
    ---
     .../VectorBaseBottomSheetDialogFragment.kt         | 14 +++++++++++++-
     .../readreceipts/DisplayReadReceiptsBottomSheet.kt |  4 +---
     .../timeline/action/MessageActionsBottomSheet.kt   |  1 +
     .../edithistory/ViewEditHistoryBottomSheet.kt      |  1 +
     .../timeline/reactions/ViewReactionsBottomSheet.kt |  1 +
     5 files changed, 17 insertions(+), 4 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt
    index 168aaf87f0..f793795910 100644
    --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt
    @@ -21,6 +21,7 @@ import android.os.Bundle
     import android.os.Parcelable
     import android.view.View
     import android.widget.FrameLayout
    +import androidx.annotation.CallSuper
     import com.airbnb.mvrx.MvRx
     import com.airbnb.mvrx.MvRxView
     import com.airbnb.mvrx.MvRxViewModelStore
    @@ -41,6 +42,8 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
         private lateinit var screenComponent: ScreenComponent
         final override val mvrxViewId: String by lazy { mvrxPersistedViewId }
     
    +    private var bottomSheetBehavior: BottomSheetBehavior? = null
    +
         val vectorBaseActivity: VectorBaseActivity by lazy {
             activity as VectorBaseActivity
         }
    @@ -70,7 +73,8 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
                         val d = dialog as BottomSheetDialog
     
                         val bottomSheet = d.findViewById(com.google.android.material.R.id.design_bottom_sheet) as FrameLayout?
    -                    BottomSheetBehavior.from(bottomSheet!!).state = BottomSheetBehavior.STATE_EXPANDED
    +                    bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet!!)
    +                    bottomSheetBehavior?.state = BottomSheetBehavior.STATE_EXPANDED
                     }
                 }
             }
    @@ -89,6 +93,14 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
             postInvalidate()
         }
     
    +    @CallSuper
    +    override fun invalidate() {
    +        if (showExpanded) {
    +            // Force the bottom sheet to be expanded
    +            bottomSheetBehavior?.state = BottomSheetBehavior.STATE_EXPANDED
    +        }
    +    }
    +
         protected fun setArguments(args: Parcelable? = null) {
             arguments = args?.let { Bundle().apply { putParcelable(MvRx.KEY_ARG, it) } }
         }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt
    index 80539d73e4..6cf5191a6d 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt
    @@ -73,9 +73,7 @@ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() {
             epoxyController.setData(displayReadReceiptArgs.readReceipts)
         }
     
    -    override fun invalidate() {
    -        // we are not using state for this one as it's static
    -    }
    +    // we are not using state for this one as it's static, so no need to override invalidate()
     
         companion object {
             fun newInstance(readReceipts: List): DisplayReadReceiptsBottomSheet {
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
    index 95b2170d68..e209a3d887 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
    @@ -70,6 +70,7 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), Message
     
         override fun invalidate() = withState(viewModel) {
             messageActionsEpoxyController.setData(it)
    +        super.invalidate()
         }
     
         companion object {
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt
    index acdef2058d..f0da92e1dd 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt
    @@ -74,6 +74,7 @@ class ViewEditHistoryBottomSheet : VectorBaseBottomSheetDialogFragment() {
     
         override fun invalidate() = withState(viewModel) {
             epoxyController.setData(it)
    +        super.invalidate()
         }
     
         companion object {
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsBottomSheet.kt
    index deb2a84818..c93c41fa97 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsBottomSheet.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsBottomSheet.kt
    @@ -71,6 +71,7 @@ class ViewReactionsBottomSheet : VectorBaseBottomSheetDialogFragment() {
     
         override fun invalidate() = withState(viewModel) {
             epoxyController.setData(it)
    +        super.invalidate()
         }
     
         companion object {
    
    From abdb83b9fd0bd3e16f416062d4be1eab4f1af0b4 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 11 Oct 2019 13:34:04 +0200
    Subject: [PATCH 157/197] Report content: change log
    
    ---
     CHANGES.md | 1 +
     1 file changed, 1 insertion(+)
    
    diff --git a/CHANGES.md b/CHANGES.md
    index d58b7017d0..1c0e8798de 100644
    --- a/CHANGES.md
    +++ b/CHANGES.md
    @@ -9,6 +9,7 @@ Improvements:
      - Do not upload file too big for the homeserver (#587)
      - Handle read markers (#84)
      - Mark all messages as read (#396)
    + - Add ability to report content (#515)
     
     Other changes:
      - Accessibility improvements to read receipts in the room timeline and reactions emoji chooser
    
    From e39c4a79257b80015523e0c8d0c956490f76a9ba Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 11 Oct 2019 16:41:02 +0200
    Subject: [PATCH 158/197] fix ktlint issue
    
    ---
     .../android/api/session/room/reporting/ReportingService.kt       | 1 -
     .../vector/riotx/features/home/room/detail/RoomDetailFragment.kt | 1 -
     2 files changed, 2 deletions(-)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/reporting/ReportingService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/reporting/ReportingService.kt
    index 9dbc6d0e9e..71ce02ac69 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/reporting/ReportingService.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/reporting/ReportingService.kt
    @@ -29,5 +29,4 @@ interface ReportingService {
          * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-rooms-roomid-report-eventid
          */
         fun reportContent(eventId: String, score: Int, reason: String, callback: MatrixCallback): Cancelable
    -
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    index 4149436166..9aa2f3cccd 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    @@ -848,7 +848,6 @@ class RoomDetailFragment :
             }
         }
     
    -
     // TimelineEventController.Callback ************************************************************
     
         override fun onUrlClicked(url: String): Boolean {
    
    From 6ccd083451956a27e774ee9e6b012112b03ac219 Mon Sep 17 00:00:00 2001
    From: ganfra 
    Date: Fri, 18 Oct 2019 19:29:57 +0200
    Subject: [PATCH 159/197] Bottom sheet: fix RecyclerView usage
    
    ---
     .../VectorBaseBottomSheetDialogFragment.kt    | 12 +++++-----
     .../DisplayReadReceiptsBottomSheet.kt         | 17 +++++++-------
     .../action/MessageActionsBottomSheet.kt       | 22 ++++++++++++++-----
     .../edithistory/ViewEditHistoryBottomSheet.kt | 19 ++++++++--------
     .../reactions/ViewReactionsBottomSheet.kt     | 17 +++++++-------
     .../res/layout/bottom_sheet_generic_list.xml  | 11 ++++++++++
     ... bottom_sheet_generic_list_with_title.xml} | 10 ++++-----
     .../bottom_sheet_generic_recycler_epoxy.xml   | 21 ------------------
     8 files changed, 62 insertions(+), 67 deletions(-)
     create mode 100644 vector/src/main/res/layout/bottom_sheet_generic_list.xml
     rename vector/src/main/res/layout/{bottom_sheet_epoxylist_with_title.xml => bottom_sheet_generic_list_with_title.xml} (75%)
     delete mode 100644 vector/src/main/res/layout/bottom_sheet_generic_recycler_epoxy.xml
    
    diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt
    index f793795910..f3c15b53ca 100644
    --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt
    @@ -30,6 +30,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialog
     import com.google.android.material.bottomsheet.BottomSheetDialogFragment
     import im.vector.riotx.core.di.DaggerScreenComponent
     import im.vector.riotx.core.di.ScreenComponent
    +import im.vector.riotx.core.utils.DimensionConverter
     import java.util.*
     
     /**
    @@ -68,14 +69,11 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
     
         override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
             return super.onCreateDialog(savedInstanceState).apply {
    +            val dialog = this as? BottomSheetDialog
    +            bottomSheetBehavior = dialog?.behavior
    +            bottomSheetBehavior?.setPeekHeight(DimensionConverter(resources).dpToPx(400), false)
                 if (showExpanded) {
    -                setOnShowListener { dialog ->
    -                    val d = dialog as BottomSheetDialog
    -
    -                    val bottomSheet = d.findViewById(com.google.android.material.R.id.design_bottom_sheet) as FrameLayout?
    -                    bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet!!)
    -                    bottomSheetBehavior?.state = BottomSheetBehavior.STATE_EXPANDED
    -                }
    +                bottomSheetBehavior?.state = BottomSheetBehavior.STATE_EXPANDED
                 }
             }
         }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt
    index 6cf5191a6d..031cf06e39 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt
    @@ -23,9 +23,10 @@ import android.view.View
     import android.view.ViewGroup
     import android.widget.LinearLayout
     import androidx.recyclerview.widget.DividerItemDecoration
    +import androidx.recyclerview.widget.LinearLayoutManager
    +import androidx.recyclerview.widget.RecyclerView
     import butterknife.BindView
     import butterknife.ButterKnife
    -import com.airbnb.epoxy.EpoxyRecyclerView
     import com.airbnb.mvrx.MvRx
     import com.airbnb.mvrx.args
     import im.vector.riotx.R
    @@ -33,7 +34,7 @@ import im.vector.riotx.core.di.ScreenComponent
     import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
     import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
     import kotlinx.android.parcel.Parcelize
    -import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.*
    +import kotlinx.android.synthetic.main.bottom_sheet_generic_list_with_title.*
     import javax.inject.Inject
     
     @Parcelize
    @@ -48,8 +49,8 @@ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() {
     
         @Inject lateinit var epoxyController: DisplayReadReceiptsController
     
    -    @BindView(R.id.bottom_sheet_display_reactions_list)
    -    lateinit var epoxyRecyclerView: EpoxyRecyclerView
    +    @BindView(R.id.bottomSheetRecyclerView)
    +    lateinit var recyclerView: RecyclerView
     
         private val displayReadReceiptArgs: DisplayReadReceiptArgs by args()
     
    @@ -58,17 +59,15 @@ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() {
         }
     
         override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    -        val view = inflater.inflate(R.layout.bottom_sheet_epoxylist_with_title, container, false)
    +        val view = inflater.inflate(R.layout.bottom_sheet_generic_list_with_title, container, false)
             ButterKnife.bind(this, view)
             return view
         }
     
         override fun onActivityCreated(savedInstanceState: Bundle?) {
             super.onActivityCreated(savedInstanceState)
    -        epoxyRecyclerView.setController(epoxyController)
    -        val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context,
    -                                                          LinearLayout.VERTICAL)
    -        epoxyRecyclerView.addItemDecoration(dividerItemDecoration)
    +        recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
    +        recyclerView.adapter = epoxyController.adapter
             bottomSheetTitle.text = getString(R.string.read_at)
             epoxyController.setData(displayReadReceiptArgs.readReceipts)
         }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
    index e209a3d887..48ab4cee12 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
    @@ -19,26 +19,34 @@ import android.os.Bundle
     import android.view.LayoutInflater
     import android.view.View
     import android.view.ViewGroup
    +import android.widget.LinearLayout
     import androidx.lifecycle.ViewModelProviders
    +import androidx.recyclerview.widget.DividerItemDecoration
    +import androidx.recyclerview.widget.LinearLayoutManager
    +import androidx.recyclerview.widget.RecyclerView
    +import butterknife.BindView
    +import butterknife.ButterKnife
     import com.airbnb.mvrx.fragmentViewModel
     import com.airbnb.mvrx.withState
     import im.vector.riotx.R
     import im.vector.riotx.core.di.ScreenComponent
     import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
     import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
    -import kotlinx.android.synthetic.main.bottom_sheet_generic_recycler_epoxy.*
    +import kotlinx.android.synthetic.main.bottom_sheet_generic_list_with_title.*
     import javax.inject.Inject
     
     /**
      * Bottom sheet fragment that shows a message preview with list of contextual actions
      */
     class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), MessageActionsEpoxyController.MessageActionsEpoxyControllerListener {
    +
         @Inject lateinit var messageActionViewModelFactory: MessageActionsViewModel.Factory
         @Inject lateinit var messageActionsEpoxyController: MessageActionsEpoxyController
     
    -    private val viewModel: MessageActionsViewModel by fragmentViewModel(MessageActionsViewModel::class)
    +    @BindView(R.id.bottomSheetRecyclerView)
    +    lateinit var recyclerView: RecyclerView
     
    -    override val showExpanded = true
    +    private val viewModel: MessageActionsViewModel by fragmentViewModel(MessageActionsViewModel::class)
     
         private lateinit var actionHandlerModel: ActionsHandler
     
    @@ -47,14 +55,16 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), Message
         }
     
         override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    -        return inflater.inflate(R.layout.bottom_sheet_generic_recycler_epoxy, container, false)
    +        val view = inflater.inflate(R.layout.bottom_sheet_generic_list, container, false)
    +        ButterKnife.bind(this, view)
    +        return view
         }
     
         override fun onActivityCreated(savedInstanceState: Bundle?) {
             super.onActivityCreated(savedInstanceState)
             actionHandlerModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java)
    -
    -        bottomSheetEpoxyRecyclerView.setController(messageActionsEpoxyController)
    +        recyclerView.layoutManager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false)
    +        recyclerView.adapter = messageActionsEpoxyController.adapter
             messageActionsEpoxyController.listener = this
         }
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt
    index f0da92e1dd..709bcb53c7 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt
    @@ -21,9 +21,10 @@ import android.view.View
     import android.view.ViewGroup
     import android.widget.LinearLayout
     import androidx.recyclerview.widget.DividerItemDecoration
    +import androidx.recyclerview.widget.LinearLayoutManager
    +import androidx.recyclerview.widget.RecyclerView
     import butterknife.BindView
     import butterknife.ButterKnife
    -import com.airbnb.epoxy.EpoxyRecyclerView
     import com.airbnb.mvrx.MvRx
     import com.airbnb.mvrx.fragmentViewModel
     import com.airbnb.mvrx.withState
    @@ -33,7 +34,7 @@ import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
     import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs
     import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
     import im.vector.riotx.features.html.EventHtmlRenderer
    -import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.*
    +import kotlinx.android.synthetic.main.bottom_sheet_generic_list_with_title.*
     import javax.inject.Inject
     
     /**
    @@ -46,8 +47,8 @@ class ViewEditHistoryBottomSheet : VectorBaseBottomSheetDialogFragment() {
         @Inject lateinit var viewEditHistoryViewModelFactory: ViewEditHistoryViewModel.Factory
         @Inject lateinit var eventHtmlRenderer: EventHtmlRenderer
     
    -    @BindView(R.id.bottom_sheet_display_reactions_list)
    -    lateinit var epoxyRecyclerView: EpoxyRecyclerView
    +    @BindView(R.id.bottomSheetRecyclerView)
    +    lateinit var recyclerView: RecyclerView
     
         private val epoxyController by lazy {
             ViewEditHistoryEpoxyController(requireContext(), viewModel.dateFormatter, eventHtmlRenderer)
    @@ -58,17 +59,17 @@ class ViewEditHistoryBottomSheet : VectorBaseBottomSheetDialogFragment() {
         }
     
         override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    -        val view = inflater.inflate(R.layout.bottom_sheet_epoxylist_with_title, container, false)
    +        val view = inflater.inflate(R.layout.bottom_sheet_generic_list_with_title, container, false)
             ButterKnife.bind(this, view)
             return view
         }
     
         override fun onActivityCreated(savedInstanceState: Bundle?) {
             super.onActivityCreated(savedInstanceState)
    -        epoxyRecyclerView.setController(epoxyController)
    -        val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context,
    -                LinearLayout.VERTICAL)
    -        epoxyRecyclerView.addItemDecoration(dividerItemDecoration)
    +        recyclerView.adapter = epoxyController.adapter
    +        recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
    +        val dividerItemDecoration = DividerItemDecoration(requireContext(), LinearLayout.VERTICAL)
    +        recyclerView.addItemDecoration(dividerItemDecoration)
             bottomSheetTitle.text = context?.getString(R.string.message_edits)
         }
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsBottomSheet.kt
    index c93c41fa97..a966007fc4 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsBottomSheet.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsBottomSheet.kt
    @@ -22,9 +22,10 @@ import android.view.View
     import android.view.ViewGroup
     import android.widget.LinearLayout
     import androidx.recyclerview.widget.DividerItemDecoration
    +import androidx.recyclerview.widget.LinearLayoutManager
    +import androidx.recyclerview.widget.RecyclerView
     import butterknife.BindView
     import butterknife.ButterKnife
    -import com.airbnb.epoxy.EpoxyRecyclerView
     import com.airbnb.mvrx.MvRx
     import com.airbnb.mvrx.fragmentViewModel
     import com.airbnb.mvrx.withState
    @@ -33,7 +34,7 @@ import im.vector.riotx.core.di.ScreenComponent
     import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
     import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs
     import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
    -import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.*
    +import kotlinx.android.synthetic.main.bottom_sheet_generic_list_with_title.*
     import javax.inject.Inject
     
     /**
    @@ -45,8 +46,8 @@ class ViewReactionsBottomSheet : VectorBaseBottomSheetDialogFragment() {
     
         @Inject lateinit var viewReactionsViewModelFactory: ViewReactionsViewModel.Factory
     
    -    @BindView(R.id.bottom_sheet_display_reactions_list)
    -    lateinit var epoxyRecyclerView: EpoxyRecyclerView
    +    @BindView(R.id.bottomSheetRecyclerView)
    +    lateinit var recyclerView: RecyclerView
     
         @Inject lateinit var epoxyController: ViewReactionsEpoxyController
     
    @@ -55,17 +56,15 @@ class ViewReactionsBottomSheet : VectorBaseBottomSheetDialogFragment() {
         }
     
         override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    -        val view = inflater.inflate(R.layout.bottom_sheet_epoxylist_with_title, container, false)
    +        val view = inflater.inflate(R.layout.bottom_sheet_generic_list_with_title, container, false)
             ButterKnife.bind(this, view)
             return view
         }
     
         override fun onActivityCreated(savedInstanceState: Bundle?) {
             super.onActivityCreated(savedInstanceState)
    -        epoxyRecyclerView.setController(epoxyController)
    -        val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context,
    -                LinearLayout.VERTICAL)
    -        epoxyRecyclerView.addItemDecoration(dividerItemDecoration)
    +        recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
    +        recyclerView.adapter = epoxyController.adapter
             bottomSheetTitle.text = context?.getString(R.string.reactions)
         }
     
    diff --git a/vector/src/main/res/layout/bottom_sheet_generic_list.xml b/vector/src/main/res/layout/bottom_sheet_generic_list.xml
    new file mode 100644
    index 0000000000..69b5ce2fac
    --- /dev/null
    +++ b/vector/src/main/res/layout/bottom_sheet_generic_list.xml
    @@ -0,0 +1,11 @@
    +
    +
    +
    diff --git a/vector/src/main/res/layout/bottom_sheet_epoxylist_with_title.xml b/vector/src/main/res/layout/bottom_sheet_generic_list_with_title.xml
    similarity index 75%
    rename from vector/src/main/res/layout/bottom_sheet_epoxylist_with_title.xml
    rename to vector/src/main/res/layout/bottom_sheet_generic_list_with_title.xml
    index a5e9c980a3..80d877ac2d 100644
    --- a/vector/src/main/res/layout/bottom_sheet_epoxylist_with_title.xml
    +++ b/vector/src/main/res/layout/bottom_sheet_generic_list_with_title.xml
    @@ -3,24 +3,22 @@
         xmlns:tools="http://schemas.android.com/tools"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
    -    android:minHeight="400dp"
         android:orientation="vertical">
     
         
     
    -    
    -
    -
    -
    -
    -    
    -
    -
    \ No newline at end of file
    
    From 053bf7aeac1019f02e55115607ec407261ef9123 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Mon, 21 Oct 2019 16:40:08 +0200
    Subject: [PATCH 160/197] Improve layout preview a bit
    
    ---
     .../src/main/res/layout/item_timeline_event_base.xml  | 11 ++++++++++-
     1 file changed, 10 insertions(+), 1 deletion(-)
    
    diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml
    index ec5cac245d..fbe3b70551 100644
    --- a/vector/src/main/res/layout/item_timeline_event_base.xml
    +++ b/vector/src/main/res/layout/item_timeline_event_base.xml
    @@ -117,7 +117,16 @@
                 android:layout_marginBottom="4dp"
                 app:dividerDrawable="@drawable/reaction_divider"
                 app:flexWrap="wrap"
    -            app:showDivider="middle" />
    +            app:showDivider="middle"
    +            tools:background="#F0E0F0"
    +            tools:layout_height="40dp">
    +
    +            
    +            
    +
    +        
     
             
    Date: Mon, 21 Oct 2019 16:40:27 +0200
    Subject: [PATCH 161/197] Remove unused import
    
    ---
     .../riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt | 1 -
     .../room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt | 2 --
     .../room/detail/timeline/action/MessageActionsBottomSheet.kt   | 3 ---
     .../room/detail/timeline/reactions/ViewReactionsBottomSheet.kt | 2 --
     4 files changed, 8 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt
    index f3c15b53ca..8d40d55a7a 100644
    --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt
    @@ -19,7 +19,6 @@ import android.app.Dialog
     import android.content.Context
     import android.os.Bundle
     import android.os.Parcelable
    -import android.view.View
     import android.widget.FrameLayout
     import androidx.annotation.CallSuper
     import com.airbnb.mvrx.MvRx
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt
    index 031cf06e39..50ade56474 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt
    @@ -21,8 +21,6 @@ import android.os.Parcelable
     import android.view.LayoutInflater
     import android.view.View
     import android.view.ViewGroup
    -import android.widget.LinearLayout
    -import androidx.recyclerview.widget.DividerItemDecoration
     import androidx.recyclerview.widget.LinearLayoutManager
     import androidx.recyclerview.widget.RecyclerView
     import butterknife.BindView
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
    index 48ab4cee12..63424bba37 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
    @@ -19,9 +19,7 @@ import android.os.Bundle
     import android.view.LayoutInflater
     import android.view.View
     import android.view.ViewGroup
    -import android.widget.LinearLayout
     import androidx.lifecycle.ViewModelProviders
    -import androidx.recyclerview.widget.DividerItemDecoration
     import androidx.recyclerview.widget.LinearLayoutManager
     import androidx.recyclerview.widget.RecyclerView
     import butterknife.BindView
    @@ -32,7 +30,6 @@ import im.vector.riotx.R
     import im.vector.riotx.core.di.ScreenComponent
     import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
     import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
    -import kotlinx.android.synthetic.main.bottom_sheet_generic_list_with_title.*
     import javax.inject.Inject
     
     /**
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsBottomSheet.kt
    index a966007fc4..d5df8f7b40 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsBottomSheet.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsBottomSheet.kt
    @@ -20,8 +20,6 @@ import android.os.Bundle
     import android.view.LayoutInflater
     import android.view.View
     import android.view.ViewGroup
    -import android.widget.LinearLayout
    -import androidx.recyclerview.widget.DividerItemDecoration
     import androidx.recyclerview.widget.LinearLayoutManager
     import androidx.recyclerview.widget.RecyclerView
     import butterknife.BindView
    
    From fce576e3a4c66632d4f5682547eaa0a752ff396a Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Mon, 21 Oct 2019 16:41:14 +0200
    Subject: [PATCH 162/197] Message action bottom sheet expanded
    
    ---
     .../room/detail/timeline/action/MessageActionsBottomSheet.kt    | 2 ++
     1 file changed, 2 insertions(+)
    
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
    index 63424bba37..3f46b991de 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
    @@ -45,6 +45,8 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), Message
     
         private val viewModel: MessageActionsViewModel by fragmentViewModel(MessageActionsViewModel::class)
     
    +    override val showExpanded = true
    +
         private lateinit var actionHandlerModel: ActionsHandler
     
         override fun injectWith(screenComponent: ScreenComponent) {
    
    From b253722b9882ade5fda3614242a9438fc5909e34 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Mon, 21 Oct 2019 16:54:00 +0200
    Subject: [PATCH 163/197] Disable animation
    
    ---
     .../room/detail/timeline/action/MessageActionsBottomSheet.kt    | 2 ++
     1 file changed, 2 insertions(+)
    
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
    index 3f46b991de..8aaa7643c2 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
    @@ -64,6 +64,8 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), Message
             actionHandlerModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java)
             recyclerView.layoutManager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false)
             recyclerView.adapter = messageActionsEpoxyController.adapter
    +        // Disable item animation
    +        recyclerView.itemAnimator = null
             messageActionsEpoxyController.listener = this
         }
     
    
    From 8e3234d188275b9477d4694c4fabcec6ff76dbaf Mon Sep 17 00:00:00 2001
    From: ganfra 
    Date: Mon, 21 Oct 2019 18:08:42 +0200
    Subject: [PATCH 164/197] Clean some code
    
    ---
     .gitignore                                      |  2 ++
     CHANGES.md                                      |  3 ++-
     vector/src/main/AndroidManifest.xml             | 14 +++++++-------
     .../riotx/ActiveSessionObservableStore.kt       | 17 +++++++++++++++++
     .../features/attachments/ContactAttachment.kt   |  9 +++++----
     .../home/room/detail/RoomDetailFragment.kt      |  1 +
     .../features/share/IncomingShareActivity.kt     |  8 ++++----
     .../src/main/res/xml/riotx_provider_paths.xml   |  5 -----
     8 files changed, 38 insertions(+), 21 deletions(-)
    
    diff --git a/.gitignore b/.gitignore
    index e54a3c380e..76cd170215 100644
    --- a/.gitignore
    +++ b/.gitignore
    @@ -14,3 +14,5 @@
     /tmp
     
     ktlint
    +.idea/copyright/New_vector.xml
    +.idea/copyright/profiles_settings.xml
    diff --git a/CHANGES.md b/CHANGES.md
    index 5cac7e8f70..38808bfa28 100644
    --- a/CHANGES.md
    +++ b/CHANGES.md
    @@ -8,7 +8,8 @@ Improvements:
      - Persist active tab between sessions (#503)
      - Do not upload file too big for the homeserver (#587)
      - Handle read markers (#84)
    - - Attachments: start using system pickers
    + - Attachments: start using system pickers (#52)
    + - Attachments: start handling incoming share (#58)
     
     Other changes:
      - Accessibility improvements to read receipts in the room timeline and reactions emoji chooser
    diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml
    index 6c0f846997..0c9bac61a1 100644
    --- a/vector/src/main/AndroidManifest.xml
    +++ b/vector/src/main/AndroidManifest.xml
    @@ -80,18 +80,18 @@
                 
             
     
    -        
    +        
                 
    -                
    -                
    +                
    +                
    +
                     
                     
                 
                 
    -                
    -                
    +                
    +                
    +
                     
                     
                 
    diff --git a/vector/src/main/java/im/vector/riotx/ActiveSessionObservableStore.kt b/vector/src/main/java/im/vector/riotx/ActiveSessionObservableStore.kt
    index c10a7f700c..fd6a92e820 100644
    --- a/vector/src/main/java/im/vector/riotx/ActiveSessionObservableStore.kt
    +++ b/vector/src/main/java/im/vector/riotx/ActiveSessionObservableStore.kt
    @@ -1,3 +1,20 @@
    +/*
    + * Copyright 2019 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.riotx
     
     import arrow.core.Option
    diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/ContactAttachment.kt b/vector/src/main/java/im/vector/riotx/features/attachments/ContactAttachment.kt
    index 90340e2ef6..80acefdcf2 100644
    --- a/vector/src/main/java/im/vector/riotx/features/attachments/ContactAttachment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/attachments/ContactAttachment.kt
    @@ -28,10 +28,11 @@ data class ContactAttachment(
     ) {
     
         fun toHumanReadable(): String {
    -        val stringBuilder = StringBuilder(displayName)
    -        phones.concatIn(stringBuilder)
    -        emails.concatIn(stringBuilder)
    -        return stringBuilder.toString()
    +        return buildString {
    +            append(displayName)
    +            phones.concatIn(this)
    +            emails.concatIn(this)
    +        }
         }
     
         private fun List.concatIn(stringBuilder: StringBuilder) {
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    index f30ee4d97d..6fa4fdceaf 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    @@ -1126,6 +1126,7 @@ class RoomDetailFragment :
         }
     
         override fun onContactAttachmentReady(contactAttachment: ContactAttachment) {
    +        super.onContactAttachmentReady(contactAttachment)
             val formattedContact = contactAttachment.toHumanReadable()
             roomDetailViewModel.process(RoomDetailActions.SendMessage(formattedContact, false))
         }
    diff --git a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt
    index 3f50ada057..197405aa53 100644
    --- a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt
    @@ -69,10 +69,10 @@ class IncomingShareActivity :
                     isShareManaged = handleTextShare(intent)
                 }
                 if (!isShareManaged) {
    -                cannottManageShare()
    +                cannotManageShare()
                 }
             } else {
    -            cannottManageShare()
    +            cannotManageShare()
             }
         }
     
    @@ -83,10 +83,10 @@ class IncomingShareActivity :
         }
     
         override fun onAttachmentsProcessFailed() {
    -        cannottManageShare()
    +        cannotManageShare()
         }
     
    -    private fun cannottManageShare() {
    +    private fun cannotManageShare() {
             Toast.makeText(this, R.string.error_handling_incoming_share, Toast.LENGTH_LONG).show()
             finish()
         }
    diff --git a/vector/src/main/res/xml/riotx_provider_paths.xml b/vector/src/main/res/xml/riotx_provider_paths.xml
    index a802c0ff97..7d3fcb2203 100644
    --- a/vector/src/main/res/xml/riotx_provider_paths.xml
    +++ b/vector/src/main/res/xml/riotx_provider_paths.xml
    @@ -3,9 +3,4 @@
         
    -
    -    
    -
     
    \ No newline at end of file
    
    From 11b5c2c3bae88d28a27cf29d953aa8ead876ec05 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Mon, 21 Oct 2019 18:17:03 +0200
    Subject: [PATCH 165/197] Restore previous log level
    
    ---
     gradle.properties | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/gradle.properties b/gradle.properties
    index 35ca815df8..2e2b110f15 100644
    --- a/gradle.properties
    +++ b/gradle.properties
    @@ -16,7 +16,7 @@ org.gradle.jvmargs=-Xmx1536m
     
     
     vector.debugPrivateData=false
    -vector.httpLogLevel=HEADERS
    +vector.httpLogLevel=NONE
     
     # Note: to debug, you can put and uncomment the following lines in the file ~/.gradle/gradle.properties to override the value above
     #vector.debugPrivateData=true
    
    From 2c8cd895339473a1899e425d8b075496e618cfb4 Mon Sep 17 00:00:00 2001
    From: ganfra 
    Date: Mon, 21 Oct 2019 19:02:28 +0200
    Subject: [PATCH 166/197] Handle rich content from app (WIP not compiling)
    
    ---
     .../detail/composer/TextComposerEditText.kt   | 55 +++++++++++++++++++
     ...constraint_set_composer_layout_compact.xml |  2 +-
     ...onstraint_set_composer_layout_expanded.xml |  2 +-
     .../main/res/layout/merge_composer_layout.xml |  2 +-
     4 files changed, 58 insertions(+), 3 deletions(-)
     create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerEditText.kt
    
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerEditText.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerEditText.kt
    new file mode 100644
    index 0000000000..04d63f39b8
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerEditText.kt
    @@ -0,0 +1,55 @@
    +/*
    + * Copyright 2019 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.riotx.features.home.room.detail.composer
    +
    +import android.content.Context
    +import android.os.Build
    +import android.util.AttributeSet
    +import android.view.inputmethod.EditorInfo
    +import android.view.inputmethod.InputConnection
    +import androidx.appcompat.widget.AppCompatEditText
    +import androidx.core.view.inputmethod.EditorInfoCompat
    +import androidx.core.view.inputmethod.InputConnectionCompat
    +
    +class TextComposerEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0)
    +    : AppCompatEditText(context, attrs, defStyleAttr) {
    +
    +    override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection {
    +        val ic: InputConnection = super.onCreateInputConnection(editorInfo)
    +        EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/png"))
    +
    +        val callback =
    +                InputConnectionCompat.OnCommitContentListener { inputContentInfo, flags, opts ->
    +                    val lacksPermission = (flags and
    +                            InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0
    +                    // read and display inputContentInfo asynchronously
    +                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && lacksPermission) {
    +                        try {
    +                            inputContentInfo.requestPermission()
    +                        } catch (e: Exception) {
    +                            return@OnCommitContentListener false // return false if failed
    +                        }
    +                    }
    +                    // read and display inputContentInfo asynchronously.
    +                    // call inputContentInfo.releasePermission() as needed.
    +                    true  // return true if succeeded
    +                }
    +        return InputConnectionCompat.createWrapper(ic, editorInfo, callback)
    +    }
    +
    +}
    diff --git a/vector/src/main/res/layout/constraint_set_composer_layout_compact.xml b/vector/src/main/res/layout/constraint_set_composer_layout_compact.xml
    index ffc77da3fb..b2fff0880b 100644
    --- a/vector/src/main/res/layout/constraint_set_composer_layout_compact.xml
    +++ b/vector/src/main/res/layout/constraint_set_composer_layout_compact.xml
    @@ -143,7 +143,7 @@
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toEndOf="@id/attachmentButton" />
     
    -    
     
    -    
     
    -    
    Date: Mon, 21 Oct 2019 22:29:36 +0100
    Subject: [PATCH 167/197] Address review comments.
    
    Signed-off-by: Dominic Fischer 
    ---
     .../vector/matrix/android/internal/crypto/DeviceListManager.kt | 2 +-
     .../android/internal/crypto/IncomingRoomKeyRequestManager.kt   | 2 +-
     .../internal/session/room/timeline/TimelineEventDecryptor.kt   | 3 ++-
     vector/src/main/java/im/vector/riotx/core/utils/FileUtils.kt   | 2 +-
     .../vector/riotx/features/home/room/list/RoomListViewModel.kt  | 2 +-
     5 files changed, 6 insertions(+), 5 deletions(-)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt
    index 5873bb25aa..7f2a23e4c2 100755
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt
    @@ -63,7 +63,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
         private fun canRetryKeysDownload(userId: String): Boolean {
             var res = false
     
    -        if (userId.isNotEmpty() && userId.contains(":")) {
    +        if (':' in userId) {
                 try {
                     synchronized(notReadyToRetryHS) {
                         res = !notReadyToRetryHS.contains(userId.substring(userId.lastIndexOf(":") + 1))
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt
    index 368293d92f..3c8d70f2f1 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt
    @@ -95,7 +95,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
                     continue
                 }
     
    -            if (deviceId == credentials.deviceId && credentials.userId == userId) {
    +            if (credentials.deviceId == deviceId && credentials.userId == userId) {
                     Timber.v("## processReceivedRoomKeyRequests() : oneself device - ignored")
                     cryptoStore.deleteIncomingRoomKeyRequest(request)
                     continue
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt
    index 555a19d842..e3b57949f4 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt
    @@ -78,9 +78,10 @@ internal class TimelineEventDecryptor(
                 existingRequests.add(eventId)
             }
             synchronized(unknownSessionsFailure) {
    -            unknownSessionsFailure.values.forEach {
    +            for (it in unknownSessionsFailure.values) {
                     if (eventId in it) {
                         Timber.d("Skip Decryption request for event $eventId, unknown session")
    +                    break
                     }
                 }
             }
    diff --git a/vector/src/main/java/im/vector/riotx/core/utils/FileUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/FileUtils.kt
    index 915a11b5de..0009fd46f9 100644
    --- a/vector/src/main/java/im/vector/riotx/core/utils/FileUtils.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/utils/FileUtils.kt
    @@ -132,5 +132,5 @@ fun getSizeOfFiles(context: Context, root: File): Int {
                     Timber.v("Get size of ${it.absolutePath}")
                     true
                 }
    -            .sumBy { root.length().toInt() }
    +            .sumBy { it.length().toInt() }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt
    index 7a654171d9..fcdb2f3138 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt
    @@ -148,7 +148,7 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room
                     setState {
                         copy(
                                 joiningRoomsIds = joiningRoomsIds - roomId,
    -                            joiningErrorRoomsIds = joiningErrorRoomsIds - roomId
    +                            joiningErrorRoomsIds = joiningErrorRoomsIds + roomId
                         )
                     }
                 }
    
    From 5e4e54153c8a9049c19d49ec10b6607f73f2da01 Mon Sep 17 00:00:00 2001
    From: Dominic Fischer 
    Date: Mon, 21 Oct 2019 22:52:58 +0100
    Subject: [PATCH 168/197] Fix build error.
    
    Signed-off-by: Dominic Fischer 
    ---
     .../internal/crypto/keysbackup/KeysBackup.kt       | 14 ++++++--------
     1 file changed, 6 insertions(+), 8 deletions(-)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt
    index b90ba58b88..ab665dbf99 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt
    @@ -630,14 +630,12 @@ internal class KeysBackup @Inject constructor(
                         }
     
                         // Get a PK decryption instance
    -                    val decryption = pkDecryptionFromRecoveryKey(recoveryKey)
    -                    if (decryption == null) {
    -                        // This should not happen anymore
    -                        Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key. Error")
    -                        throw InvalidParameterException("Invalid recovery key")
    -                    }
    -
    -                    decryption
    +                    pkDecryptionFromRecoveryKey(recoveryKey)
    +                }
    +                if (decryption == null) {
    +                    // This should not happen anymore
    +                    Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key. Error")
    +                    throw InvalidParameterException("Invalid recovery key")
                     }
     
                     stepProgressListener?.onStepProgress(StepProgressListener.Step.DownloadingKey)
    
    From 64b3568d5178b6f3ae1d07e6f88380922afe5569 Mon Sep 17 00:00:00 2001
    From: Valere 
    Date: Tue, 22 Oct 2019 11:57:11 +0200
    Subject: [PATCH 169/197] Fix / event mapper persist the clear type in type
    
    ---
     .../android/internal/database/helper/ChunkEntityHelper.kt       | 2 +-
     .../matrix/android/internal/database/mapper/EventMapper.kt      | 2 +-
     2 files changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt
    index 4c65fc1e64..e9ffa140c9 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt
    @@ -124,7 +124,7 @@ internal fun ChunkEntity.add(roomId: String,
             backwardsDisplayIndex = currentDisplayIndex
         }
         var currentStateIndex = lastStateIndex(direction, defaultValue = stateIndexOffset)
    -    if (direction == PaginationDirection.FORWARDS && EventType.isStateEvent(event.getClearType())) {
    +    if (direction == PaginationDirection.FORWARDS && EventType.isStateEvent(event.type)) {
             currentStateIndex += 1
             forwardsStateIndex = currentStateIndex
         } else if (direction == PaginationDirection.BACKWARDS && timelineEvents.isNotEmpty()) {
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt
    index daeb7b241f..ed5f04ef75 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt
    @@ -37,7 +37,7 @@ internal object EventMapper {
             val resolvedPrevContent = event.prevContent ?: event.unsignedData?.prevContent
             eventEntity.prevContent = ContentMapper.map(resolvedPrevContent)
             eventEntity.stateKey = event.stateKey
    -        eventEntity.type = event.getClearType()
    +        eventEntity.type = event.type
             eventEntity.sender = event.senderId
             eventEntity.originServerTs = event.originServerTs
             eventEntity.redacts = event.redacts
    
    From 3dc5ef54abf002c910e8c9826133fd54efee11e2 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Tue, 22 Oct 2019 12:21:50 +0200
    Subject: [PATCH 170/197] Fix compilation warnings
    
    ---
     .../matrix/android/api/session/events/model/Event.kt       | 2 +-
     .../crypto/algorithms/megolm/MXMegolmDecryption.kt         | 7 -------
     .../internal/crypto/algorithms/olm/MXOlmEncryption.kt      | 2 +-
     .../src/main/java/im/vector/riotx/core/utils/FileUtils.kt  | 2 +-
     .../features/settings/VectorSettingsGeneralFragment.kt     | 6 ++----
     5 files changed, 5 insertions(+), 14 deletions(-)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt
    index 7aea73233d..bc6885eddc 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt
    @@ -134,7 +134,7 @@ data class Event(
         }
     
         fun toContentStringWithIndent(): String {
    -        val contentMap = toContent()?.toMutableMap() ?: HashMap()
    +        val contentMap = toContent().toMutableMap()
             return JSONObject(contentMap).toString(4)
         }
     
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt
    index 6ffaf776b2..0230141e1b 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt
    @@ -244,13 +244,6 @@ internal class MXMegolmDecryption(private val userId: String,
                 keysClaimed = event.getKeysClaimed().toMutableMap()
             }
     
    -        if (roomKeyContent.sessionId == null
    -                || roomKeyContent.sessionKey == null
    -                || roomKeyContent.roomId == null) {
    -            Timber.e("## invalid roomKeyContent")
    -            return
    -        }
    -
             val added = olmDevice.addInboundGroupSession(roomKeyContent.sessionId,
                     roomKeyContent.sessionKey,
                     roomKeyContent.roomId,
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmEncryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmEncryption.kt
    index b6d1b98546..1c275940af 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmEncryption.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmEncryption.kt
    @@ -66,7 +66,7 @@ internal class MXOlmEncryption(
             )
     
             messageEncrypter.encryptMessage(messageMap, deviceInfos)
    -        return messageMap.toContent()!!
    +        return messageMap.toContent()
         }
     
         /**
    diff --git a/vector/src/main/java/im/vector/riotx/core/utils/FileUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/FileUtils.kt
    index 0009fd46f9..1a19b49872 100644
    --- a/vector/src/main/java/im/vector/riotx/core/utils/FileUtils.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/utils/FileUtils.kt
    @@ -126,7 +126,7 @@ fun getFileExtension(fileUri: String): String? {
      * Size
      * ========================================================================================== */
     
    -fun getSizeOfFiles(context: Context, root: File): Int {
    +fun getSizeOfFiles(root: File): Int {
         return root.walkTopDown()
                 .onEnter {
                     Timber.v("Get size of ${it.absolutePath}")
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt
    index 7ad7ba5ce1..331b6e935a 100644
    --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt
    @@ -185,8 +185,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
     
             // clear medias cache
             findPreference(VectorPreferences.SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY)!!.let {
    -            val size = getSizeOfFiles(requireContext(),
    -                    File(requireContext().cacheDir, DiskCache.Factory.DEFAULT_DISK_CACHE_DIR))
    +            val size = getSizeOfFiles(File(requireContext().cacheDir, DiskCache.Factory.DEFAULT_DISK_CACHE_DIR))
     
                 it.summary = TextUtils.formatFileSize(requireContext(), size.toLong())
     
    @@ -203,8 +202,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
                             // On BG thread
                             Glide.get(requireContext()).clearDiskCache()
     
    -                        newSize = getSizeOfFiles(requireContext(),
    -                                File(requireContext().cacheDir, DiskCache.Factory.DEFAULT_DISK_CACHE_DIR))
    +                        newSize = getSizeOfFiles(File(requireContext().cacheDir, DiskCache.Factory.DEFAULT_DISK_CACHE_DIR))
                         }
     
                         it.summary = TextUtils.formatFileSize(requireContext(), newSize.toLong())
    
    From fea54952d3bca1bc4c9e291f7e81d86969b63ff1 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Tue, 22 Oct 2019 12:26:56 +0200
    Subject: [PATCH 171/197] Code quality
    
    ---
     .../crypto/actions/EnsureOlmSessionsForUsersAction.kt         | 4 ++--
     1 file changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt
    index 3fd833dfb4..0c649cce89 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt
    @@ -38,9 +38,9 @@ internal class EnsureOlmSessionsForUsersAction @Inject constructor(private val o
     
                 devices.filter {
                     // Don't bother setting up session to ourself
    -                it.identityKey() != olmDevice.deviceCurve25519Key &&
    +                it.identityKey() != olmDevice.deviceCurve25519Key
                             // Don't bother setting up sessions with blocked users
    -                        !it.isVerified
    +                        && !it.isVerified
                 }
             }
             return ensureOlmSessionsForDevicesAction.handle(devicesByUser)
    
    From b57c71b1c9486939dc72ce3d2489a1248101af22 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Tue, 22 Oct 2019 12:27:55 +0200
    Subject: [PATCH 172/197] Remove unused import
    
    ---
     .../matrix/android/internal/crypto/DefaultCryptoService.kt    | 1 -
     .../matrix/android/internal/crypto/keysbackup/KeysBackup.kt   | 4 ----
     2 files changed, 5 deletions(-)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt
    index bb629fd881..cf5506a443 100755
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt
    @@ -21,7 +21,6 @@ package im.vector.matrix.android.internal.crypto
     import android.content.Context
     import android.os.Handler
     import android.os.Looper
    -import arrow.core.Try
     import com.squareup.moshi.Types
     import com.zhuinden.monarchy.Monarchy
     import dagger.Lazy
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt
    index e640e807c8..b3ee138591 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt
    @@ -21,7 +21,6 @@ import android.os.Looper
     import androidx.annotation.UiThread
     import androidx.annotation.VisibleForTesting
     import androidx.annotation.WorkerThread
    -import arrow.core.Try
     import im.vector.matrix.android.api.MatrixCallback
     import im.vector.matrix.android.api.auth.data.Credentials
     import im.vector.matrix.android.api.failure.Failure
    @@ -70,9 +69,6 @@ import org.matrix.olm.OlmPkMessage
     import timber.log.Timber
     import java.security.InvalidParameterException
     import javax.inject.Inject
    -import kotlin.coroutines.resume
    -import kotlin.coroutines.resumeWithException
    -import kotlin.coroutines.suspendCoroutine
     import kotlin.random.Random
     
     /**
    
    From 3c40f64fb7b61257c22256f1b5a2d3f7925d64f6 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Tue, 22 Oct 2019 12:33:25 +0200
    Subject: [PATCH 173/197] Add a few comments
    
    ---
     .../session/room/timeline/TimelineEventDecryptor.kt         | 6 ++++--
     1 file changed, 4 insertions(+), 2 deletions(-)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt
    index e3b57949f4..276c458811 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt
    @@ -53,8 +53,10 @@ internal class TimelineEventDecryptor(
     
         private var executor: ExecutorService? = null
     
    -    private val existingRequests = HashSet()
    -    private val unknownSessionsFailure = HashMap>()
    +    // Set of eventIds which are currently decrypting
    +    private val existingRequests = mutableSetOf()
    +    // sessionId -> list of eventIds
    +    private val unknownSessionsFailure = mutableMapOf>()
     
         fun start() {
             executor = Executors.newSingleThreadExecutor()
    
    From 7416fec93e2f5de866822ceaaf7fed7f4f5e33eb Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Tue, 22 Oct 2019 12:37:17 +0200
    Subject: [PATCH 174/197] Do not decrypt event if session is unknown
    
    ---
     .../session/room/timeline/TimelineEventDecryptor.kt         | 6 +++---
     1 file changed, 3 insertions(+), 3 deletions(-)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt
    index 276c458811..7c014f9e80 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt
    @@ -80,10 +80,10 @@ internal class TimelineEventDecryptor(
                 existingRequests.add(eventId)
             }
             synchronized(unknownSessionsFailure) {
    -            for (it in unknownSessionsFailure.values) {
    -                if (eventId in it) {
    +            for (eventIds in unknownSessionsFailure.values) {
    +                if (eventId in eventIds) {
                         Timber.d("Skip Decryption request for event $eventId, unknown session")
    -                    break
    +                    return
                     }
                 }
             }
    
    From c7a4d3419211b47f109b158531eda8897fa02380 Mon Sep 17 00:00:00 2001
    From: ganfra 
    Date: Tue, 22 Oct 2019 12:37:59 +0200
    Subject: [PATCH 175/197] Attachments: handle rich content from keyboard
    
    ---
     .../features/attachments/AttachmentsHelper.kt | 21 ++++++++-------
     .../home/room/detail/RoomDetailFragment.kt    | 13 ++++++++++
     ...omposerEditText.kt => ComposerEditText.kt} | 26 +++++++++++--------
     .../room/detail/composer/TextComposerView.kt  | 15 +++++++++--
     .../features/share/IncomingShareActivity.kt   |  5 +++-
     ...constraint_set_composer_layout_compact.xml |  2 +-
     ...onstraint_set_composer_layout_expanded.xml |  2 +-
     .../main/res/layout/merge_composer_layout.xml |  2 +-
     8 files changed, 59 insertions(+), 27 deletions(-)
     rename vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/{TextComposerEditText.kt => ComposerEditText.kt} (76%)
    
    diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt
    index 1e2d59a4c3..8a4a0d9309 100644
    --- a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt
    @@ -16,12 +16,12 @@
     package im.vector.riotx.features.attachments
     
     import android.app.Activity
    +import android.content.Context
     import android.content.Intent
     import android.os.Bundle
     import androidx.fragment.app.Fragment
     import com.kbeanie.multipicker.api.Picker.*
     import com.kbeanie.multipicker.core.PickerManager
    -import com.kbeanie.multipicker.utils.IntentUtils
     import im.vector.matrix.android.BuildConfig
     import im.vector.matrix.android.api.session.content.ContentAttachmentData
     import im.vector.riotx.core.platform.Restorable
    @@ -34,15 +34,16 @@ private const val PENDING_TYPE_KEY = "PENDING_TYPE_KEY"
      * This class helps to handle attachments by providing simple methods.
      * The process is asynchronous and you must implement [Callback] methods to get the data or a failure.
      */
    -class AttachmentsHelper private constructor(private val pickerManagerFactory: PickerManagerFactory) : Restorable {
    +class AttachmentsHelper private constructor(private val context: Context,
    +                                            private val pickerManagerFactory: PickerManagerFactory) : Restorable {
     
         companion object {
             fun create(fragment: Fragment, callback: Callback): AttachmentsHelper {
    -            return AttachmentsHelper(FragmentPickerManagerFactory(fragment, callback))
    +            return AttachmentsHelper(fragment.requireContext(), FragmentPickerManagerFactory(fragment, callback))
             }
     
             fun create(activity: Activity, callback: Callback): AttachmentsHelper {
    -            return AttachmentsHelper(ActivityPickerManagerFactory(activity, callback))
    +            return AttachmentsHelper(activity, ActivityPickerManagerFactory(activity, callback))
             }
         }
     
    @@ -163,16 +164,16 @@ class AttachmentsHelper private constructor(private val pickerManagerFactory: Pi
          *
          * @return true if it can handle the intent data, false otherwise
          */
    -    fun handleShare(intent: Intent): Boolean {
    -        val type = intent.type ?: return false
    +    fun handleShareIntent(intent: Intent): Boolean {
    +        val type = intent.resolveType(context) ?: return false
             if (type.startsWith("image")) {
    -            imagePicker.submit(IntentUtils.getPickerIntentForSharing(intent))
    +            imagePicker.submit(intent)
             } else if (type.startsWith("video")) {
    -            videoPicker.submit(IntentUtils.getPickerIntentForSharing(intent))
    +            videoPicker.submit(intent)
             } else if (type.startsWith("audio")) {
    -            videoPicker.submit(IntentUtils.getPickerIntentForSharing(intent))
    +            videoPicker.submit(intent)
             } else if (type.startsWith("application") || type.startsWith("file") || type.startsWith("*")) {
    -            filePicker.submit(IntentUtils.getPickerIntentForSharing(intent))
    +            filePicker.submit(intent)
             } else {
                 return false
             }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    index 6fa4fdceaf..567650c606 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    @@ -605,6 +605,19 @@ class RoomDetailFragment :
             composerLayout.composerRelatedMessageCloseButton.setOnClickListener {
                 roomDetailViewModel.process(RoomDetailActions.ExitSpecialMode(composerLayout.composerEditText.text.toString()))
             }
    +        composerLayout.callback = object : TextComposerView.Callback {
    +            override fun onRichContentSelected(contentUri: Uri): Boolean {
    +                val shareIntent = Intent().apply {
    +                    action = Intent.ACTION_SEND
    +                    data = contentUri
    +                }
    +                val isHandled = attachmentsHelper.handleShareIntent(shareIntent)
    +                if (!isHandled) {
    +                    Toast.makeText(requireContext(), R.string.error_handling_incoming_share, Toast.LENGTH_SHORT).show()
    +                }
    +                return isHandled
    +            }
    +        }
         }
     
         private fun setupAttachmentButton() {
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerEditText.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt
    similarity index 76%
    rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerEditText.kt
    rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt
    index 04d63f39b8..3b1165b016 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerEditText.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt
    @@ -18,38 +18,42 @@
     package im.vector.riotx.features.home.room.detail.composer
     
     import android.content.Context
    +import android.net.Uri
     import android.os.Build
     import android.util.AttributeSet
     import android.view.inputmethod.EditorInfo
     import android.view.inputmethod.InputConnection
    -import androidx.appcompat.widget.AppCompatEditText
    +import android.widget.EditText
     import androidx.core.view.inputmethod.EditorInfoCompat
     import androidx.core.view.inputmethod.InputConnectionCompat
     
    -class TextComposerEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0)
    -    : AppCompatEditText(context, attrs, defStyleAttr) {
    +class ComposerEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = android.R.attr.editTextStyle)
    +    : EditText(context, attrs, defStyleAttr) {
    +
    +    interface Callback {
    +        fun onRichContentSelected(contentUri: Uri): Boolean
    +    }
    +
    +    var callback: Callback? = null
     
         override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection {
             val ic: InputConnection = super.onCreateInputConnection(editorInfo)
    -        EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/png"))
    +        EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("*/*"))
     
             val callback =
    -                InputConnectionCompat.OnCommitContentListener { inputContentInfo, flags, opts ->
    +                InputConnectionCompat.OnCommitContentListener { inputContentInfo, flags, _ ->
                         val lacksPermission = (flags and
                                 InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0
    -                    // read and display inputContentInfo asynchronously
                         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && lacksPermission) {
                             try {
                                 inputContentInfo.requestPermission()
                             } catch (e: Exception) {
    -                            return@OnCommitContentListener false // return false if failed
    +                            return@OnCommitContentListener false
                             }
                         }
    -                    // read and display inputContentInfo asynchronously.
    -                    // call inputContentInfo.releasePermission() as needed.
    -                    true  // return true if succeeded
    +                    callback?.onRichContentSelected(inputContentInfo.contentUri) ?: false
    +
                     }
             return InputConnectionCompat.createWrapper(ic, editorInfo, callback)
         }
    -
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt
    index 62df5d5e95..192a3a6977 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt
    @@ -17,9 +17,9 @@
     package im.vector.riotx.features.home.room.detail.composer
     
     import android.content.Context
    +import android.net.Uri
     import android.util.AttributeSet
     import android.view.ViewGroup
    -import android.widget.EditText
     import android.widget.ImageButton
     import android.widget.ImageView
     import android.widget.TextView
    @@ -39,6 +39,12 @@ import im.vector.riotx.R
     class TextComposerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
                                                      defStyleAttr: Int = 0) : ConstraintLayout(context, attrs, defStyleAttr) {
     
    +    interface Callback {
    +        fun onRichContentSelected(contentUri: Uri): Boolean
    +    }
    +
    +    var callback: Callback? = null
    +
         @BindView(R.id.composer_related_message_sender)
         lateinit var composerRelatedMessageTitle: TextView
         @BindView(R.id.composer_related_message_preview)
    @@ -50,7 +56,7 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib
         @BindView(R.id.composer_related_message_close)
         lateinit var composerRelatedMessageCloseButton: ImageButton
         @BindView(R.id.composerEditText)
    -    lateinit var composerEditText: EditText
    +    lateinit var composerEditText: ComposerEditText
         @BindView(R.id.composer_avatar_view)
         lateinit var composerAvatarImageView: ImageView
     
    @@ -62,6 +68,11 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib
             inflate(context, R.layout.merge_composer_layout, this)
             ButterKnife.bind(this)
             collapse(false)
    +        composerEditText.callback = object : Callback, ComposerEditText.Callback {
    +            override fun onRichContentSelected(contentUri: Uri): Boolean {
    +                return callback?.onRichContentSelected(contentUri) ?: false
    +            }
    +        }
         }
     
         fun collapse(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) {
    diff --git a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt
    index 197405aa53..0d2f9ee040 100644
    --- a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt
    @@ -20,6 +20,7 @@ import android.content.ClipDescription
     import android.content.Intent
     import android.os.Bundle
     import android.widget.Toast
    +import com.kbeanie.multipicker.utils.IntentUtils
     import im.vector.matrix.android.api.session.content.ContentAttachmentData
     import im.vector.riotx.R
     import im.vector.riotx.core.di.ActiveSessionHolder
    @@ -64,7 +65,9 @@ class IncomingShareActivity :
             }
             attachmentsHelper = AttachmentsHelper.create(this, this).register()
             if (intent?.action == Intent.ACTION_SEND || intent?.action == Intent.ACTION_SEND_MULTIPLE) {
    -            var isShareManaged = attachmentsHelper.handleShare(intent)
    +            var isShareManaged = attachmentsHelper.handleShareIntent(
    +                    IntentUtils.getPickerIntentForSharing(intent)
    +            )
                 if (!isShareManaged) {
                     isShareManaged = handleTextShare(intent)
                 }
    diff --git a/vector/src/main/res/layout/constraint_set_composer_layout_compact.xml b/vector/src/main/res/layout/constraint_set_composer_layout_compact.xml
    index b2fff0880b..ac04dfe3ec 100644
    --- a/vector/src/main/res/layout/constraint_set_composer_layout_compact.xml
    +++ b/vector/src/main/res/layout/constraint_set_composer_layout_compact.xml
    @@ -143,7 +143,7 @@
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toEndOf="@id/attachmentButton" />
     
    -    
     
    -    
     
    -    
    Date: Tue, 22 Oct 2019 12:42:01 +0200
    Subject: [PATCH 176/197] Fix compilation issue
    
    ---
     .../riotx/gplay/push/fcm/VectorFirebaseMessagingService.kt    | 4 ++--
     1 file changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/vector/src/gplay/java/im/vector/riotx/gplay/push/fcm/VectorFirebaseMessagingService.kt b/vector/src/gplay/java/im/vector/riotx/gplay/push/fcm/VectorFirebaseMessagingService.kt
    index 271d747fef..7feb89879e 100755
    --- a/vector/src/gplay/java/im/vector/riotx/gplay/push/fcm/VectorFirebaseMessagingService.kt
    +++ b/vector/src/gplay/java/im/vector/riotx/gplay/push/fcm/VectorFirebaseMessagingService.kt
    @@ -213,10 +213,10 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
                     }
                 } else {
                     if (notifiableEvent is NotifiableMessageEvent) {
    -                    if (notifiableEvent.senderName.isEmpty()) {
    +                    if (notifiableEvent.senderName.isNullOrEmpty()) {
                             notifiableEvent.senderName = data["sender_display_name"] ?: data["sender"] ?: ""
                         }
    -                    if (notifiableEvent.roomName.isEmpty()) {
    +                    if (notifiableEvent.roomName.isNullOrEmpty()) {
                             notifiableEvent.roomName = findRoomNameBestEffort(data, session) ?: ""
                         }
                     }
    
    From 3abce34484dc126ab6d28149dbb4c1e4e315a9b2 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Tue, 22 Oct 2019 12:45:36 +0200
    Subject: [PATCH 177/197] Add in existingRequest only if not filtered
    
    ---
     .../room/timeline/TimelineEventDecryptor.kt        | 14 +++++++-------
     1 file changed, 7 insertions(+), 7 deletions(-)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt
    index 7c014f9e80..b8be58779c 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt
    @@ -72,13 +72,6 @@ internal class TimelineEventDecryptor(
         }
     
         fun requestDecryption(eventId: String) {
    -        synchronized(existingRequests) {
    -            if (eventId in existingRequests) {
    -                Timber.d("Skip Decryption request for event $eventId, already requested")
    -                return
    -            }
    -            existingRequests.add(eventId)
    -        }
             synchronized(unknownSessionsFailure) {
                 for (eventIds in unknownSessionsFailure.values) {
                     if (eventId in eventIds) {
    @@ -87,6 +80,13 @@ internal class TimelineEventDecryptor(
                     }
                 }
             }
    +        synchronized(existingRequests) {
    +            if (eventId in existingRequests) {
    +                Timber.d("Skip Decryption request for event $eventId, already requested")
    +                return
    +            }
    +            existingRequests.add(eventId)
    +        }
             executor?.execute {
                 Realm.getInstance(realmConfiguration).use { realm ->
                     processDecryptRequest(eventId, realm)
    
    From 78dfd6b3e622a4e155bb83b607716fdac6070b88 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Tue, 22 Oct 2019 14:20:43 +0200
    Subject: [PATCH 178/197] Fix potential lock due to nested
     `synchronized(unknownSessionsFailure)`
    
    ---
     .../room/timeline/TimelineEventDecryptor.kt   | 27 ++++++++++---------
     1 file changed, 15 insertions(+), 12 deletions(-)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt
    index b8be58779c..62ae5f621b 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt
    @@ -38,15 +38,14 @@ internal class TimelineEventDecryptor(
         private val newSessionListener = object : NewSessionListener {
             override fun onNewSession(roomId: String?, senderKey: String, sessionId: String) {
                 synchronized(unknownSessionsFailure) {
    -                val toDecryptAgain = ArrayList()
    -                val eventIds = unknownSessionsFailure[sessionId]
    -                if (eventIds != null) toDecryptAgain.addAll(eventIds)
    -                if (toDecryptAgain.isNotEmpty()) {
    -                    eventIds?.clear()
    -                    toDecryptAgain.forEach {
    -                        requestDecryption(it)
    -                    }
    -                }
    +                unknownSessionsFailure[sessionId]
    +                        .orEmpty()
    +                        .toList()
    +                        .also {
    +                            unknownSessionsFailure[sessionId]?.clear()
    +                        }
    +            }.forEach {
    +                requestDecryption(it)
                 }
             }
         }
    @@ -67,8 +66,12 @@ internal class TimelineEventDecryptor(
             cryptoService.removeSessionListener(newSessionListener)
             executor?.shutdownNow()
             executor = null
    -        unknownSessionsFailure.clear()
    -        existingRequests.clear()
    +        synchronized(unknownSessionsFailure) {
    +            unknownSessionsFailure.clear()
    +        }
    +        synchronized(existingRequests) {
    +            existingRequests.clear()
    +        }
         }
     
         fun requestDecryption(eventId: String) {
    @@ -108,7 +111,7 @@ internal class TimelineEventDecryptor(
                     eventEntity.setDecryptionResult(result)
                 }
             } catch (e: MXCryptoError) {
    -            Timber.v("Failed to decrypt event $eventId $e")
    +            Timber.v(e, "Failed to decrypt event $eventId")
                 if (e is MXCryptoError.Base && e.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
                     // Keep track of unknown sessions to automatically try to decrypt on new session
                     realm.executeTransaction {
    
    From 492ed3954a7798f4a83219497495131ed63dafa1 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Tue, 22 Oct 2019 10:20:51 +0200
    Subject: [PATCH 179/197] code cleanup
    
    ---
     .../matrix/android/internal/extensions/RealmExtensions.kt     | 4 +---
     .../matrix/android/internal/session/room/send/TextContent.kt  | 1 -
     2 files changed, 1 insertion(+), 4 deletions(-)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/extensions/RealmExtensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/extensions/RealmExtensions.kt
    index 8e658aab5c..8934bdb0b6 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/extensions/RealmExtensions.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/extensions/RealmExtensions.kt
    @@ -19,7 +19,5 @@ package im.vector.matrix.android.internal.extensions
     import io.realm.RealmObject
     
     internal fun RealmObject.assertIsManaged() {
    -    if (!isManaged) {
    -        throw IllegalStateException("${javaClass.simpleName} entity should be managed to use this function")
    -    }
    +    check(isManaged) { "${javaClass.simpleName} entity should be managed to use this function" }
     }
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/TextContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/TextContent.kt
    index ff31fc30c8..27b68e95eb 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/TextContent.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/TextContent.kt
    @@ -26,7 +26,6 @@ import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply
      */
     data class TextContent(
             val text: String,
    -
             val formattedText: String? = null
     ) {
         fun takeFormatted() = formattedText ?: text
    
    From f43dcb1183d8a65079c3c44912d508f0a57923c9 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Tue, 22 Oct 2019 15:29:06 +0200
    Subject: [PATCH 180/197] Update room summary when saving local echo from
     DefaultRelationService
    
    ---
     .../room/relation/DefaultRelationService.kt   | 31 ++++++-------------
     1 file changed, 9 insertions(+), 22 deletions(-)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt
    index 68669171c7..11be821d7e 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt
    @@ -32,10 +32,8 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
     import im.vector.matrix.android.api.util.Cancelable
     import im.vector.matrix.android.api.util.Optional
     import im.vector.matrix.android.api.util.toOptional
    -import im.vector.matrix.android.internal.database.helper.addSendingEvent
     import im.vector.matrix.android.internal.database.mapper.asDomain
     import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
    -import im.vector.matrix.android.internal.database.model.RoomEntity
     import im.vector.matrix.android.internal.database.query.where
     import im.vector.matrix.android.internal.di.UserId
     import im.vector.matrix.android.internal.session.room.send.EncryptEventWorker
    @@ -67,9 +65,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
     
         override fun sendReaction(targetEventId: String, reaction: String): Cancelable {
             val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction)
    -                .also {
    -                    saveLocalEcho(it)
    -                }
    +                .also { saveLocalEcho(it) }
             val sendRelationWork = createSendEventWork(event, true)
             TimelineSendEventWorkCommon.postWork(context, roomId, sendRelationWork)
             return CancelableWork(context, sendRelationWork.id)
    @@ -89,9 +85,8 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
                         // TODO?
                     }
                     data.redactEventId?.let { toRedact ->
    -                    val redactEvent = eventFactory.createRedactEvent(roomId, toRedact, null).also {
    -                        saveLocalEcho(it)
    -                    }
    +                    val redactEvent = eventFactory.createRedactEvent(roomId, toRedact, null)
    +                            .also { saveLocalEcho(it) }
                         val redactWork = createRedactEventWork(redactEvent, toRedact, null)
     
                         TimelineSendEventWorkCommon.postWork(context, roomId, redactWork)
    @@ -125,9 +120,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
                                      compatibilityBodyText: String): Cancelable {
             val event = eventFactory
                     .createReplaceTextEvent(roomId, targetEventId, newBodyText, newBodyAutoMarkdown, msgType, compatibilityBodyText)
    -                .also {
    -                    saveLocalEcho(it)
    -                }
    +                .also { saveLocalEcho(it) }
             return if (cryptoService.isRoomEncrypted(roomId)) {
                 val encryptWork = createEncryptEventWork(event, listOf("m.relates_to"))
                 val workRequest = createSendEventWork(event, false)
    @@ -149,9 +142,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
                             replyToEdit,
                             originalTimelineEvent,
                             newBodyText, true, MessageType.MSGTYPE_TEXT, compatibilityBodyText)
    -                .also {
    -                    saveLocalEcho(it)
    -                }
    +                .also { saveLocalEcho(it) }
             return if (cryptoService.isRoomEncrypted(roomId)) {
                 val encryptWork = createEncryptEventWork(event, listOf("m.relates_to"))
                 val workRequest = createSendEventWork(event, false)
    @@ -174,9 +165,9 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
         }
     
         override fun replyToMessage(eventReplied: TimelineEvent, replyText: String, autoMarkdown: Boolean): Cancelable? {
    -        val event = eventFactory.createReplyTextEvent(roomId, eventReplied, replyText, autoMarkdown)?.also {
    -            saveLocalEcho(it)
    -        } ?: return null
    +        val event = eventFactory.createReplyTextEvent(roomId, eventReplied, replyText, autoMarkdown)
    +                ?.also { saveLocalEcho(it) }
    +                ?: return null
     
             return if (cryptoService.isRoomEncrypted(roomId)) {
                 val encryptWork = createEncryptEventWork(event, listOf("m.relates_to"))
    @@ -220,10 +211,6 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
          * the same transaction id is received (in unsigned data)
          */
         private fun saveLocalEcho(event: Event) {
    -        monarchy.writeAsync { realm ->
    -            val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst()
    -                    ?: return@writeAsync
    -            roomEntity.addSendingEvent(event)
    -        }
    +        eventFactory.saveLocalEcho(monarchy, event)
         }
     }
    
    From 7388a408b842cdf5283db5c24096e1398e3bfb09 Mon Sep 17 00:00:00 2001
    From: ganfra 
    Date: Tue, 22 Oct 2019 17:13:38 +0200
    Subject: [PATCH 181/197] Permissions: allow to provide the rationale message
     as it requires "context" and cannot be generic
    
    ---
     .../riotx/core/utils/PermissionsTools.kt      | 68 ++++---------------
     .../setup/KeysBackupSetupActivity.kt          |  2 +-
     .../setup/KeysBackupSetupStep3Fragment.kt     |  8 ++-
     .../room/detail/composer/ComposerEditText.kt  |  1 -
     .../VectorSettingsSecurityPrivacyFragment.kt  |  2 +-
     vector/src/main/res/values/strings.xml        |  3 +
     6 files changed, 24 insertions(+), 60 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt b/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt
    index a7aab97f36..2c03bcb943 100644
    --- a/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt
    @@ -21,8 +21,8 @@ import android.app.Activity
     import android.content.Context
     import android.content.pm.PackageManager
     import android.os.Build
    -import android.text.TextUtils
     import android.widget.Toast
    +import androidx.annotation.StringRes
     import androidx.appcompat.app.AlertDialog
     import androidx.core.app.ActivityCompat
     import androidx.core.content.ContextCompat
    @@ -102,8 +102,9 @@ fun logPermissionStatuses(context: Context) {
      */
     fun checkPermissions(permissionsToBeGrantedBitMap: Int,
                          activity: Activity,
    -                     requestCode: Int = PERMISSION_REQUEST_CODE): Boolean {
    -    return checkPermissions(permissionsToBeGrantedBitMap, activity, null, requestCode)
    +                     requestCode: Int,
    +                     @StringRes rationaleMessage: Int = 0): Boolean {
    +    return checkPermissions(permissionsToBeGrantedBitMap, activity, null, requestCode, rationaleMessage)
     }
     
     /**
    @@ -115,8 +116,9 @@ fun checkPermissions(permissionsToBeGrantedBitMap: Int,
      */
     fun checkPermissions(permissionsToBeGrantedBitMap: Int,
                          fragment: Fragment,
    -                     requestCode: Int = PERMISSION_REQUEST_CODE): Boolean {
    -    return checkPermissions(permissionsToBeGrantedBitMap, fragment.activity, fragment, requestCode)
    +                     requestCode: Int,
    +                     @StringRes rationaleMessage: Int = 0): Boolean {
    +    return checkPermissions(permissionsToBeGrantedBitMap, fragment.activity, fragment, requestCode, rationaleMessage)
     }
     
     /**
    @@ -140,7 +142,9 @@ fun checkPermissions(permissionsToBeGrantedBitMap: Int,
     private fun checkPermissions(permissionsToBeGrantedBitMap: Int,
                                  activity: Activity?,
                                  fragment: Fragment?,
    -                             requestCode: Int): Boolean {
    +                             requestCode: Int,
    +                             @StringRes rationaleMessage: Int
    +                             ): Boolean {
         var isPermissionGranted = false
     
         // sanity check
    @@ -163,7 +167,6 @@ private fun checkPermissions(permissionsToBeGrantedBitMap: Int,
             val permissionListAlreadyDenied = ArrayList()
             val permissionsListToBeGranted = ArrayList()
             var isRequestPermissionRequired = false
    -        var explanationMessage = ""
     
             // retrieve the permissions to be granted according to the request code bit map
             if (PERMISSION_CAMERA == permissionsToBeGrantedBitMap and PERMISSION_CAMERA) {
    @@ -203,58 +206,11 @@ private fun checkPermissions(permissionsToBeGrantedBitMap: Int,
             }
     
             // if some permissions were already denied: display a dialog to the user before asking again.
    -        if (!permissionListAlreadyDenied.isEmpty()) {
    -            if (permissionsToBeGrantedBitMap == PERMISSIONS_FOR_VIDEO_IP_CALL || permissionsToBeGrantedBitMap == PERMISSIONS_FOR_AUDIO_IP_CALL) {
    -                // Permission request for VOIP call
    -                if (permissionListAlreadyDenied.contains(Manifest.permission.CAMERA)
    -                        && permissionListAlreadyDenied.contains(Manifest.permission.RECORD_AUDIO)) {
    -                    // Both missing
    -                    explanationMessage += activity.getString(R.string.permissions_rationale_msg_camera_and_audio)
    -                } else if (permissionListAlreadyDenied.contains(Manifest.permission.RECORD_AUDIO)) {
    -                    // Audio missing
    -                    explanationMessage += activity.getString(R.string.permissions_rationale_msg_record_audio)
    -                    explanationMessage += activity.getString(R.string.permissions_rationale_msg_record_audio_explanation)
    -                } else if (permissionListAlreadyDenied.contains(Manifest.permission.CAMERA)) {
    -                    // Camera missing
    -                    explanationMessage += activity.getString(R.string.permissions_rationale_msg_camera)
    -                    explanationMessage += activity.getString(R.string.permissions_rationale_msg_camera_explanation)
    -                }
    -            } else {
    -                permissionListAlreadyDenied.forEach {
    -                    when (it) {
    -                        Manifest.permission.CAMERA                 -> {
    -                            if (!TextUtils.isEmpty(explanationMessage)) {
    -                                explanationMessage += "\n\n"
    -                            }
    -                            explanationMessage += activity.getString(R.string.permissions_rationale_msg_camera)
    -                        }
    -                        Manifest.permission.RECORD_AUDIO           -> {
    -                            if (!TextUtils.isEmpty(explanationMessage)) {
    -                                explanationMessage += "\n\n"
    -                            }
    -                            explanationMessage += activity.getString(R.string.permissions_rationale_msg_record_audio)
    -                        }
    -                        Manifest.permission.WRITE_EXTERNAL_STORAGE -> {
    -                            if (!TextUtils.isEmpty(explanationMessage)) {
    -                                explanationMessage += "\n\n"
    -                            }
    -                            explanationMessage += activity.getString(R.string.permissions_rationale_msg_storage)
    -                        }
    -                        Manifest.permission.READ_CONTACTS          -> {
    -                            if (!TextUtils.isEmpty(explanationMessage)) {
    -                                explanationMessage += "\n\n"
    -                            }
    -                            explanationMessage += activity.getString(R.string.permissions_rationale_msg_contacts)
    -                        }
    -                        else                                       -> Timber.v("## checkPermissions(): already denied permission not supported")
    -                    }
    -                }
    -            }
    -
    +        if (permissionListAlreadyDenied.isNotEmpty() && rationaleMessage != 0) {
                 // display the dialog with the info text
                 AlertDialog.Builder(activity)
                         .setTitle(R.string.permissions_rationale_popup_title)
    -                    .setMessage(explanationMessage)
    +                    .setMessage(rationaleMessage)
                         .setOnCancelListener { Toast.makeText(activity, R.string.missing_permissions_warning, Toast.LENGTH_SHORT).show() }
                         .setPositiveButton(R.string.ok) { _, _ ->
                             if (!permissionsListToBeGranted.isEmpty()) {
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt
    index 291da66362..6868fb84bb 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt
    @@ -130,7 +130,7 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
         }
     
         private fun exportKeysManually() {
    -        if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS)) {
    +        if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS, R.string.permissions_rationale_msg_keys_backup_export)) {
                 ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener {
                     override fun onPassphrase(passphrase: String) {
                         showWaitingView()
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt
    index 4a7514416d..7b61ca2c0f 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt
    @@ -135,7 +135,13 @@ class KeysBackupSetupStep3Fragment : VectorBaseFragment() {
             }
     
             dialog.findViewById(R.id.keys_backup_setup_save)?.setOnClickListener {
    -            if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS)) {
    +            val permissionsChecked = checkPermissions(
    +                    PERMISSIONS_FOR_WRITING_FILES,
    +                    this,
    +                    PERMISSION_REQUEST_CODE_EXPORT_KEYS,
    +                    R.string.permissions_rationale_msg_keys_backup_export
    +            )
    +            if (permissionsChecked) {
                     exportRecoveryKeyToFile(recoveryKey)
                 }
                 dialog.dismiss()
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt
    index 3b1165b016..9c894df880 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt
    @@ -52,7 +52,6 @@ class ComposerEditText @JvmOverloads constructor(context: Context, attrs: Attrib
                             }
                         }
                         callback?.onRichContentSelected(inputContentInfo.contentUri) ?: false
    -
                     }
             return InputConnectionCompat.createWrapper(ic, editorInfo, callback)
         }
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    index dfc7004554..55805434fa 100644
    --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    @@ -212,7 +212,7 @@ class VectorSettingsSecurityPrivacyFragment : VectorSettingsBaseFragment() {
          */
         private fun exportKeys() {
             // We need WRITE_EXTERNAL permission
    -        if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS)) {
    +        if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS, R.string.permissions_rationale_msg_keys_backup_export)) {
                 activity?.let { activity ->
                     ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener {
                         override fun onPassphrase(passphrase: String) {
    diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
    index 8b83175784..ca5a37ee84 100644
    --- a/vector/src/main/res/values/strings.xml
    +++ b/vector/src/main/res/values/strings.xml
    @@ -368,6 +368,9 @@
         Riot needs permission to access your camera and your microphone to perform video calls.\n\nPlease allow access on the next pop-ups to be able to make the call.
         Riot can check your address book to find other Matrix users based on their email and phone numbers. If you agree to share your address book for this purpose, please allow access on the next pop-up.
         Riot can check your address book to find other Matrix users based on their email and phone numbers.\n\nDo you agree to share your address book for this purpose?
    +    Riot needs permission to save your E2E keys on disk.\n\nPlease allow access on the next pop-up to be able to export your keys manually.
    +
    +
     
         Sorry. Action not performed, due to missing permissions
     
    
    From 377a228f88ba16b9fb0202f4b78667420170a675 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Tue, 22 Oct 2019 17:31:07 +0200
    Subject: [PATCH 182/197] Improve code
    
    ---
     .../session/room/timeline/TimelineEventDecryptor.kt      | 9 ++++-----
     1 file changed, 4 insertions(+), 5 deletions(-)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt
    index 62ae5f621b..967dd66397 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt
    @@ -39,8 +39,8 @@ internal class TimelineEventDecryptor(
             override fun onNewSession(roomId: String?, senderKey: String, sessionId: String) {
                 synchronized(unknownSessionsFailure) {
                     unknownSessionsFailure[sessionId]
    +                        ?.toList()
                             .orEmpty()
    -                        .toList()
                             .also {
                                 unknownSessionsFailure[sessionId]?.clear()
                             }
    @@ -55,7 +55,7 @@ internal class TimelineEventDecryptor(
         // Set of eventIds which are currently decrypting
         private val existingRequests = mutableSetOf()
         // sessionId -> list of eventIds
    -    private val unknownSessionsFailure = mutableMapOf>()
    +    private val unknownSessionsFailure = mutableMapOf>()
     
         fun start() {
             executor = Executors.newSingleThreadExecutor()
    @@ -84,11 +84,10 @@ internal class TimelineEventDecryptor(
                 }
             }
             synchronized(existingRequests) {
    -            if (eventId in existingRequests) {
    +            if (!existingRequests.add(eventId)) {
                     Timber.d("Skip Decryption request for event $eventId, already requested")
                     return
                 }
    -            existingRequests.add(eventId)
             }
             executor?.execute {
                 Realm.getInstance(realmConfiguration).use { realm ->
    @@ -120,7 +119,7 @@ internal class TimelineEventDecryptor(
                     event.content?.toModel()?.let { content ->
                         content.sessionId?.let { sessionId ->
                             synchronized(unknownSessionsFailure) {
    -                            val list = unknownSessionsFailure.getOrPut(sessionId) { ArrayList() }
    +                            val list = unknownSessionsFailure.getOrPut(sessionId) { mutableSetOf() }
                                 list.add(eventId)
                             }
                         }
    
    From 6d55c15761e6f4f01e167ef988e73d994bc74154 Mon Sep 17 00:00:00 2001
    From: ganfra 
    Date: Tue, 22 Oct 2019 17:41:21 +0200
    Subject: [PATCH 183/197] Fix lint issue
    
    ---
     .../vector/riotx/features/home/room/detail/RoomDetailFragment.kt | 1 -
     1 file changed, 1 deletion(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    index 5fccf4386a..e20ec63769 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    @@ -292,7 +292,6 @@ class RoomDetailFragment :
                     null                      -> Timber.v("No share data to process")
                 }
             }
    -
         }
     
         override fun onDestroy() {
    
    From 1de02c2fbb57d3563b50712c8a6f090690d1eda3 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Tue, 22 Oct 2019 17:41:59 +0200
    Subject: [PATCH 184/197] Ensure android.text.TextUtils will never be used
     again
    
    ---
     tools/check/forbidden_strings_in_code.txt | 3 +++
     1 file changed, 3 insertions(+)
    
    diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt
    index 7e00295a48..36f26452f0 100644
    --- a/tools/check/forbidden_strings_in_code.txt
    +++ b/tools/check/forbidden_strings_in_code.txt
    @@ -156,3 +156,6 @@ Formatter\.formatFileSize===1
     
     ### Use TextUtils.formatFileSize with short format param to true
     Formatter\.formatShortFileSize===1
    +
    +### Use kotlin stdlib to test or compare strings
    +android\.text\.TextUtils
    
    From dbc17ae5158559094fd5f8a8afa159b84b345540 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Tue, 22 Oct 2019 18:23:53 +0200
    Subject: [PATCH 185/197] Use AppCompatEditText instead of EditText
    
    ---
     .../features/home/room/detail/RoomDetailFragment.kt    | 10 +++++-----
     .../home/room/detail/composer/ComposerEditText.kt      |  4 ++--
     2 files changed, 7 insertions(+), 7 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    index e20ec63769..13d9ac4a3d 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    @@ -421,7 +421,7 @@ class RoomDetailFragment :
             if (text != composerLayout.composerEditText.text.toString()) {
                 // Ignore update to avoid saving a draft
                 composerLayout.composerEditText.setText(text)
    -            composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
    +            composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length ?: 0)
             }
         }
     
    @@ -1135,21 +1135,21 @@ class RoomDetailFragment :
                 val myDisplayName = session.getUser(session.myUserId)?.displayName
                 if (myDisplayName == text) {
                     // current user
    -                if (composerLayout.composerEditText.text.isBlank()) {
    +                if (composerLayout.composerEditText.text.isNullOrBlank()) {
                         composerLayout.composerEditText.append(Command.EMOTE.command + " ")
    -                    composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
    +                    composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length ?: 0)
     //                    vibrate = true
                     }
                 } else {
                     // another user
    -                if (composerLayout.composerEditText.text.isBlank()) {
    +                if (composerLayout.composerEditText.text.isNullOrBlank()) {
                         // Ensure displayName will not be interpreted as a Slash command
                         if (text.startsWith("/")) {
                             composerLayout.composerEditText.append("\\")
                         }
                         composerLayout.composerEditText.append(sanitizeDisplayName(text) + ": ")
                     } else {
    -                    composerLayout.composerEditText.text.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayName(text) + " ")
    +                    composerLayout.composerEditText.text?.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayName(text) + " ")
                     }
     
     //                vibrate = true
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt
    index 9c894df880..273aeecbfa 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt
    @@ -23,12 +23,12 @@ import android.os.Build
     import android.util.AttributeSet
     import android.view.inputmethod.EditorInfo
     import android.view.inputmethod.InputConnection
    -import android.widget.EditText
    +import androidx.appcompat.widget.AppCompatEditText
     import androidx.core.view.inputmethod.EditorInfoCompat
     import androidx.core.view.inputmethod.InputConnectionCompat
     
     class ComposerEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = android.R.attr.editTextStyle)
    -    : EditText(context, attrs, defStyleAttr) {
    +    : AppCompatEditText(context, attrs, defStyleAttr) {
     
         interface Callback {
             fun onRichContentSelected(contentUri: Uri): Boolean
    
    From cac5fb725a6f177da99502020ea386232de926eb Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Tue, 22 Oct 2019 18:35:05 +0200
    Subject: [PATCH 186/197] Code cleanup
    
    ---
     .../features/home/room/detail/composer/TextComposerView.kt  | 6 ++----
     1 file changed, 2 insertions(+), 4 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt
    index 192a3a6977..0a6d3dde08 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt
    @@ -39,9 +39,7 @@ import im.vector.riotx.R
     class TextComposerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
                                                      defStyleAttr: Int = 0) : ConstraintLayout(context, attrs, defStyleAttr) {
     
    -    interface Callback {
    -        fun onRichContentSelected(contentUri: Uri): Boolean
    -    }
    +    interface Callback : ComposerEditText.Callback
     
         var callback: Callback? = null
     
    @@ -60,7 +58,7 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib
         @BindView(R.id.composer_avatar_view)
         lateinit var composerAvatarImageView: ImageView
     
    -    var currentConstraintSetId: Int = -1
    +    private var currentConstraintSetId: Int = -1
     
         private val animationDuration = 100L
     
    
    From 50bf6df7fe2f5e76e6e6c7909c40f78eecbd3097 Mon Sep 17 00:00:00 2001
    From: ganfra 
    Date: Wed, 23 Oct 2019 11:55:19 +0200
    Subject: [PATCH 187/197] Room summary: fix some issues with local echo and
     sending event
    
    ---
     .../internal/database/query/ReadQueries.kt    |  5 +++-
     .../query/TimelineEventEntityQueries.kt       | 25 +++++++++++--------
     .../session/room/RoomSummaryUpdater.kt        |  2 +-
     3 files changed, 19 insertions(+), 13 deletions(-)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt
    index 9462f582ff..b52f35fa92 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt
    @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.database.query
     import com.zhuinden.monarchy.Monarchy
     import im.vector.matrix.android.internal.database.model.ChunkEntity
     import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
    +import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory
     
     internal fun isEventRead(monarchy: Monarchy,
                              userId: String?,
    @@ -26,7 +27,9 @@ internal fun isEventRead(monarchy: Monarchy,
         if (userId.isNullOrBlank() || roomId.isNullOrBlank() || eventId.isNullOrBlank()) {
             return false
         }
    -
    +    if (LocalEchoEventFactory.isLocalEchoId(eventId)) {
    +        return true
    +    }
         var isEventRead = false
     
         monarchy.doWithRealm { realm ->
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt
    index d7e2f59742..49474e8e6b 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt
    @@ -61,25 +61,28 @@ internal fun TimelineEventEntity.Companion.findWithSenderMembershipEvent(realm:
     internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm,
                                                            roomId: String,
                                                            includesSending: Boolean,
    -                                                       includedTypes: List = emptyList(),
    -                                                       excludedTypes: List = emptyList()): TimelineEventEntity? {
    +                                                       filterTypes: List = emptyList()): TimelineEventEntity? {
         val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: return null
    -    val eventList = if (includesSending && roomEntity.sendingTimelineEvents.isNotEmpty()) {
    -        roomEntity.sendingTimelineEvents
    +    val sendingTimelineEvents = roomEntity.sendingTimelineEvents.where().filterTypes(filterTypes)
    +    val liveEvents = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)?.timelineEvents?.where()?.filterTypes(filterTypes)
    +    val query = if (includesSending && sendingTimelineEvents.findAll().isNotEmpty()) {
    +        sendingTimelineEvents
         } else {
    -        ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)?.timelineEvents
    -    }
    -    val query = eventList?.where()
    -    if (includedTypes.isNotEmpty()) {
    -        query?.`in`(TimelineEventEntityFields.ROOT.TYPE, includedTypes.toTypedArray())
    -    } else if (excludedTypes.isNotEmpty()) {
    -        query?.not()?.`in`(TimelineEventEntityFields.ROOT.TYPE, excludedTypes.toTypedArray())
    +        liveEvents
         }
         return query
                 ?.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING)
                 ?.findFirst()
     }
     
    +internal fun RealmQuery.filterTypes(filterTypes: List): RealmQuery {
    +    return if (filterTypes.isEmpty()) {
    +        this
    +    } else {
    +        this.`in`(TimelineEventEntityFields.ROOT.TYPE, filterTypes.toTypedArray())
    +    }
    +}
    +
     internal fun RealmQuery.next(from: Int? = null, strict: Boolean = true): TimelineEventEntity? {
         if (from != null) {
             if (strict) {
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt
    index 0fb3d40bb5..0d28720ec6 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt
    @@ -87,7 +87,7 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId
                 roomSummaryEntity.membership = membership
             }
     
    -        val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, includedTypes = PREVIEWABLE_TYPES)
    +        val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, filterTypes = PREVIEWABLE_TYPES)
             val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()?.asDomain()
     
             roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0
    
    From 3196dcb57e6638b35a80993137e93a5d0ac5e169 Mon Sep 17 00:00:00 2001
    From: ganfra 
    Date: Wed, 23 Oct 2019 12:20:03 +0200
    Subject: [PATCH 188/197] MessageActions: disable if not synced atm
    
    ---
     .../main/java/im/vector/riotx/core/extensions/TimelineEvent.kt | 3 ++-
     .../room/detail/timeline/action/MessageActionsViewModel.kt     | 2 +-
     2 files changed, 3 insertions(+), 2 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/TimelineEvent.kt b/vector/src/main/java/im/vector/riotx/core/extensions/TimelineEvent.kt
    index 6c7a6be1fd..387105c480 100644
    --- a/vector/src/main/java/im/vector/riotx/core/extensions/TimelineEvent.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/extensions/TimelineEvent.kt
    @@ -17,11 +17,12 @@
     package im.vector.riotx.core.extensions
     
     import im.vector.matrix.android.api.session.events.model.EventType
    +import im.vector.matrix.android.api.session.room.send.SendState
     import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
     
     fun TimelineEvent.canReact(): Boolean {
         // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
    -    return root.getClearType() == EventType.MESSAGE && root.sendState.isSent() && !root.isRedacted()
    +    return root.getClearType() == EventType.MESSAGE && root.sendState == SendState.SYNCED && !root.isRedacted()
     }
     
     fun TimelineEvent.displayReadMarker(myUserId: String): Boolean {
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
    index 3b25a9e908..135496264d 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
    @@ -201,7 +201,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
                     if (canCancel(event)) {
                         add(SimpleAction.Cancel(eventId))
                     }
    -            } else {
    +            } else if (event.root.sendState == SendState.SYNCED) {
                     if (!event.root.isRedacted()) {
                         if (canReply(event, messageContent)) {
                             add(SimpleAction.Reply(eventId))
    
    From ff81715783da6d2914878c8c38d0a68510c4f781 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Wed, 23 Oct 2019 14:18:40 +0200
    Subject: [PATCH 189/197] Import Strings from Riot
    
    ---
     .../src/main/res/values-de/strings.xml        |  10 +
     .../src/main/res/values-eu/strings.xml        |   1 +
     .../src/main/res/values-fi/strings.xml        |   5 +-
     .../src/main/res/values-fr/strings.xml        |   1 +
     .../src/main/res/values-hu/strings.xml        |   1 +
     .../src/main/res/values-it/strings.xml        |   1 +
     .../src/main/res/values-ko/strings.xml        |  11 +-
     .../src/main/res/values-nl/strings.xml        |   1 +
     .../src/main/res/values-ru/strings.xml        |   1 +
     .../src/main/res/values-sq/strings.xml        |   1 +
     .../src/main/res/values-vls/strings.xml       |   3 +-
     .../src/main/res/values-zh-rTW/strings.xml    |   1 +
     .../src/main/res/values/strings.xml           |   1 +
     tools/import_from_riot.sh                     |  89 ++--
     vector/src/main/res/values-ar/strings.xml     |  10 +-
     .../src/main/res/values-b+sr+Latn/strings.xml | 405 +++++++++++++++++
     vector/src/main/res/values-bg/strings.xml     |  29 +-
     vector/src/main/res/values-bn-rIN/strings.xml |   8 -
     vector/src/main/res/values-bs/strings.xml     |   8 -
     vector/src/main/res/values-ca/strings.xml     |   6 +-
     vector/src/main/res/values-cs/strings.xml     |   8 -
     vector/src/main/res/values-da/strings.xml     |   8 -
     vector/src/main/res/values-de/strings.xml     |  16 +-
     vector/src/main/res/values-el/strings.xml     |   2 +-
     vector/src/main/res/values-es-rMX/strings.xml |   4 -
     vector/src/main/res/values-es/strings.xml     |  10 +-
     vector/src/main/res/values-eu/strings.xml     | 115 ++++-
     vector/src/main/res/values-fa/strings.xml     |   8 -
     vector/src/main/res/values-fi/strings.xml     |  75 ++-
     vector/src/main/res/values-fr/strings.xml     | 110 ++++-
     vector/src/main/res/values-gl/strings.xml     |   8 -
     vector/src/main/res/values-hu/strings.xml     | 108 ++++-
     vector/src/main/res/values-id/strings.xml     |   8 -
     vector/src/main/res/values-in/strings.xml     |   8 -
     vector/src/main/res/values-is/strings.xml     |   8 -
     vector/src/main/res/values-it/strings.xml     | 112 ++++-
     vector/src/main/res/values-ja/strings.xml     |   8 -
     vector/src/main/res/values-ko/strings.xml     | 426 +++++++++++-------
     vector/src/main/res/values-lv/strings.xml     |   8 -
     vector/src/main/res/values-nl/strings.xml     |  17 +-
     vector/src/main/res/values-nn/strings.xml     |   4 -
     vector/src/main/res/values-pl/strings.xml     |   8 -
     vector/src/main/res/values-pt-rBR/strings.xml |   4 -
     vector/src/main/res/values-pt/strings.xml     |   8 -
     vector/src/main/res/values-ru/strings.xml     | 133 ++++--
     vector/src/main/res/values-sk/strings.xml     |   8 -
     vector/src/main/res/values-sq/strings.xml     | 110 ++++-
     vector/src/main/res/values-te/strings.xml     |   8 -
     vector/src/main/res/values-tr/strings.xml     |   8 -
     vector/src/main/res/values-uk/strings.xml     |   4 -
     vector/src/main/res/values-zh-rCN/strings.xml |   8 -
     vector/src/main/res/values-zh-rTW/strings.xml | 110 ++++-
     vector/src/main/res/values/strings.xml        | 151 ++++++-
     vector/src/main/res/values/strings_riotX.xml  |  59 ---
     54 files changed, 1628 insertions(+), 655 deletions(-)
     create mode 100644 vector/src/main/res/values-b+sr+Latn/strings.xml
    
    diff --git a/matrix-sdk-android/src/main/res/values-de/strings.xml b/matrix-sdk-android/src/main/res/values-de/strings.xml
    index 3f37ec35f6..59b5ee4212 100644
    --- a/matrix-sdk-android/src/main/res/values-de/strings.xml
    +++ b/matrix-sdk-android/src/main/res/values-de/strings.xml
    @@ -166,4 +166,14 @@
         Sende eine Nachricht…
         Sendewarteschlange leeren
     
    +    Erste Synchronisation: Importiere Benutzerkonto…
    +    Erste Synchronisation: Importiere Cryptoschlüssel
    +    Erste Synchronisation: Importiere Räume
    +    Erste Synchronisation: Importiere betretene Räume
    +    Erste Synchronisation: Importiere eingeladene Räume
    +    Erste Synchronisation: Importiere verlassene Räume
    +    Erste Synchronisation: Importiere Gemeinschaften
    +    Erste Synchronisation: Importiere Benutzerdaten
    +
    +    %1$s hat die Einladung an %2$s, den Raum zu betreten, zurückgezogen
     
    diff --git a/matrix-sdk-android/src/main/res/values-eu/strings.xml b/matrix-sdk-android/src/main/res/values-eu/strings.xml
    index 25e0df10f4..5b36858253 100644
    --- a/matrix-sdk-android/src/main/res/values-eu/strings.xml
    +++ b/matrix-sdk-android/src/main/res/values-eu/strings.xml
    @@ -172,4 +172,5 @@
         Mezua bidaltzen…
         Garbitu bidalketa-ilara
     
    +    %1$s erabiltzaileak %2$s gelara elkartzeko gonbidapena indargabetu du
     
    diff --git a/matrix-sdk-android/src/main/res/values-fi/strings.xml b/matrix-sdk-android/src/main/res/values-fi/strings.xml
    index 1cfc499380..3c02e5c2ce 100644
    --- a/matrix-sdk-android/src/main/res/values-fi/strings.xml
    +++ b/matrix-sdk-android/src/main/res/values-fi/strings.xml
    @@ -22,7 +22,7 @@
         %s soitti äänipuhelun.
         %s vastasi puheluun.
         %s lopetti puhelun.
    -    %1$s muutti tulevan huonehistorian näkyväksi käyttäjälle %2$s
    +    %1$s muutti tulevan huonehistorian näkyväksi seuraaville: %2$s
         kaikki huoneen jäsenet, kutsumisestaan asti.
         kaikki huoneen jäsenet, liittymisestään asti.
         kaikki huoneen jäsenet.
    @@ -34,7 +34,7 @@
         VoIP-konferenssi alkoi
         VoIP-konferenssi päättyi
     
    -    (myös profiilikuva vaihdettiin)
    +    (myös kuva vaihdettiin)
         %1$s poisti huoneen nimen
         %1$s poisti huoneen aiheen
         %1$s päivitti profiilinsa %2$s
    @@ -173,4 +173,5 @@
         Lähetetään viestiä…
         Tyhjennä lähetysjono
     
    +    %1$s veti takaisin käyttäjän %2$s liittymiskutsun huoneeseen
     
    diff --git a/matrix-sdk-android/src/main/res/values-fr/strings.xml b/matrix-sdk-android/src/main/res/values-fr/strings.xml
    index 9541555d75..98a98a3e7a 100644
    --- a/matrix-sdk-android/src/main/res/values-fr/strings.xml
    +++ b/matrix-sdk-android/src/main/res/values-fr/strings.xml
    @@ -172,4 +172,5 @@
         Envoi du message…
         Vider la file d’envoi
     
    +    %1$s a révoqué l’invitation pour %2$s à rejoindre le salon
     
    diff --git a/matrix-sdk-android/src/main/res/values-hu/strings.xml b/matrix-sdk-android/src/main/res/values-hu/strings.xml
    index e3d4e88c60..da6b8f5687 100644
    --- a/matrix-sdk-android/src/main/res/values-hu/strings.xml
    +++ b/matrix-sdk-android/src/main/res/values-hu/strings.xml
    @@ -171,4 +171,5 @@
         Üzenet küldése…
         Küldő sor ürítése
     
    +    %1$s visszavonta a meghívót a belépéshez ebbe a szobába: %2$s
     
    diff --git a/matrix-sdk-android/src/main/res/values-it/strings.xml b/matrix-sdk-android/src/main/res/values-it/strings.xml
    index 1edc038175..a8d844ddde 100644
    --- a/matrix-sdk-android/src/main/res/values-it/strings.xml
    +++ b/matrix-sdk-android/src/main/res/values-it/strings.xml
    @@ -172,4 +172,5 @@
         Invio messaggio in corso …
         Cancella la coda di invio
     
    +    %1$s ha revocato l\'invito a %2$s di unirsi alla stanza
     
    diff --git a/matrix-sdk-android/src/main/res/values-ko/strings.xml b/matrix-sdk-android/src/main/res/values-ko/strings.xml
    index 30145e31a3..959ff8a96e 100644
    --- a/matrix-sdk-android/src/main/res/values-ko/strings.xml
    +++ b/matrix-sdk-android/src/main/res/values-ko/strings.xml
    @@ -26,12 +26,12 @@
         %s님이 전화를 받았습니다.
         %s님이 전화를 끊었습니다.
         %1$s님이 이후 %2$s에게 방 기록을 공개했습니다
    -    초대된 시점부터 모든 방 구성원.
    -    들어온 시점부터 모든 방 구성원.
    -    모든 방 구성원.
    +    초대된 시점부터 모든 방 구성원
    +    들어온 시점부터 모든 방 구성원
    +    모든 방 구성원
         누구나.
         알 수 없음 (%s).
    -    %1$s님이 종단 간 암호화를 켰습니다 (%2$s)
    +    %1$s님이 종단간 암호화를 켰습니다 (%2$s)
         %s님이 방을 업그레이드했습니다.
     
         %1$s님이 VoIP 회의를 요청했습니다
    @@ -52,7 +52,7 @@
         ** 암호를 해독할 수 없음: %s **
         발신인의 기기에서 이 메시지의 키를 보내지 않았습니다.
     
    -    이 답장의 질문
    +    관련 대화
     
         검열할 수 없습니다
         메시지를 보낼 수 없습니다
    @@ -170,4 +170,5 @@
         메시지 보내는 중…
         전송 대기 열 지우기
     
    +    %1$s님이 %2$s님에게 방에 참가하라고 보낸 초대를 취소했습니다
     
    diff --git a/matrix-sdk-android/src/main/res/values-nl/strings.xml b/matrix-sdk-android/src/main/res/values-nl/strings.xml
    index c2d524e486..6658f21583 100644
    --- a/matrix-sdk-android/src/main/res/values-nl/strings.xml
    +++ b/matrix-sdk-android/src/main/res/values-nl/strings.xml
    @@ -181,4 +181,5 @@
         Bericht wordt verstuurd…
         Uitgaande wachtrij legen
     
    +    %1$s heeft de uitnodiging voor %2$s om het gesprek toe te treden ingetrokken
     
    diff --git a/matrix-sdk-android/src/main/res/values-ru/strings.xml b/matrix-sdk-android/src/main/res/values-ru/strings.xml
    index e0637ea671..07411c097f 100644
    --- a/matrix-sdk-android/src/main/res/values-ru/strings.xml
    +++ b/matrix-sdk-android/src/main/res/values-ru/strings.xml
    @@ -185,4 +185,5 @@
         Отправка сообщения…
         Очистить очередь отправки
     
    +    %1$s отозвал приглашение %2$s присоединиться к комнате
     
    diff --git a/matrix-sdk-android/src/main/res/values-sq/strings.xml b/matrix-sdk-android/src/main/res/values-sq/strings.xml
    index 03d5df2b81..cffd55a7f7 100644
    --- a/matrix-sdk-android/src/main/res/values-sq/strings.xml
    +++ b/matrix-sdk-android/src/main/res/values-sq/strings.xml
    @@ -168,4 +168,5 @@
         Po dërgohet mesazh…
         Spastro radhë pritjeje
     
    +    %1$s shfuqizoi ftesën për %2$s për pjesëmarrje te dhoma
     
    diff --git a/matrix-sdk-android/src/main/res/values-vls/strings.xml b/matrix-sdk-android/src/main/res/values-vls/strings.xml
    index eb533e15a0..dad88788e4 100644
    --- a/matrix-sdk-android/src/main/res/values-vls/strings.xml
    +++ b/matrix-sdk-android/src/main/res/values-vls/strings.xml
    @@ -44,7 +44,7 @@
         Bericht verwyderd [reden: %1$s]
         Bericht verwyderd deur %1$s [reden: %2$s]
         %1$s èt zyn/heur profiel %2$s bygewerkt
    -    %1$s èt een uutnodigienge noa %2$s gesteurd vo ’t gesprek toe te treden
    +    %1$s èt een uutnodigienge noa %2$s gesteurd vo ’t gesprek toe te treedn
         %1$s èt d’uutnodigienge vo %2$s anveird
     
         ** Kun nie ountsleuteln: %s **
    @@ -172,4 +172,5 @@
         Bericht wor verstuurd…
         Uutgoande wachtreeke leegn
     
    +    %1$s èt d’uutnodigienge vo %2$s vo ’t gesprek toe te treedn ingetrokkn
     
    diff --git a/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml b/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml
    index e0e66a80a0..5b5ae3beb0 100644
    --- a/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml
    +++ b/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml
    @@ -170,4 +170,5 @@
         正在傳送訊息……
         清除傳送佇列
     
    +    %1$s 撤銷了 %2$s 加入聊天室的邀請
     
    diff --git a/matrix-sdk-android/src/main/res/values/strings.xml b/matrix-sdk-android/src/main/res/values/strings.xml
    index 40d359076c..604bb13a41 100644
    --- a/matrix-sdk-android/src/main/res/values/strings.xml
    +++ b/matrix-sdk-android/src/main/res/values/strings.xml
    @@ -46,6 +46,7 @@
         Message removed by %1$s [reason: %2$s]
         %1$s updated their profile %2$s
         %1$s sent an invitation to %2$s to join the room
    +    %1$s revoked the invitation for %2$s to join the room
         %1$s accepted the invitation for %2$s
     
         ** Unable to decrypt: %s **
    diff --git a/tools/import_from_riot.sh b/tools/import_from_riot.sh
    index 4bc19ca86c..2e4b332a3c 100755
    --- a/tools/import_from_riot.sh
    +++ b/tools/import_from_riot.sh
    @@ -65,50 +65,51 @@ cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-zh-rTW/strings.xml ./mat
     echo
     echo "Copy strings to RiotX"
     
    -cp ../riot-android/vector/src/main/res/values/strings.xml        ./vector/src/main/res/values/strings.xml
    -cp ../riot-android/vector/src/main/res/values-ar/strings.xml     ./vector/src/main/res/values-ar/strings.xml
    -cp ../riot-android/vector/src/main/res/values-bg/strings.xml     ./vector/src/main/res/values-bg/strings.xml
    -cp ../riot-android/vector/src/main/res/values-bn-rIN/strings.xml ./vector/src/main/res/values-bn-rIN/strings.xml
    -cp ../riot-android/vector/src/main/res/values-bs/strings.xml     ./vector/src/main/res/values-bs/strings.xml
    -cp ../riot-android/vector/src/main/res/values-ca/strings.xml     ./vector/src/main/res/values-ca/strings.xml
    -cp ../riot-android/vector/src/main/res/values-cs/strings.xml     ./vector/src/main/res/values-cs/strings.xml
    -cp ../riot-android/vector/src/main/res/values-da/strings.xml     ./vector/src/main/res/values-da/strings.xml
    -cp ../riot-android/vector/src/main/res/values-de/strings.xml     ./vector/src/main/res/values-de/strings.xml
    -cp ../riot-android/vector/src/main/res/values-el/strings.xml     ./vector/src/main/res/values-el/strings.xml
    -cp ../riot-android/vector/src/main/res/values-eo/strings.xml     ./vector/src/main/res/values-eo/strings.xml
    -cp ../riot-android/vector/src/main/res/values-es/strings.xml     ./vector/src/main/res/values-es/strings.xml
    -cp ../riot-android/vector/src/main/res/values-es-rMX/strings.xml ./vector/src/main/res/values-es-rMX/strings.xml
    -cp ../riot-android/vector/src/main/res/values-eu/strings.xml     ./vector/src/main/res/values-eu/strings.xml
    -cp ../riot-android/vector/src/main/res/values-fa/strings.xml     ./vector/src/main/res/values-fa/strings.xml
    -cp ../riot-android/vector/src/main/res/values-fi/strings.xml     ./vector/src/main/res/values-fi/strings.xml
    -cp ../riot-android/vector/src/main/res/values-fr/strings.xml     ./vector/src/main/res/values-fr/strings.xml
    -cp ../riot-android/vector/src/main/res/values-fr-rCA/strings.xml ./vector/src/main/res/values-fr-rCA/strings.xml
    -cp ../riot-android/vector/src/main/res/values-gl/strings.xml     ./vector/src/main/res/values-gl/strings.xml
    -cp ../riot-android/vector/src/main/res/values-hu/strings.xml     ./vector/src/main/res/values-hu/strings.xml
    -cp ../riot-android/vector/src/main/res/values-id/strings.xml     ./vector/src/main/res/values-id/strings.xml
    -cp ../riot-android/vector/src/main/res/values-in/strings.xml     ./vector/src/main/res/values-in/strings.xml
    -cp ../riot-android/vector/src/main/res/values-is/strings.xml     ./vector/src/main/res/values-is/strings.xml
    -cp ../riot-android/vector/src/main/res/values-it/strings.xml     ./vector/src/main/res/values-it/strings.xml
    -cp ../riot-android/vector/src/main/res/values-ja/strings.xml     ./vector/src/main/res/values-ja/strings.xml
    -cp ../riot-android/vector/src/main/res/values-ko/strings.xml     ./vector/src/main/res/values-ko/strings.xml
    -cp ../riot-android/vector/src/main/res/values-lv/strings.xml     ./vector/src/main/res/values-lv/strings.xml
    -cp ../riot-android/vector/src/main/res/values-nl/strings.xml     ./vector/src/main/res/values-nl/strings.xml
    -cp ../riot-android/vector/src/main/res/values-nn/strings.xml     ./vector/src/main/res/values-nn/strings.xml
    -cp ../riot-android/vector/src/main/res/values-pl/strings.xml     ./vector/src/main/res/values-pl/strings.xml
    -cp ../riot-android/vector/src/main/res/values-pt/strings.xml     ./vector/src/main/res/values-pt/strings.xml
    -cp ../riot-android/vector/src/main/res/values-pt-rBR/strings.xml ./vector/src/main/res/values-pt-rBR/strings.xml
    -cp ../riot-android/vector/src/main/res/values-ro/strings.xml     ./vector/src/main/res/values-ro/strings.xml
    -cp ../riot-android/vector/src/main/res/values-ru/strings.xml     ./vector/src/main/res/values-ru/strings.xml
    -cp ../riot-android/vector/src/main/res/values-sk/strings.xml     ./vector/src/main/res/values-sk/strings.xml
    -cp ../riot-android/vector/src/main/res/values-sq/strings.xml     ./vector/src/main/res/values-sq/strings.xml
    -cp ../riot-android/vector/src/main/res/values-te/strings.xml     ./vector/src/main/res/values-te/strings.xml
    -cp ../riot-android/vector/src/main/res/values-th/strings.xml     ./vector/src/main/res/values-th/strings.xml
    -cp ../riot-android/vector/src/main/res/values-tlh/strings.xml    ./vector/src/main/res/values-tlh/strings.xml
    -cp ../riot-android/vector/src/main/res/values-tr/strings.xml     ./vector/src/main/res/values-tr/strings.xml
    -cp ../riot-android/vector/src/main/res/values-uk/strings.xml     ./vector/src/main/res/values-uk/strings.xml
    -cp ../riot-android/vector/src/main/res/values-vls/strings.xml    ./vector/src/main/res/values-vls/strings.xml
    -cp ../riot-android/vector/src/main/res/values-zh-rCN/strings.xml ./vector/src/main/res/values-zh-rCN/strings.xml
    -cp ../riot-android/vector/src/main/res/values-zh-rTW/strings.xml ./vector/src/main/res/values-zh-rTW/strings.xml
    +cp ../riot-android/vector/src/main/res/values/strings.xml           ./vector/src/main/res/values/strings.xml
    +cp ../riot-android/vector/src/main/res/values-ar/strings.xml        ./vector/src/main/res/values-ar/strings.xml
    +cp ../riot-android/vector/src/main/res/values-b+sr+Latn/strings.xml ./vector/src/main/res/values-b+sr+Latn/strings.xml
    +cp ../riot-android/vector/src/main/res/values-bg/strings.xml        ./vector/src/main/res/values-bg/strings.xml
    +cp ../riot-android/vector/src/main/res/values-bn-rIN/strings.xml    ./vector/src/main/res/values-bn-rIN/strings.xml
    +cp ../riot-android/vector/src/main/res/values-bs/strings.xml        ./vector/src/main/res/values-bs/strings.xml
    +cp ../riot-android/vector/src/main/res/values-ca/strings.xml        ./vector/src/main/res/values-ca/strings.xml
    +cp ../riot-android/vector/src/main/res/values-cs/strings.xml        ./vector/src/main/res/values-cs/strings.xml
    +cp ../riot-android/vector/src/main/res/values-da/strings.xml        ./vector/src/main/res/values-da/strings.xml
    +cp ../riot-android/vector/src/main/res/values-de/strings.xml        ./vector/src/main/res/values-de/strings.xml
    +cp ../riot-android/vector/src/main/res/values-el/strings.xml        ./vector/src/main/res/values-el/strings.xml
    +cp ../riot-android/vector/src/main/res/values-eo/strings.xml        ./vector/src/main/res/values-eo/strings.xml
    +cp ../riot-android/vector/src/main/res/values-es/strings.xml        ./vector/src/main/res/values-es/strings.xml
    +cp ../riot-android/vector/src/main/res/values-es-rMX/strings.xml    ./vector/src/main/res/values-es-rMX/strings.xml
    +cp ../riot-android/vector/src/main/res/values-eu/strings.xml        ./vector/src/main/res/values-eu/strings.xml
    +cp ../riot-android/vector/src/main/res/values-fa/strings.xml        ./vector/src/main/res/values-fa/strings.xml
    +cp ../riot-android/vector/src/main/res/values-fi/strings.xml        ./vector/src/main/res/values-fi/strings.xml
    +cp ../riot-android/vector/src/main/res/values-fr/strings.xml        ./vector/src/main/res/values-fr/strings.xml
    +cp ../riot-android/vector/src/main/res/values-fr-rCA/strings.xml    ./vector/src/main/res/values-fr-rCA/strings.xml
    +cp ../riot-android/vector/src/main/res/values-gl/strings.xml        ./vector/src/main/res/values-gl/strings.xml
    +cp ../riot-android/vector/src/main/res/values-hu/strings.xml        ./vector/src/main/res/values-hu/strings.xml
    +cp ../riot-android/vector/src/main/res/values-id/strings.xml        ./vector/src/main/res/values-id/strings.xml
    +cp ../riot-android/vector/src/main/res/values-in/strings.xml        ./vector/src/main/res/values-in/strings.xml
    +cp ../riot-android/vector/src/main/res/values-is/strings.xml        ./vector/src/main/res/values-is/strings.xml
    +cp ../riot-android/vector/src/main/res/values-it/strings.xml        ./vector/src/main/res/values-it/strings.xml
    +cp ../riot-android/vector/src/main/res/values-ja/strings.xml        ./vector/src/main/res/values-ja/strings.xml
    +cp ../riot-android/vector/src/main/res/values-ko/strings.xml        ./vector/src/main/res/values-ko/strings.xml
    +cp ../riot-android/vector/src/main/res/values-lv/strings.xml        ./vector/src/main/res/values-lv/strings.xml
    +cp ../riot-android/vector/src/main/res/values-nl/strings.xml        ./vector/src/main/res/values-nl/strings.xml
    +cp ../riot-android/vector/src/main/res/values-nn/strings.xml        ./vector/src/main/res/values-nn/strings.xml
    +cp ../riot-android/vector/src/main/res/values-pl/strings.xml        ./vector/src/main/res/values-pl/strings.xml
    +cp ../riot-android/vector/src/main/res/values-pt/strings.xml        ./vector/src/main/res/values-pt/strings.xml
    +cp ../riot-android/vector/src/main/res/values-pt-rBR/strings.xml    ./vector/src/main/res/values-pt-rBR/strings.xml
    +cp ../riot-android/vector/src/main/res/values-ro/strings.xml        ./vector/src/main/res/values-ro/strings.xml
    +cp ../riot-android/vector/src/main/res/values-ru/strings.xml        ./vector/src/main/res/values-ru/strings.xml
    +cp ../riot-android/vector/src/main/res/values-sk/strings.xml        ./vector/src/main/res/values-sk/strings.xml
    +cp ../riot-android/vector/src/main/res/values-sq/strings.xml        ./vector/src/main/res/values-sq/strings.xml
    +cp ../riot-android/vector/src/main/res/values-te/strings.xml        ./vector/src/main/res/values-te/strings.xml
    +cp ../riot-android/vector/src/main/res/values-th/strings.xml        ./vector/src/main/res/values-th/strings.xml
    +cp ../riot-android/vector/src/main/res/values-tlh/strings.xml       ./vector/src/main/res/values-tlh/strings.xml
    +cp ../riot-android/vector/src/main/res/values-tr/strings.xml        ./vector/src/main/res/values-tr/strings.xml
    +cp ../riot-android/vector/src/main/res/values-uk/strings.xml        ./vector/src/main/res/values-uk/strings.xml
    +cp ../riot-android/vector/src/main/res/values-vls/strings.xml       ./vector/src/main/res/values-vls/strings.xml
    +cp ../riot-android/vector/src/main/res/values-zh-rCN/strings.xml    ./vector/src/main/res/values-zh-rCN/strings.xml
    +cp ../riot-android/vector/src/main/res/values-zh-rTW/strings.xml    ./vector/src/main/res/values-zh-rTW/strings.xml
     
     echo
     echo "Success!"
    diff --git a/vector/src/main/res/values-ar/strings.xml b/vector/src/main/res/values-ar/strings.xml
    index a0013f3481..35e8f246aa 100644
    --- a/vector/src/main/res/values-ar/strings.xml
    +++ b/vector/src/main/res/values-ar/strings.xml
    @@ -1,6 +1,7 @@
     
     
         ar
    +    ""
     
         السمة الفاتحة
         السمة الداكنة
    @@ -111,8 +112,6 @@
         كلمة السر
         كلمة السر الجديدة
         اسم المستخدم
    -    أضِف عنوان بريد إلكتروني إلى حسابك ليستطيع الغير اكتشافك على الشبكة، وأيضا لإتاحة خاصية تصفير كلمة السر.
    -    أضِف رقم هاتف إلى حسابك ليستطيع الغير اكتشافك على الشبكة.
         عنوان البريد الإلكتروني
         عنوان البريد الإلكتروني (اختياري)
         رقم الهاتف
    @@ -373,7 +372,6 @@
     
         
         تحذير!
    -    العربية
     
         لم تُرسل الرسائل بسبب وجود أجهزة مجهولة. ماذا أفعل؟ %1$s. ‏%2$s.
         ليس ’⁨%s⁩‘ تنسيقا صالحا لاختصار
    @@ -533,12 +531,6 @@
         أمتأكد من بدء محادثة صوتية؟
         أمتأكد من بدء محادثة صورية؟
     
    -    أضِف عنوان بريد إلكتروني و/أو رقم هاتف لحسابك ليستطيع الناس اكتشافك.
    -
    -ستقدر أيضا على تصفير كلمة السر عبر البريد الإلكتروني.
    -    أضِف عنوان بريد إلكتروني ورقم هاتف لحسابك ليستطيع الناس اكتشافك.
    -
    -ستقدر أيضا على تصفير كلمة السر عبر البريد الإلكتروني.
         عنوان البريد مُعرّف بالفعل.
         الأمارة غير صالحة
         التسجيل عبر البريد ورقم الهاتف معا ليس مدعوما إلى حين توفر واجهة API لذلك. سيُستخدم رقم الهاتف فقط دون البريد.
    diff --git a/vector/src/main/res/values-b+sr+Latn/strings.xml b/vector/src/main/res/values-b+sr+Latn/strings.xml
    new file mode 100644
    index 0000000000..8bce8a32ff
    --- /dev/null
    +++ b/vector/src/main/res/values-b+sr+Latn/strings.xml
    @@ -0,0 +1,405 @@
    +
    +
    +    sr
    +    RS
    +    Latn
    +
    +    Svetla Tema
    +    Tamna Tema
    +    Crna Tema
    +    Status.im Tema
    +
    +    Inicijalizacija servisa
    +    Sinhronizacija u toku…
    +    Bučna obaveštenja
    +    Tiha obaveštenja
    +
    +    Poruke
    +    Soba
    +    Podešavanja
    +    Podaci o članu
    +    Istorijski
    +    Prijava bug-a
    +    Pošalji nalepnicu
    +    Rezervna kopija ključeva
    +    Koristi rezervnu kopiju ključeva
    +    Verifikuj uređaj
    +
    +    Kreiranje rezervne kopije ključeva se nije završilo, molim sačekajte…
    +    Izgubićete vaše šifrovane poruke ukoliko se sada odjavite
    +    Kreiranje rezervne kopije ključeva je u toku. Ako se odjavite sada, izgubićete pristup vašim šifrovanim porukama.
    +    Sigurnosna kopija ključeva bi trebalo da bude aktivna na svim vašim uređajima kako bi izbegli gubitak pristupa vašim šifrovanim porukama.
    +    Ne želim moje šifrovane poruke
    +    Pravljenje rezervne kopije ključeva u toku…
    +    Koristi rezervnu kopiju ključeva
    +    Da li ste sigurni\?
    +    Izgubićete pristup vašim šifrovanim porukama ukoliko ne napravite rezervu kopiju ključeva pre nego što se odjavite.
    +
    +    Učitavanje…
    +
    +    U redu
    +    Otkaži
    +    Sačuvaj
    +    Napusti
    +    Ostani
    +    Pošalji
    +    Kopiraj
    +    Pošalji ponovo
    +    Ukloni
    +    Podeli
    +    Kasnije
    +    Prosledi
    +    Obriši
    +    Preimenuj
    +    Konferencijski poziv u toku.
    +\nPriključi se kao %1$s ili %2$s
    +    Glas
    +    Video
    +    Nemoguća uspostava poziva, molim probajte kasnije
    +    Neke opcije mogu nedostajati usled manjka dozvola…
    +    Ova akcija nije moguća zbog nedosatka dozvola.
    +    Treba vam dozvola da pozovete kako bi započeli konferencijski poziv u ovoj sobi
    +    Nemoguća uspostava poziva
    +    Informacije o uređaju
    +    Konferencijski pozivi nisu podržani u šifrovanim sobama
    +    Ipak zovi
    +    Ipak pošalji
    +    ili
    +    Pošalji pozivnicu
    +    Prihvati
    +    Preskoči
    +    Gotovo
    +    Obustavi
    +    Ignoriši
    +    Pregledaj
    +    Odbaci
    +
    +    Izađi
    +    Akcije
    +    Odjavi se
    +    Da li ste sigurni da želite da se odjavite\?
    +    Glasovni poziv
    +    Video poziv
    +    Globalna pretraga
    +    Označi sve kao pročitano
    +    Brzi odgovor
    +    Označi kao pročitano
    +    Otvori
    +    Zatvori
    +    Onemogući
    +
    +    Potvrda
    +    Upozorenje
    +    Greška
    +
    +    Ljudi
    +    Sobe
    +    Pozivnice
    +    Nizak prioritet
    +    Lokalni adresar
    +    Lista korisnika
    +    Samo Matrix kontakti
    +    Sobe
    +    Lista soba
    +    Nema soba
    +    Nema javnih soba
    +    Nema grupa
    +
    +    Ako je moguće, molim opišite na engleskom.
    +    Opišite vaš problem ovde
    +    Korisničko ime
    +    Napravi nalog
    +    Prijavi se
    +    Odjavi se
    +    Započni novo ćaskanje
    +    Započni glasovni poziv
    +    Započni video poziv
    +
    +    Ne pitaj ponovo
    +
    +    Pošalji nalepnicu
    +    Napravi fotografiju ili video snimak
    +    Napravi fotografiju
    +    Napravi video snimak
    +
    +    Prijavi se
    +    Napravi nalog
    +    Preskoči
    +    Lozinka
    +    Nova lozinka
    +    Korisničko ime
    +    Broj telefona
    +    Broj telefona (opciono)
    +    Ponovi lozinku
    +    Potvrdite vašu novu lozinku
    +    Pogrešno korisničko ime i/ili lozinka
    +    Obriši
    +    Vidi izvor
    +    Prijavi sadržaj
    +    Napredak (%s%%)
    +
    +    Pošalji u
    +    Priključi se sobi
    +    Pretraži
    +
    +    Pošalji datoteke
    +    Adresa elektronske pošte
    +    Adresa elektronske pošte (opciono)
    +    Lozinka je prekratka (minimum 6)
    +    Lozinka nedostaje
    +    Ovo ne izgleda kao validna adresa elektronske pošte
    +    Ovo ne izgleda kao validan broj telefona
    +    Ova adresa elektronske pošte je već definisana.
    +    Adresa elektronske pošte nedostaje
    +    Broj telefona nedostaje
    +    Nedostaje adresa elektronske pošte ili broj telefona
    +    Lozinke nisu iste
    +    Zaboravili ste lozinku\?
    +    Koristi prilagođene opcije servera (napredno)
    +    Molim proverite vašu elektronsku poštu kako bi nastavili registraciju
    +    Korisničko ime je već u upotrebi
    +    Verifikovao sam adresu elektronske pošte
    +    Kako bi resetovali vašu lozinku, unesite adresu elektronske pošte povezanu sa vašim nalogom:
    +    Morate uneti adresu elektronske pošte povezane sa vašim nalogom.
    +    Morate uneti novu lozinku.
    +    Mobilni
    +
    +    Poslato je previše zahteva
    +    Ovo korisničko ime je već korišćeno
    +    Link u elektronskoj pošti još uvek nije kliknut
    +
    +    Zahtev za ključ poslat.
    +
    +    Zahtev poslat
    +    Pošalji kao
    +    Original
    +    Veliko
    +    Srednje
    +    Malo
    +
    +    Juče
    +    Danas
    +
    +    Ime sobe
    +    Tema sobe
    +
    +    Pozivi
    +    Koristi podrazumevani Riot zvuk zvona za dolazeće pozive
    +    Zvuk zvona dolazećeg poziva
    +    Izaberite zvuk zvona za pozive:
    +
    +    Poziv
    +    Poziv uspostavljen
    +    Poziv se uspostavlja…
    +    Poziv završen
    +    Pozivam…
    +    Dolazeći poziv
    +    Dolazeći video poziv
    +    Dolazeći glasovni poziv
    +    Poziv u toku…
    +    Video poziv u toku..
    +
    +    Izbaci iz ove sobe
    +    Izbaci
    +    Pomeni
    +    Servis Obaveštenja
    +    Servis obaveštenja radi.
    +    Servis obaveštenja ne radi.
    +\nPokušajte da ponovo pokrenete aplikaciju.
    +    Pokreni servis
    +
    +    Servis obaveštenja, automatsko ponovno pokretanje
    +    Servis je eliminisan i pokrenut ponovo automatski.
    +    Servis nije uspeo da se ponovo pokrene
    +
    +    Servis će se početi sa radom prilikom ponovnog pokretanja uređaja.
    +    Optimizacija potrošnje baterije
    +    Optimizacija potrošnje baterije ne utiče na Riot.
    +    Ignoriši optimizacije
    +
    +    Normalno
    +    Smanjena privatnost
    +    Aplikaciji treba dozvola za rad u pozadini
    +    • Obaveštenja sadrže samo meta podatke
    +    • Obaveštenja sadrže meta podatke i poruku
    +    "•  Obaveštenja "neće prikazivati sadržaj poruke
    +
    +    Zvuk obaveštenja
    +    Omogući obaveštenja za ovaj nalog
    +    Omogući obaveštenja za ovaj uređaj
    +    Upali ekran na 3 sekunde
    +    Podesi bučna obaveštenja
    +    Podesi obaveštenja o pozivima
    +    Podesi tiha obaveštenja
    +    Izabeti LED boju, vibraciju, zvuk…
    +
    +
    +    Kada sam pozvan u sobu
    +    Poruke koje je poslao bot
    +
    +    Sinhronizacija u pozadini
    +    Optimizovano za potrošnju baterije
    +    Bez sinhronizacije u pozadini
    +    sekundu
    +    sekundi
    +
    +    Verzija
    +    olm verzija
    +    Drugo
    +    Napredno
    +    Kriptografija
    +    Rukovanje kriptografskim ključevima
    +    Učitaj kontakte
    +    Dozvola za kontakte
    +    Zemlja telefonskog imenika
    +    Glavni prikaz
    +    Uređaji
    +    Šalji obaveštenja o pisanju
    +    Dozvoli da drugi korisnici znaju da pisete.
    +    Prikaži obaveštenja o pročitanim porukama
    +    Klikni na obaveštenja o pročitanim porukama za detaljan prikaz.
    +    Pokaži obaveštenja o priključivanjima i napuštanjima
    +    Ne odnosi se na pozivnice, izbacivanja i zabrane.
    +    Prikaži obaveštenja o nalogu
    +    Uključuje obaveštenja o promeni avatara i imena.
    +    Vibriraj kad neko pomene korisnika
    +    Pošalji poruku pritiskom na enter
    +    Dugme enter na softverskoj tastaturi će poslati poruku umesto da napravi novu liniju
    +
    +    Deaktiviraj nalog
    +    Deaktiviraj moj nalog
    +    Pronalaženje
    +    Upravljajte vašim podešavanjima za pronalaženje.
    +    Privatnost obaveštenja
    +    Riot može da radi u pozadini kako bi upravljao vašim obaveštenjima sigurno i privatno. Ovo može da utiče na potrošnju baterije.
    +    Dozvoli
    +    Izaberi drugu opciju
    +
    +    Pozadinska veza
    +    Da, želim da pomognem!
    +
    +    Režim uštede podataka
    +    Režim uštede podataka primenjuje poseban filter tako da se obaveštenja o prisutnosti i pisanju filtriraju.
    +
    +    Podaci o uređaju
    +    ID
    +    Javno ime
    +    Promeni javno ime
    +    Poslednji put viđen
    +    Ova operacija zahteva dodatnu autentifikaciju.
    +\nMolim unesite vašu lozinku, kako bi nastavili.
    +    Autentifikacija
    +    Lozinka:
    +    Pošalji
    +
    +    Prijavljen kao
    +    Jezik
    +    Izaberi jezik
    +
    +    Čeka se verifikacija
    +    Molim proverite vašu elektronsku poštu i kliknite na link u njoj. Kad ovo uradite, kliknite na nastavi.
    +    Lozinka
    +    Promeni lozinku
    +    Trenutna lozinka
    +    Nova lozinka
    +    Potvrdite novu lozinku
    +    Promeni lozinku
    +    Promena lozinke nije uspela
    +    Lozinka nije tačna
    +    Vaša lozinka je promenjena
    +    Izaberi zemlju
    +
    +    Zemlja
    +    Molim izaberite zemlju
    +    Broj telefona
    +    Pogrešan broj telefona za izabranu zemlju
    +    Verifikacija putem telefona
    +    Poslali smo vam SMS sa aktivacionim kodom. Molimo unesite ovaj kod ispod.
    +    Unesi aktivacioni kod
    +    Desila se greška prilikom validacije vašeg broja telefona
    +    Kod
    +    Desila se greška prilikom verifikacije vašeg broja telefona.
    +    Dodatne informacije: %s
    +
    +    Izaberi
    +    Izaberi
    +    3 dana
    +    1 nedelja
    +    1 mesec
    +    Zauvek
    +
    +    Omiljeno
    +    Nizak prioritet
    +    Ništa
    +
    +    Pristup sobi
    +    Ko može čitati istoriju\?
    +    Ko može pristupiti ovoj sobi\?
    +
    +    BIlo ko
    +    Samo članovi (od trenutka kad je ova opcija izabrana)
    +    Samo članovi (od trenutka kad su pozvani)
    +    Samo članovi (od trenutka kad su se pridružili)
    +
    +    Samo ljudi koji su pozvani
    +    Bilo ko, ko zna link sobe, sem gostiju
    +    Bilo ko, ko zna link sobe, uključujući goste
    +
    +    Izbačeni korisnici
    +
    +    Napredno
    +    Interni ID ove sobe
    +    Adrese
    +    Laboratorije
    +    Ovo su eksperimentalne karakteristike koje se mogu pokvariti na neočekivane načine. Koristiti sa pažnjom.
    +    Nova adresa (npr. #foo:matrix.org)
    +
    +    Algoritam
    +    ID sesije
    +    Greška prilikom dešifrovanja
    +
    +    Informacije o uređaju pošiljaoca
    +    Javno ime
    +    Verifikacija
    +    Uvezi ključeve za sobu
    +    Uvezi ključeve iz datoteke
    +    Uvezi
    +    Šifruj samo ka verifikovanim uređajima
    +    Nikad ne šalji šifrovane poruke ka neverifikovanim uređajima sa ovog uređaja.
    +    Verifikovan
    +    Na crnoj listi
    +
    +    nepoznat uređaj
    +    nepoznata IP adresa
    +    ništa
    +
    +    Verifikuj
    +    Ukloni verifikaciju
    +    Stavi na crnu listu
    +    Ukloni sa crne liste
    +
    +    Verifikuj uređaj
    +    Soba sadrži nepoznate uređaje
    +    Soba
    +    Nove poruke
    +    Nova pozivnica
    +    Ja
    +    Veličina fonta
    +    Najmanji
    +    Mali
    +    Normalni
    +    Veliki
    +    Veći
    +    Najveći
    +    Ogroman
    +
    +    Slanje zahteva nije uspelo.
    +    Niste u ovoj sobi.
    +    Nemate dozvolu da uradite to u ovoj sobi.
    +    Nedostajući room_id u zahtevu.
    +    Nedostajući user_id u zahtevu.
    +    Soba %s nije vidljiva.
    +    Nedostaje parametar koji je obavezan.
    +    Parametar nije ispravan.
    +    Šalji glasovne poruke
    +
    diff --git a/vector/src/main/res/values-bg/strings.xml b/vector/src/main/res/values-bg/strings.xml
    index d8570ef0a4..e03e174e54 100644
    --- a/vector/src/main/res/values-bg/strings.xml
    +++ b/vector/src/main/res/values-bg/strings.xml
    @@ -212,14 +212,6 @@
         Парола
         Нова парола
         Потребителско име
    -    Добавете имейл адрес в профила си, за да позволите на потребители да ви откриват и да можете да възстановявате Вашата парола.
    -    Добавете телефонен номер в профила си, за да позволите на потребители да ви откриват.
    -    Добавете имейл адрес и/или телефонен номер в профила си, за да позволите на потребители да ви откриват.
    -
    -Имейл адресът също Ви позволява да възстановите Вашата парола.
    -    Добавете имейл адрес и телефонен номер в профила си, за да позволите на потребители да Ви откриват.
    -
    -Имейл адресът също Ви позволява да възстановявате Вашата парола.
         Имейл адрес
         Имейл адрес (по избор)
         Телефонен номер
    @@ -1487,7 +1479,7 @@
     
         Събитие изтрито от потребителя
         Събитие модерирано от администратор на стаята
    -    Последно редактирано от %s на %s
    +    Последно редактирано от %1$s на %2$s
     
     
         Повредено събитие, не може да се покаже
    @@ -1566,7 +1558,7 @@
     \n
     \nRiotX поддържа: • Вход в съществуващ акаунт • Създаване на стая и влизане в публични стаи • Приемане и отхвърляне на покани • Показване на списък със стаите • Преглеждане на информация за стая • Изпращане на текстови съобщения • Изпращане на прикачени файлове • Четене и писане на съобщения в шифровани стаи • Шифроване: резервни копия на E2E ключове, потвърждение на устройства, заявяване и отговаряне на заявки за споделяне на ключове • Уведомления • Светла, Тъмна и Черна тема
     \n
    -\nЗасега не всички функции на Riot са налични в RiotX. Основни липсващи (и скоро пристигащи!) функции са: • Създаване на нов профил • Настройки на стаи (показване на членове и т.н.) • Създаване на директни чатове • Обаждания • Приспособления • …
    +\nЗасега не всички функции на Riot са налични в RiotX. Основни липсващи (и скоро пристигащи!) функции са: • Създаване на нов профил • Настройки на стаи (показване на членове и т.н.) • Обаждания • Приспособления • …
     
         Директни съобщения
     
    @@ -1613,4 +1605,21 @@
     
         Виж историята на редакциите
     
    +    Прегледай
    +    Откажи
    +
    +    За да продължите трябва да приемете условията за използване на услугата.
    +
    +    Предишни версии на Riot имат проблем със сигурността, давайки достъп на сървъра за самоличност (%1$s) до профила ви. Ако се доверявате на %2$s, може да игнорирате проблема; в противен случай, излезте от профила си и влезте наново.
    +\n
    +\nНаучете повече подробности тук:
    +\nhttps://medium.com/@RiotChat/36b4792ea0d6
    +
    +    Условия за ползване
    +    Прегледай условията
    +    Бъдете откриваеми от други
    +    Използвайте ботове, връзки с други мрежи, приспособления и стикери
    +
    +    Прочетете на
    +
     
    diff --git a/vector/src/main/res/values-bn-rIN/strings.xml b/vector/src/main/res/values-bn-rIN/strings.xml
    index ab4c223fa6..06ee50219e 100644
    --- a/vector/src/main/res/values-bn-rIN/strings.xml
    +++ b/vector/src/main/res/values-bn-rIN/strings.xml
    @@ -202,14 +202,6 @@
         পাসওয়ার্ড
         নতুন পাসওয়ার্ড
         ব্যবহারকারীর নাম
    -    ব্যবহারকারীদের আপনাকে খুঁজে পাওয়ার জন্য আপনার অ্যাকাউন্টে একটি ইমেল ঠিকানা যোগ করুন এবং আপনাকে পাসওয়ার্ড পুনরায় সেট করতে দিন।
    -    ব্যবহারকারীদের আপনাকে খুঁজে পাওয়ার জন্য আপনার অ্যাকাউন্টে একটি ফোন নম্বর যুক্ত করুন।
    -    ব্যবহারকারীদের আপনাকে খুঁজে পাওয়ার জন্য আপনার অ্যাকাউন্টে একটি ইমেল ঠিকানা এবং / অথবা একটি ফোন নম্বর যোগ করুন।
    -\n
    -\nইমেল ঠিকানা আপনাকে আপনার পাসওয়ার্ড পুনরায় সেট করতে দেয়।
    -    ব্যবহারকারীদের আপনাকে খুঁজে পাওয়ার জন্য আপনার অ্যাকাউন্টে একটি ইমেল ঠিকানা এবং একটি ফোন নম্বর যুক্ত করুন।
    -\n
    -\nইমেল ঠিকানা আপনাকে আপনার পাসওয়ার্ড পুনরায় সেট করতে দেয়।
         ইমেইল ঠিকানা
         ইমেইল ঠিকানা (ঐচ্ছিক)
         ফোন নম্বর
    diff --git a/vector/src/main/res/values-bs/strings.xml b/vector/src/main/res/values-bs/strings.xml
    index 7bebad6c05..e48652a9b6 100644
    --- a/vector/src/main/res/values-bs/strings.xml
    +++ b/vector/src/main/res/values-bs/strings.xml
    @@ -138,14 +138,6 @@
         Lozinka
         Nova lozinka
         Korisničko ime
    -    Dodajte email adresu na svoj račun kako biste korisnicima omogućili da vas pronađu, te vam omoguće resetovanje lozinke.
    -    Dodaj broj telefona na svoj račun da bi omogućili korisnicima da vas pronađu.
    -    Dodajte email adresu i / ili telefonski broj na svoj račun da biste omogućili korisnicima da vas pronađu.
    -
    -Email adresa će vam također omogućiti resetovanje lozinke.
    -    Dodajte email adresu i telefonski broj na svoj račun da biste omogućili korisnicima da vas pronađu.
    -
    -Email adresa će vam također omogućiti resetovanje lozinke.
         Email adresa
         Email adresa (nije obavezno)
         Broj telefona
    diff --git a/vector/src/main/res/values-ca/strings.xml b/vector/src/main/res/values-ca/strings.xml
    index ca7a2287b5..d9af5deb46 100644
    --- a/vector/src/main/res/values-ca/strings.xml
    +++ b/vector/src/main/res/values-ca/strings.xml
    @@ -169,10 +169,6 @@
         Contrasenya
         Contrasenya nova
         Nom d\'usuari
    -    "Afegiu un correu electrònic al vostre compte per tal que altres usuaris us trobin i per poder reiniciar la contrasenya."
    -    "Afegiu un número de telefon al vostre compte per tal que altres usuaris us trobin."
    -    "Afegiu un correu electrònic i/o un númerode telefon al vostre compte per tal que altres usuaris us trobin.\n\nSi afegiu un correu electrònic també us permetrà poder reiniciar la contrasenya."
    -    "Afegiu un correu electrònic i un número de telefon al vostre compte per tal que altres usuaris us trobin.\n\nSi afegiu un correu electrònic també us permetrà poder reiniciar la contrasenya."
         Correu electrònic
         Correu electrònic (opcional)
         Número de telefon
    @@ -1531,7 +1527,7 @@ Per què triar Riot.im?
     
         Esdeveniment eliminat per l\'usuari
         Esdeveniment moderat per l\'administrador de la sala
    -    Última edició per %s el %s
    +    Última edició per %1$s el %2$s
     
     
         Esdeveniment mal format, no es pot mostrar
    diff --git a/vector/src/main/res/values-cs/strings.xml b/vector/src/main/res/values-cs/strings.xml
    index eda505ed14..53f1d35d79 100644
    --- a/vector/src/main/res/values-cs/strings.xml
    +++ b/vector/src/main/res/values-cs/strings.xml
    @@ -158,8 +158,6 @@
         Heslo
         Nové heslo
         Uživatelské jméno
    -    Přidat emailovou adresu k vašemu účtu aby vás mohli ostatní uživatelé najít a abyste si mohl vyresetovat heslo.
    -    Přidat telefonní číslo k vašemu účtu aby vás mohli ostatní uživatelé najít.
         E-mailová adresa
         E-mailová adresa (nepovinné)
         Telefonní číslo
    @@ -237,12 +235,6 @@
     
         Šifrovaná zpráva
     
    -    Přidejte k účtu e-mailovou adresu a/nebo telefoní číslo, aby Vás mohli ostatní nalézt.
    -
    -E-mailová adresa Vám také umožní obnovit heslo.
    -    Přidejte k účtu e-mailovou adresu a telefoní číslo, aby Vás mohli ostatní nalézt.
    -
    -E-mailová adresa Vám také umožní obnovit heslo.
         Registrace e-mailem a telefonním číslem najednou není zatím podporována z důvodu neexistujícího API. Pouze telefonní čislo bude bráno v potaz.
     
     Vaši e-mailovou adresu můžete přidat k profilu v nastavení.
    diff --git a/vector/src/main/res/values-da/strings.xml b/vector/src/main/res/values-da/strings.xml
    index d6ee79a342..fff61797f5 100644
    --- a/vector/src/main/res/values-da/strings.xml
    +++ b/vector/src/main/res/values-da/strings.xml
    @@ -137,14 +137,6 @@
         Adgangskode
         Ny adgangskode
         Brugernavn
    -    Tilføj en mailadresse til din konto for at lade brugere finde dig og for at gendanne dit password.
    -    Tilføj et telefonnummer til din konto for at brugere kan finde dig.
    -    Tilføj en emailadresse og/eller et telefonnummer til din konto for at andre brugere kan finde dig.
    -
    -En emailadresse vil også give dig mulighed for at nulstille din adgangskode.
    -    Tilføj en emailadresse og et telefonnummer til din konto for at andre brugere kan finde dig.
    -
    -En emailadresse vil også give dig mulighed for at nulstille dit password.
         Emailadresse
         Emailadresse (valgfri)
         Telefonnummer
    diff --git a/vector/src/main/res/values-de/strings.xml b/vector/src/main/res/values-de/strings.xml
    index 313ead3a27..b8b86b5081 100644
    --- a/vector/src/main/res/values-de/strings.xml
    +++ b/vector/src/main/res/values-de/strings.xml
    @@ -140,14 +140,6 @@
         Passwort
         Neues Passwort
         Benutzername
    -    Füge eine E-Mail-Adresse zu deinem Account hinzu, damit andere Benutzer dich finden können und du dein Passwort zurücksetzen kannst.
    -    Füge eine Telefonnummer zu deinem Benutzerkonto hinzu, damit andere Benutzer dich finden können.
    -    Füge eine E-Mail-Adresse und/oder eine Telefonnummer zu deinem Benutzerkonto hinzu, damit andere Benutzer dich finden können.
    -
    -Das Hinzufügen einer E-Mail-Adresse erlaubt es dir außerdem, dein Passwort zurückzusetzen.
    -    Füge eine E-Mail-Adresse und eine Telefonnummer zu deinem Benutzerkonto hinzu, damit andere Benutzer dich finden können.
    -
    -Das Hinzufügen einer E-Mail-Adresse erlaubt es dir außerdem, dein Passwort zurückzusetzen.
         E-Mail-Adresse
         E-Mail-Adresse (optional)
         Telefonnummer
    @@ -1527,7 +1519,7 @@ Wenn du diese neue Wiederherstellungsmethode nicht eingerichtet hast, kann ein A
     
         Ereignis von Benutzer gelöscht
         Ereignis moderiert durch Raum-Administrator
    -    Zuletzt bearbeitet von %s am %s
    +    Zuletzt bearbeitet von %1$s am %2$s
     
     
         Neuen Raum erstellen
    @@ -1650,7 +1642,7 @@ Wenn du diese neue Wiederherstellungsmethode nicht eingerichtet hast, kann ein A
     
         Können Sie nicht finden, wonach Sie suchen\?
         Erstelle einen neuen Raum
    -    Name oder ID (#Beispiel: matrix.org)
    +    Name oder ID (#beispiel:matrix.org)
     
         Aktivieren Sie das Streichen, um in der Zeitleiste zu antworten
     
    @@ -1660,4 +1652,8 @@ Wenn du diese neue Wiederherstellungsmethode nicht eingerichtet hast, kann ein A
     
         Raum betreten…
     
    +    Ablehnen
    +
    +    app_id:
    +    Überprüfung
     
    diff --git a/vector/src/main/res/values-el/strings.xml b/vector/src/main/res/values-el/strings.xml
    index 6e4706bc63..4b70066fe4 100644
    --- a/vector/src/main/res/values-el/strings.xml
    +++ b/vector/src/main/res/values-el/strings.xml
    @@ -3,7 +3,7 @@
         
         
         el
    -    ΕL
    +    GR
     
     
         Ακύρωση
    diff --git a/vector/src/main/res/values-es-rMX/strings.xml b/vector/src/main/res/values-es-rMX/strings.xml
    index f9196688b8..423ecb5438 100644
    --- a/vector/src/main/res/values-es-rMX/strings.xml
    +++ b/vector/src/main/res/values-es-rMX/strings.xml
    @@ -139,10 +139,6 @@
         Contraseña
         Contraseña nueva
         Nombre de usuario
    -    "Añadir un correo electrónico a tu cuenta para que otros te descubran, y para que puedas reiniciar tu contraseña."
    -    "Añadir número telefónico a tu cuenta para que otros te descubran."
    -    "Añadir un correo electrónico y/o número telefónico a tu cuenta para que otros te descubran.\n\nEl correo electrónico te permitira reiniciar tu contraseña."
    -    "Añadir un correo electrónico y un número telefónico a tu cuenta para que otros te descubran.\n\nEl correo electrónico te permitira reiniciar tu contraseña."
         Correo electrónico
         Correo electrónico (opcional)
         Número telefónico
    diff --git a/vector/src/main/res/values-es/strings.xml b/vector/src/main/res/values-es/strings.xml
    index ce983090c6..894f7e3d47 100644
    --- a/vector/src/main/res/values-es/strings.xml
    +++ b/vector/src/main/res/values-es/strings.xml
    @@ -140,14 +140,6 @@
         Contraseña
         Nueva contraseña
         Nombre de usuario
    -    Añade una dirección de correo electrónico a tu cuenta para poder ser descubierto por otros usuarios, y ser capaz de restablecer tu contraseña.
    -    Añade un número telefónico a tu cuenta para poder ser descubierto por otros usuarios.
    -    Añade una dirección de correo electrónico y/o un número telefónico a tu cuenta para poder ser descubierto por otros usuarios.
    -
    -Además, la dirección de correo electrónico te permitirá restablecer tu contraseña.
    -    Añade una dirección de correo electrónico y un número telefónico a tu cuenta para poder ser descubierto por otros usuarios.
    -
    -Además, la dirección de correo electrónico te permitirá restablecer tu contraseña.
         Dirección de correo electrónico
         Dirección de correo electrónico (opcional)
         Número telefónico
    @@ -1540,7 +1532,7 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de
     
         Evento borrado por el usuario
         Evento moderado por el administrador de la sala
    -    Última edición por %s on %s
    +    Última edición por %1$s on %2$s
     
     
         Evento con error, no se puede mostrar
    diff --git a/vector/src/main/res/values-eu/strings.xml b/vector/src/main/res/values-eu/strings.xml
    index f4c2b81ece..71b5a6ab30 100644
    --- a/vector/src/main/res/values-eu/strings.xml
    +++ b/vector/src/main/res/values-eu/strings.xml
    @@ -129,14 +129,6 @@
         Pasahitza
         Pasahitz berria
         Erabiltzaile-izena
    -    Gehitu e-mail helbide bat zure kontura erabiltzaileek zu aurkitzea baimentzeko, eta zuk pasahitza berrezarri ahal izateko.
    -    Gehitu telefono zenbaki bat zure kontura beste erabiltzaileek zu aurkitzea ahalbidetzeko.
    -    Gehitu e-mail helbide bat eta/edo telefono zenbaki bat zure kontura beste erabiltzaileek zu aurkitzea ahalbidetzeko.
    -
    -E-mail helbideak zuri pasahitza berrezartzea ahalbidetzen dizu ere.
    -    Gehitu e-mail helbide bat eta telefono zenbaki bat zure kontura beste erabiltzaileek zu aurkitzea ahalbidetzeko.
    -
    -E-mail helbideak zuri pasahitza berrezartzea ahalbidetzen dizu ere.
         E-mail helbidea
         E-mail helbidea (aukerakoa)
         Telefono zenbakia
    @@ -422,7 +414,7 @@ Kontuan izan ekintza honek aplikazioa berrabiaraziko duela eta denbora bat behar
         Bigarren planoko sinkronizazioa
         Gaitu bigarren planoko sinkronizazioa
         Sinkronizazio eskaerak debora-muga gainditu du
    -    Eskaeren arteko itxaronaldia
    +    Sinkronizazioen arteko itxaronaldia
         segundo
         segundo
     
    @@ -449,10 +441,10 @@ Kontuan izan ekintza honek aplikazioa berrabiaraziko duela eta denbora bat behar
         Finkatu irakurri gabeko mezuak dituzten gelak
         Gailuak
         Erakutsi mezu guztien denbora-zigilua
    -    Gailuaren xehetasunak
    +    Gailuaren informazioa
         ID
    -    Izena
    -    Gailuaren izena
    +    Izen publikoa
    +    Aldatu izen publikoa
         Azkenekoz ikusia
         %1$s @ %2$s
         Eragiketa honek autentifikazio gehigarria behar du.
    @@ -582,9 +574,9 @@ Kontuan izan ekintza honek aplikazioa berrabiaraziko duela eta denbora bat behar
         Deszifratze errorea
     
         Igorlearen gailuaren informazioa
    -    Gailuaren izena
    -    Izena
    -    Gailuaren IDa
    +    Izen publikoa
    +    Izen publikoa
    +    IDa
         Gailuaren gakoa
         Egiaztaketa
         Ed25519 hatz-marka
    @@ -878,7 +870,7 @@ Baten bat gehitu orain?
         Desaktibatu kontua
     
         %1$s hasiera-zerbitzaria erabiltzen jarraitzeko erabilera baldintzak irakurri eta onartu behar dituzu.
    -    Irakurri orain
    +    Berrikusi orain
     
         Desaktibatu kontua
         Honek kontua behin betirako erabilgaitza bihurtuko du. Ezin izango duzu saioa hasi, eta ezin izango du beste inork ID hori erabili. Kontua dagoen gela guztietatik aterako da, eta kontuaren xehetasunak identitate-zerbitzaritik ezabatuko dira. Ekintza hau ezin da desegin.
    @@ -1056,7 +1048,7 @@ Kontuan izan ekintza honek aplikazioa berrabiaraziko duela eta denbora bat behar
     
         Onartu
     
    -    Berrikusi eta onartu hasiera-zerbitzari honen politikak:
    +    Irakurri eta onartu hasiera-zerbitzari honen baldintzak:
     
         %1$d/%2$d gako ongi inportatu dira.
     
    @@ -1494,7 +1486,7 @@ Abisua: Fitxategi hau ezabatu daiteke aplikazioa desinstalatzen bada.
     
         Gertaera ezabatu du erabiltzaileak
         Gertaera moderatu du gelako administratzaileak
    -    %s erabiltzaileak editatuta, azkenekoz: %s
    +    %1$s erabiltzaileak editatuta, azkenekoz: %2$s
     
     
         Gaizki formatutako gertaera, ezin da bistaratu
    @@ -1572,7 +1564,7 @@ Abisua: Fitxategi hau ezabatu daiteke aplikazioa desinstalatzen bada.
     \n
     \nRiotX bezeroak honakoa ahalbidetzen du: • Badagoen kontu batean saioa hasi • Gelak sortu eta gela publikoetara elkartu • Gonbidapenak onartu edo ukatu • Erabiltzailearen gelak zerrendatu • Gelaren xehetasunak ikusi • Testuzko mezuak bidali • Eranskinak bidali • Zifratutako geletan mezuak irakurri eta idatzi • Zifratzea: E2Egakoen babeskopia, gailuaren egiaztaketa aurreratua, gakoa partekatzeko eskaria eta erantzuna • Push jakinarazpena • Gai argia, iluna eta beltza
     \n
    -\nEz dira oraindik Riot bezeroaren ezaugarri guztiak ezarri RiotX bezeroan. Falta diren (eta laster etorriko direnen) artean nabarmenak dira: • Kontua sortzea • Gelaren ezarpenak (gelako kideak zerrendatzea, eta abar.) • Txat zuzenerako gelak sortzea • Deiak • Trepetak • …
    +\nEz dira oraindik Riot bezeroaren ezaugarri guztiak ezarri RiotX bezeroan. Falta diren (eta laster etorriko direnen) artean nabarmenak dira: • Kontua sortzea • Gelaren ezarpenak (gelako kideak zerrendatzea, eta abar.) • Deiak • Trepetak • …
     
         app_display_name:
         Mezu zuzenak
    @@ -1620,4 +1612,89 @@ Abisua: Fitxategi hau ezabatu daiteke aplikazioa desinstalatzen bada.
     
         Ikusi edizioen historiala
     
    +    Bat ere ez
    +    Indargabetu
    +    Deskonektatu
    +    Berrikusi
    +    Ukatu
    +
    +    Ez da identitate-zerbitzaririk konfiguratu.
    +
    +    Deiak huts egin du zerbitzaria gaizki konfiguratuta dagoelako
    +    Eskatu zure hasiera-zerbitzariko administratzaileari (%1$s) TURN zerbitzari bat konfiguratu dezala deiak ondo funtzionatu dezaten.
    +\n
    +\nBestela, %2$s zerbitzari publikoa erabili dezakezu, baina hau ez da hain fidagarria izango, eta zure IP-a partekatuko du zerbitzari horrekin. Hau ezarpenetan aldatu dezakezu.
    +    Saiatu %s erabiltzen
    +    Ez galdetu berriro
    +
    +    Ezarri E-mail bat kontua berreskuratzeko, eta gero aukeran zure ezagunek aurkitu zaitzaten.
    +    Ezarri telefono bat gero aukeran zure ezagunek aurkitu zaitzaten.
    +    Ezarri E-mail bat kontua berreskuratzeko. Erabili geroo aukeran e-maila edo telefonoa zure ezagunek aurkitu zaitzaten.
    +    Ezarri E-mail bat kontua berreskuratzeko. Erabili geroo aukeran e-maila edo telefonoa zure ezagunek aurkitu zaitzaten.
    +    Ezin izan da hasiera-zerbitzari bat atzitu URL honetan, egiaztatu ezazu
    +    Baimendu lehenetsitako deien laguntzarako zerbitzaria
    +    %s erabiliko da laguntzarako zure hasiera-zerbitzariak ez badu bat eskaintzen (Zure IP helbidea deian partekatuko da)
    +    Gehitu identitate-zerbitzari bat zure ezarpenetan ekintza hau burutzeko.
    +    Bigarren planoko sinkronizazio modua (Esperimentala)
    +    Bateria erabilerarako optimizatua
    +    Riot bigarren planoan sinkronizatuko da gailuaren baliabide mugatuen erabilera ahal beste murriztuz (bateria).
    +\nZure gailuaren baliabideen egoeraren arabera, sistema eragileak sinkronizazioa atzeratu dezake.
    +    Denbora errealerako optimizatua
    +    Riot bigarren planoan sinkronizatuko da maiztasun finkoarekin (konfiguragarria).
    +\nHonek irrati eta bateriaren erabileran eragina izango du, eta Riot gertaerei adi dagoela dion jakinarazpen bat bistaratuko da etengabe.
    +    Ez sinkronizatu bigarren planoan
    +    Ez zaizu jasotako mezuei buruz jakinaraziko aplikazioa bigarren planoan dagoenean.
    +    Huts egin du ezarpenak eguneratzean.
    +
    +
    +    Hobetsitako sinkronizazio tartea
    +    %s
    +\nSinkronizazioa atzeratu daiteke zure baliabideen arabera (bateria) edo gailuaren egoeraren arabera (lo).
    +    Aurkitzea
    +    Kudeatu aurkitzeko ezarpenak.
    +    Izen publikoa (Zurekin komunikatzen den jendeak ikusi dezake)
    +    Gailu baten izen publikoa zurekin komunikatzen den jendeak ikusi dezake
    +    Jarraitzeko erabilera baldintzak onartu behar dituzu.
    +
    +    Ez duzu identitate zerbitzaririk erabiltzen
    +    Ez da identitate zerbitzaririk konfiguratu, zure pasahitza berrezartzeko beharrezkoa da.
    +
    +    Riot bertsio zaharrek segurtasun akats bat zuten eta zure identitate zerbitzariak (%1$s) zure kontua atzitu zezakeen. %2$s fidagarritzat jotzen baduzu hau ezikusi dezakezu, bestela maiatu saioa eta hasi berriro.
    +\n
    +\nXehetasun gehiago hemen:
    +\nhttps://medium.com/@RiotChat/36b4792ea0d6
    +
    +    Badirudi beste hasiera-zerbitzari batera konektatzen saiatzen ari zarela. Saioa amaitu nahi duzu\?
    +
    +    Erabilera baldintzak
    +    Irakurri baldintzak
    +    Izan besteentzat aurkigarria
    +    Erabili botak, zubiak, trepetak eta eranskailu multzoak
    +
    +    Irakurri hemen
    +
    +
    +    Identitate-zerbitzaria
    +    Deskonektatu identitate-zerbitzaria
    +    Konfiguratu identitate-zerbitzaria
    +    Aldatu identitate-zerbitzaria
    +    Orain %1$s erabiltzen ari zara ezagunak aurkitzeko eta ezagunek zu aurkitzeko.
    +    Orain ez duzu identitate-zerbitzaririk erabiltzen. Kontaktuak aurkitzeko eta aurkigarria izateko, gehitu bat azpian.
    +    E-mail helbide aurkigarria
    +    Aurkitze aukerak behin e-mail bat gehitu duzunean agertuko dira.
    +    Aurkitze aukerak behin telefono zenbaki bat gehitu duzunean agertuko dira.
    +    Identitate-zerbitzaritik deskonektatzean beste erabiltzaileek ezin izango zaituzte e-mail edo telefonoa erabilita aurkitu, eta zuk ezin izango dituzu e-mail edo telefonoa erabilita aurkitu.
    +    Telefono zenbaki aurkigarriak
    +    Berrespen e-mail bat bidali dizugu %s helbidera, begiratu zure e-maila eta sakatu baieztapen esteka
    +    Egiteke
    +
    +    Sartu identitate-zerbitzari berria
    +    Ezin izan da identitate-zerbitzarira konektatu
    +    Sartu identitate-zerbitzariaren URL-a
    +    Identitate-zerbitzariak ez du erabilera baldintzarik
    +    Hautatu duzun identitate-zerbitzariak ez du erabilera baldintzarik. Jarraitu soilik zerbitzuaren jabea fidagarritzat jotzen baduzu
    +    SMS mezu bat bidali zaizu %s zenbakira. Sartu hemen mezu horrek daukan egiaztatze-kodea.
    +
    +    Orain e-mail helbideak edo telefono zenbakiak partekatzen dituzu %s zerbitzarian. %s zerbitzarira konektatu beharko zara partekatzeari uzteko.
    +    Onartu %s identitate-zerbitzariaren erabilera baldintzak besteek zu e-mail helbidea edo telefonoa erabiliz aurkitzea ahalbidetzeko.
     
    diff --git a/vector/src/main/res/values-fa/strings.xml b/vector/src/main/res/values-fa/strings.xml
    index be63b4fc55..7f8a2d0c3a 100644
    --- a/vector/src/main/res/values-fa/strings.xml
    +++ b/vector/src/main/res/values-fa/strings.xml
    @@ -326,14 +326,6 @@
         پیوند دائمی
         مشاهده منبع رمز نشده
         تاریخچه
    -    یک ایمیل به حساب خود اضافه کنید تا دیگر کاربران بتوانند شما را پیدا کنند و برای شما نیز امکاناتی مانند بازیابی گذرواژه فعال شود.
    -    یک شماره تلفن به حساب خود اضافه کنید تا دیگر کاربران بتوانند شما را توسط آن پیدا کنند.
    -    یک ایمیل یا شماره تلفن به حساب خود اضافه کنید تا دیگر کاربران بتوانند شما را پیدا کنند.
    -\n
    -\nنشانی ایمیل امکان بازیابی گذرواژه را نیز به شما می‌دهد.
    -    یک ایمیل و یک شماره تلفن به حساب خود اضافه کنید تا دیگر کاربران بتوانند شما را پیدا کنند.
    -\n
    -\nنشانی ایمیل امکان بازیابی گذرواژه را نیز به شما می‌دهد.
         نام کاربری تنها می‌تواند شامل حروف انگلیسی، اعداد، نقطه، خط زیر و خط تیره باشد
         ثبت‌نام با ورود همزمان ایمیل و شماره تلفن در حال حاضر پشتیبانی نمی‌شود. تنها شماره تلفن شما برای حساب‌تان ثبت خواهد شد.
     \n
    diff --git a/vector/src/main/res/values-fi/strings.xml b/vector/src/main/res/values-fi/strings.xml
    index 60ddb267f4..74680e0fb2 100644
    --- a/vector/src/main/res/values-fi/strings.xml
    +++ b/vector/src/main/res/values-fi/strings.xml
    @@ -148,14 +148,6 @@
         Salasana
         Uusi salasana
         Käyttäjätunnus
    -    "Lisää sähköpostiosoite tiliisi antaaksesi muiden käyttäjien löytää sinut. Voit myös nollata salasanasi."
    -    Lisää puhelinnumero tiliisi, jotta muut käyttäjät löytävät sinut.
    -    Lisää sähköpostiosoite ja/tai puhelinnumero tiliisi, jotta muut käyttäjät löytävät sinut.
    -\n
    -\nSähköpostiosoitteella voit myös nollata salasanasi.
    -    Lisää sähköpostiosoite ja puhelinnumero tiliisi, jotta muut käyttäjät löytävät sinut.
    -\n
    -\nSähköpostiosoitteella voit myös nollata salasanasi.
         Sähköpostiosoite
         Sähköpostiosoite (valinnainen)
         Puhelinnumero
    @@ -360,7 +352,7 @@
         Viesteja ei lähetetty. %1$s vai %2$s\?
         Viestejä ei lähetetty koska huoneessa on tuntemattomia laitteita. %1$s vai %2$s\?
         Lähetä kaikki uudelleen
    -    peru kaikki
    +    Peruuta kaikki
         Lähetä lähettämättömät viestit
         Poista lähettämättömät viestit
         Tiedostoa ei löydy
    @@ -463,7 +455,7 @@
         Taustasynkronointi
         Käytä taustasynkronointia
         Synkronointipyynnön aikakatkaisu
    -    Viive jokaisen pyynnön välillä
    +    Viive synkronointien välillä
         sekunti
         sekuntia
     
    @@ -493,8 +485,8 @@
         Laitteet
         Laitteen tiedot
         ID
    -    Nimi
    -    Laitteen nimi
    +    Julkinen nimi
    +    Päivitä julkinen nimi
         Viimeksi käytetty
         %1$s @ %2$s
         Tämä toimenpide vaatii lisätunnistautumisen.
    @@ -627,8 +619,8 @@
         Salauksenpurkuvirhe
     
         Lähettävän laitteen tiedot
    -    Laitteen nimi
    -    Nimi
    +    Julkinen nimi
    +    Julkinen nimi
         Laitteen ID
         Laitteen avain
         Vahvistus
    @@ -1534,7 +1526,7 @@ Jotta et menetä mitään, automaattiset päivitykset kannattaa pitää käytös
         Asetukset
         Tietoturva ja yksityisyys
         Olet ajan tasalla!
    -    Viimeksi muokannut %s %s
    +    Viimeksi muokannut %1$s %2$s
     
     
         Vaihda verkkoa
    @@ -1612,4 +1604,57 @@ Jotta et menetä mitään, automaattiset päivitykset kannattaa pitää käytös
         Play Storen kuvaus
         Push-säännöt
         %1$s luodaksesi tilin.
    +    Katkaise yhteys
    +    Kieltäydy
    +
    +    Identiteettipalvelinta ei ole määritetty.
    +
    +    Puhelu epäonnistui väärin määritetyn palvelimen takia
    +    Älä kysy uudestaan
    +
    +    Kotipalvelinta ei voi tavoittaa tästä URL-osoitteesta, tarkista osoite
    +    Lisää identiteettipalvelin asetuksissasi, jotta voit tehdä tämän toiminnon.
    +    Taustasynkronointitila (kokeellinen)
    +    Ei taustasynkronointia
    +    Et saa ilmoituksia saapuvista viesteistä, kun sovellus on taustalla.
    +    Asetusten päivittäminen epäonnistui.
    +
    +
    +    Haluttu synkronointiväli
    +    %s
    +\nSynkronointia saatetaan lykätä resursseista (akusta) tai laitteen tilasta (virransäästö) riippuen.
    +    Julkinen nimi (näkyy ihmisille, joihin olet yhteydessä)
    +    Laitteen julkinen nimi näkyy ihmisille, joihin olet yhteydessä
    +    Jatkaaksesi sinun täytyy hyväksyä palvelun käyttöehdot.
    +
    +    Et käytä identiteettipalvelinta
    +    Identiteettipalvelinta ei ole määritetty, salasanan palautus vaaditaan.
    +
    +    Näyttää, että yrität yhdistää toiseen kotipalvelimeen. Haluatko kirjautua ulos\?
    +
    +    URL-osoite:
    +    Ota käyttöön pyyhkäisemällä vastaaminen aikajanalla
    +
    +    Käyttöehdot
    +    Lue ehdot
    +    Käytä botteja, siltoja, sovelmia ja tarrapaketteja
    +
    +    Lue osoitteessa
    +
    +
    +    Identiteettipalvelin
    +    Katkaise yhteys identiteettipalvelimeen
    +    Määritä identiteettipalvelin
    +    Vaihda identiteettipalvelinta
    +    Käytät palvelinta %1$s löytääksesi tuntemiasi ihmisiä ja jotta he löytäisivät sinut.
    +    Et käytä tällä hetkellä identiteettipalvelinta. Jotta voit löytää tuntemiasi ihmisiä ja jotta he löytävät sinut, määritä identiteettipalvelin alla.
    +    Odottaa
    +
    +    Syötä uusi identiteettipalvelin
    +    Identiteettipalvelimeen ei saatu yhteyttä
    +    Syötä identiteettipalvelimen URL-osoite
    +    Identiteettipalvelimella ei ole käyttöehtoja
    +    Valitsemallasi identiteettipalvelimella ei ole käyttöehtoja. Jatka vain, jos luotat palvelun omistajaan
    +    Jaat sähköpostiosoitteita tai puhelinnumeroita identiteettipalvelimella %s. Sinun täytyy yhdistää uudelleen palvelimeen %s, jotta voit lopettaa niiden jakamisen.
    +    Hyväksy identiteettipalvelimen (%s) käyttöehdot salliaksesi, että sinut voi löytää sähköpostiosoitteen tai puhelinnumeron perusteella.
     
    diff --git a/vector/src/main/res/values-fr/strings.xml b/vector/src/main/res/values-fr/strings.xml
    index 5e7dbe8e6f..3ca4f03934 100644
    --- a/vector/src/main/res/values-fr/strings.xml
    +++ b/vector/src/main/res/values-fr/strings.xml
    @@ -293,7 +293,7 @@
         Synchronisation en arrière-plan
         Activer la synchronisation en arrière-plan
         Délai d’attente de la requête de synchronisation
    -    Délai entre chaque requête
    +    Délai entre chaque synchronisation
         seconde
         secondes
     
    @@ -317,10 +317,10 @@
         Épingler les salons avec des notifications manquées
         Épingler les salons avec des messages non lus
         Appareils
    -    Détails de l’appareil
    +    Informations de l’appareil
         Identifiant
    -    Nom
    -    Nom de l’appareil
    +    Nom public
    +    Mettre à jour le nom public
         Vu la dernière fois
         Authentification
         Mot de passe :
    @@ -391,9 +391,9 @@
         Erreur de déchiffrement
     
         Informations sur l’appareil de l’expéditeur
    -    Nom de l’appareil
    -    Nom
    -    Identifiant de l’appareil
    +    Nom public
    +    Nom public
    +    Identifiant
         Clé de l’appareil
         Vérification
         Empreinte Ed25519
    @@ -439,14 +439,6 @@
     
         Le rapport d’erreur a bien été envoyé
         L’envoi du rapport d’erreur a échoué (%s)
    -    Ajoutez une adresse e-mail à votre compte pour permettre aux autres utilisateurs de vous retrouver et pour pouvoir réinitialiser votre mot de passe.
    -    Ajoutez un numéro de téléphone à votre compte pour permettre aux autres utilisateurs de vous retrouver.
    -    Ajoutez une adresse e-mail et / ou un numéro de téléphone à votre compte pour permettre aux autres utilisateurs de vous retrouver.
    -
    -L’adresse e-mail vous permettra également de réinitialiser votre mot de passe.
    -    Ajoutez une adresse e-mail et un numéro de téléphone à votre compte pour permettre aux autres utilisateurs de vous retrouver.
    -
    -L’adresse e-mail vous permettra également de réinitialiser votre mot de passe.
         Les noms d’utilisateurs ne peuvent contenir que des lettres, des nombres, des points, des traits d’union et des tirets bas
         Cela ne ressemble pas à une adresse e-mail valide
         Cela ne ressemble pas à un numéro de téléphone valide
    @@ -1501,7 +1493,7 @@ Si vous n’avez pas configuré de nouvelle méthode de récupération, un attaq
     
         Évènement supprimé par l’utilisateur
         Évènement modéré par l’administrateur du salon
    -    Dernière édition par %s le %s
    +    Dernière édition par %1$s le %2$s
     
     
         Évènement malformé, affichage impossible
    @@ -1578,7 +1570,7 @@ Si vous n’avez pas configuré de nouvelle méthode de récupération, un attaq
     \n
     \nRiotX prend en charge : • Se connecter à un compte existant • Créer de salons et rejoindre des salons publics • Accepter et refuser des invitations • Lister les salons des utilisateurs • Voir les informations des salons • Envoyer des messages texte • Envoyer des pièces jointes • Lire et écrire des messages dans les salons chiffrés • Chiffrement : sauvegarde des clés de chiffrement, vérification avancée des appareils, demande et réponse de partage de clé • Notifications • Thèmes clair, sombre et noir
     \n
    -\nToutes les fonctionnalités de Riot ne sont pas encore implémentées dans RiotX. Principales fonctionnalités manquantes (et qui arrivent bientôt !) : • Création de compte • Réglages des salons (lister les membres du salon etc.) • Création de salons de discussion directe • Appels • Widgets • …
    +\nToutes les fonctionnalités de Riot ne sont pas encore implémentées dans RiotX. Principales fonctionnalités manquantes (et qui arrivent bientôt !) : • Création de compte • Réglages des salons (lister les membres du salon etc.) • Appels • Widgets • …
     
         Messages directs
     
    @@ -1625,4 +1617,88 @@ Si vous n’avez pas configuré de nouvelle méthode de récupération, un attaq
     
         Voir l’historique des éditions
     
    +    Examiner
    +    Refuser
    +
    +    Pour continuer, vous devez accepter les conditions de ce service.
    +
    +    Les versions précédentes de Riot avaient un problème de sécurité qui pouvait permettre à votre serveur d’identité (%1$s) d’accéder à votre compte. Si vous faites confiance à %2$s, vous pouvez ignorer cela ; sinon déconnectez-vous et reconnectez-vous.
    +\n
    +\nVous trouverez plus de détails ici :
    +\nhttps://medium.com/@RiotChat/36b4792ea0d6
    +
    +    Conditions de service
    +    Examiner les conditions
    +    Être découvrable par les autres
    +    Utiliser des robots, passerelles, widgets et packs de stickers
    +
    +    Lu à
    +
    +    Aucun
    +    Révoquer
    +    Déconnecter
    +    Aucun serveur d’identité configuré.
    +
    +    L’appel a échoué en raison d’un serveur mal configuré
    +    Demandez à l’administrateur de votre serveur d’accueil (%1$s) de configurer un serveur TURN afin que les appels fonctionnent de manière fiable.
    +\n
    +\nSinon, vous pouvez essayer d’utiliser le serveur public à %2$s, mais ça ne sera pas fiable et ça partagera votre adresse IP avec ce serveur. Vous pouvez aussi régler cela dans les paramètres.
    +    Essayez d’utiliser %s
    +    Ne plus me demander
    +
    +    Renseignez une adresse e-mail pour la récupération de compte et pour être éventuellement découvrable par les personnes qui vous connaissent.
    +    Renseignez un numéro de téléphone pour être éventuellement découvrable par les personnes qui vous connaissent.
    +    Renseignez une adresse e-mail pour la récupération de compte. Utilisez ensuite un e-mail ou un téléphone pour être éventuellement découvrable par les personnes qui vous connaissent.
    +    Renseignez une adresse e-mail pour la récupération de compte. Utilisez ensuite un e-mail ou un téléphone pour être éventuellement découvrable par les personnes qui vous connaissent.
    +    Impossible de joindre le serveur d’accueil à cette URL, veuillez la vérifier
    +    Autoriser le serveur d’assistance d’appel de secours
    +    Utilisera %s comme assistant quand votre serveur d’accueil n’en offre pas (votre adresse IP sera partagée lors d’un appel)
    +    Ajoutez un serveur d’identité dans vos paramètres pour réaliser cette action.
    +    Mode de synchronisation en arrière-plan (expérimental)
    +    Optimisé pour la batterie
    +    Riot se synchronisera en arrière-plan de façon à préserver les ressources limitées de l’appareil (batterie).
    +\nSelon l’état des ressources de votre appareil, la synchronisation peut être retardée par le système d’exploitation.
    +    Optimisé pour le temps réel
    +    Riot se synchronisera en arrière-plan de façon périodique à un moment précis (configurable).
    +\nCela aura un impact sur l’utilisation de la radio et de la batterie, une notification permanente sera affichée indiquant que Riot est à l’écoute des évènements.
    +    Aucune synchronisation en arrière-plan
    +    Vous ne serez pas notifié(e) des messages entrants quand l’application est en arrière-plan.
    +    Échec de la mise à jour des paramètres.
    +
    +
    +    Intervalle de synchronisation préféré
    +    %s
    +\nLa synchronisation peut être retardée selon les ressources (batterie) ou l’état (veille) de l’appareil.
    +    Découverte
    +    Gérer vos paramètres de découverte.
    +    Nom public (visible par les personnes avec qui vous communiquez)
    +    Le nom public d’un appareil est visible par les personnes avec qui vous communiquez
    +    Vous n’utilisez aucun serveur d’identité
    +    Aucun serveur d’identité n’est configuré, il est nécessaire pour réinitialiser votre mot de passe.
    +
    +    Il semblerait que vous essayez de vous connecter à un autre serveur d’accueil. Voulez-vous vous déconnecter \?
    +
    +    Serveur d’identité
    +    Se déconnecter du serveur d’identité
    +    Configurer le serveur d’identité
    +    Modifier le serveur d’identité
    +    Vous utilisez actuellement %1$s pour découvrir et être découvrable par les contacts existants que vous connaissez.
    +    Vous n’utilisez actuellement aucun serveur d’identité. Pour découvrir et être découvrable par les contacts existants que vous connaissez, configurez-en un ci-dessous.
    +    Adresses e-mail découvrables
    +    Les options de découverte apparaîtront quand vous aurez ajouté(e) un e-mail.
    +    Les options de découverte apparaîtront quand vous aurez ajouté un numéro de téléphone.
    +    La déconnexion du serveur d’identité signifie que vous ne pourrez plus être découvrable par les autres utilisateurs et que vous ne pourrez plus inviter d’autres personnes par e-mail ou par téléphone.
    +    Numéros de téléphone découvrables
    +    Nous vous avons envoyé un e-mail de confirmation à %s, vérifiez vos e-mails et cliquez sur le lien de confirmation
    +    En attente
    +
    +    Renseignez un nouveau serveur d’identité
    +    Impossible de se connecter au serveur d’identité
    +    Veuillez renseigner l’URL du serveur d’identité
    +    Le serveur d’identité n’a pas de conditions de service
    +    Le serveur d’identité qui vous avez choisi n’a pas de conditions de service. Continuez uniquement si vous faites confiance au propriétaire de ce service
    +    Un SMS a été envoyé à %s. Saisissez le code de vérification qu’il contient.
    +
    +    Vous partagez actuellement des adresse e-mails et des numéros de téléphone sur le serveur d’identité %s. Vous devrez vous reconnecter à %s pour arrêter de les partager.
    +    Acceptez les conditions de service du serveur d’identité (%s) pour vous permettre d’être découvrable avec une adresse e-mail ou un numéro de téléphone.
     
    diff --git a/vector/src/main/res/values-gl/strings.xml b/vector/src/main/res/values-gl/strings.xml
    index cafceca47b..e708f27c70 100644
    --- a/vector/src/main/res/values-gl/strings.xml
    +++ b/vector/src/main/res/values-gl/strings.xml
    @@ -168,14 +168,6 @@ Quere engadir algún?
         Contrasinal
         Novo contrasinal
         Nome de usuario
    -    Engadíndolle unha dirección de correo á súa conta permite que outros usuarios o atopen e que poida restaurar o seu contrasinal.
    -    Engadíndolle o teléfono á súa conta permite que outros usuarios o atopen.
    -    Engade unha dirección de correo e/ou número de teléfono á súa conta e permite que outros usuarios o atopen.
    -
    -A dirección de correo permitiralle restaurar o contrasinal.
    -    Engade unha dirección de correo e un número de teléfono á súa conta para permitirlle a outros usuarios que o atopen.
    -
    -A dirección de correo permitiralle restaurar o contrasinal.
         Enderezo de correo
         Enderezo de correo (opcional)
         Número de teléfono
    diff --git a/vector/src/main/res/values-hu/strings.xml b/vector/src/main/res/values-hu/strings.xml
    index 3d0174b9d8..255887c340 100644
    --- a/vector/src/main/res/values-hu/strings.xml
    +++ b/vector/src/main/res/values-hu/strings.xml
    @@ -129,14 +129,6 @@
         Jelszó
         Új jelszó
         Felhasználónév
    -    Adj meg egy e-mail címet a fiókodhoz, hogy mások megtalálhassanak és hogy visszaállíthasd a jelszavadat.
    -    Adj meg telefonszámot a fiókodhoz, hogy mások megtalálhassanak.
    -    Adj meg e-mail címet és/vagy telefonszámot a fiókodhoz, hogy mások megtalálhassanak.
    -
    - Az e-mail címmel a jelszavadat is helyreállíthatod.
    -    Adj meg e-mail címet és telefonszámot a fiókodhoz, hogy mások megtalálhassanak.
    -
    - Az e-mail címmel a jelszavadat is helyreállíthatod.
         E-mail cím
         E-mail cím (nem kötelező)
         Telefonszám
    @@ -421,7 +413,7 @@ Vedd figyelembe, hogy az alkalmazás újraindul  ami sok időt vehet igénybe."<
         Szinkronizálás a háttérben
         Háttérben történő szinkronizáció engedélyezése
         Szinkronizáció kérelem időtúllépés
    -    Késleltetés kérések között
    +    Késleltetés a szinkronizációk között
         másodperc
         másodperc
     
    @@ -448,8 +440,8 @@ Vedd figyelembe, hogy az alkalmazás újraindul  ami sok időt vehet igénybe."<
         Eszközök
         Eszköz adatai
         Azonosító
    -    Név
    -    Készüléknév
    +    Nyilvános Név
    +    Nyilvános Név frissítése
         Legutóbb láttuk
         %1$s @ %2$s
         Ez a művelethez újra azonosításra van szükség.
    @@ -565,9 +557,9 @@ Vedd figyelembe, hogy az alkalmazás újraindul  ami sok időt vehet igénybe."<
         Visszafejtés hiba
     
         A küldő eszköz információi
    -    Eszköz információ
    -    Név
    -    Eszköz azonosító
    +    Nyilvános név
    +    Nyilvános név
    +    Azon.
         Eszköz kulcs
         Hitelesítés
         Ed25519 ujjlenyomat
    @@ -1498,7 +1490,7 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró
     
         Az eseményt a felhasználó törölte
         Az eseményt a szoba adminisztrátora moderálta
    -    Utoljára szerkesztette: %s ekkor: %s
    +    Utoljára szerkesztette: %1$s ekkor: %2$s
     
     
         Hibás esemény, nem lehet megjeleníteni
    @@ -1577,7 +1569,7 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró
     \n
     \nRiotX ezeket támogatja: • Bejelentkezés létező fiókba • Szoba készítés és nyilvános szobába való belépés • Meghívók fogadása és elutasítás • Felhasználók szobáinak listázása • Szoba adatainak megtekintése • Szöveges üzenet küldése • Csatolmány küldése • Titkosított szobákban üzenetek olvasása és írása • Titkosítás: Végponttól végpontig titkosító kulcsok mentése, fejlett eszköz ellenőrzés, kulcs megosztás kérése és válasz • „Push” értesítések • Világos, sötét és fekete téma
     \n
    -\nNem minden Riot funkció támogatott a RiotX-ben jelenleg. A fő hiányzó (és hamarosan elérhető!) funkciók: • Felhasználói fiók létrehozása • Szoba beállítások (szoba tagság mutatása, stb…) • Közvetlen beszélgetések indítása • Hívások • Kisalkalmazások • …
    +\nNem minden Riot funkció támogatott a RiotX-ben jelenleg. A fő hiányzó (és hamarosan elérhető!) funkciók: • Felhasználói fiók létrehozása • Szoba beállítások (szoba tagság mutatása, stb…) • Hívások • Kisalkalmazások • …
     
         Közvetlen beszélgetés
     
    @@ -1624,4 +1616,88 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró
     
         Szerkesztési napló megtekintése
     
    +    Átnéz
    +    Elutasít
    +
    +    A továbblépéshez el kell fogadnod a Felhasználási feltételeket.
    +
    +    A Riot előző verziója egy biztonsági hibát tartalmazott ami az Azonosítási Szervernek (%1$s) hozzáférést adott a fiókodhoz. Ha megbízol a %2$s szerverben, akkor ezt figyelmen kívül hagyhatod különben jelentkezz ki és újra vissza.
    +\n
    +\nTovábbi információ itt:
    +\nhttps://medium.com/@RiotChat/36b4792ea0d6
    +
    +    Felhasználási feltételek
    +    Feltételek átnézése
    +    Mások által is megtalálható legyél
    +    Használj botokat, hidakat (bridges), kisalkalmazásokat és matrica csomagokat
    +
    +    Olvasd itt
    +
    +    Nincs
    +    Visszavon
    +    Lecsatlakozik
    +    Azonosítási szerver nincs beállítva.
    +
    +    A hívás sikertelen a hibásan beállított szerver miatt
    +    Kérd meg a matrix szervered (%1$s) adminisztrátorát, hogy állítson be egy TURN szervert, hogy a hívások megbízhatóan működjenek.
    +\n
    +\nAlternatív megoldásként használhatod a nyilvános szervert itt: %2$s, de ez lehet, hogy nem lesz annyira megbízható és az IP címedet megosztja a szerverrel. Ezt a Beállításokban tudod megadni.
    +    Próbáld ki ezt: %s
    +    Ne kérdezz újra
    +
    +    Fiók visszaállításhoz e-mail beállítása, később esetleg megtalálhatnak akik ismernek téged.
    +    Telefonszám beállítása, és később esetleg megtalálhatnak akik ismernek téged.
    +    Fiók visszaállításhoz e-mail beállítás. Később esetleg e-mail vagy telefonszám alapján megtalálhatnak akik ismernek téged.
    +    Fiók visszaállításhoz e-mail beállítás. Később esetleg e-mail vagy telefonszám alapján megtalálhatnak akik ismernek téged.
    +    A matrix szerver elérhetetlen ezen a címen, kérlek ellenőrizd
    +    Tartalék hívás segítő szerver engedélyezése
    +    Segítségként %s lesz használatban ha a matrix szervered nem ajánl fel egyet (a hívás ideje alatt az IP címed megosztásra kerül)
    +    A beállításokban adj hozzá egy azonosítási szervert ehhez a művelethez.
    +    Háttér Szinkronizálási Mód (Kísérleti)
    +    Optimalizált akkumulátor használat
    +    Riot a háttérben úgy szinkronizál, hogy a leginkább kímélje az eszköz korlátozott erőforrásait (akkumulátor).
    +\nAz eszköz erőforrásainak állapotától függően a szinkronizációt az operációs rendszer elhalaszthatja.
    +    Optimalizálás valós idejű használatra
    +    Riot a háttérben rendszeresen, pontosan a megadott időközönként, szinkronizálni fog (beállítható).
    +\nEz befolyásolja a rádió és az akkumulátor használatot, és folyamatosan egy értesítés fog megjelenni arról, hogy a riot 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.
    +    A beállítások frissítése nem sikerült.
    +
    +
    +    Előnyben részesített szinkronizációs időköz
    +    %s
    +\nA szinkronizáció az eszköz erőforrásainak (akkumulátor) és állapotának (alvó mód) függvényében késleltetve lehet.
    +    Felderítés
    +    Felderítési beállítások megváltoztatása.
    +    Nyilvános név (a beszélgetőpartnerek által látható)
    +    Az eszköz nyilvános neve látható azoknál akikkel beszélgetsz
    +    Nem használsz Azonosítási Szervert
    +    Nincs beállítva azonosítási szerver amire a jelszó visszaállításához szükség van.
    +
    +    Úgy látszik másik matrix szerverhez szeretnél csatlakozni. Kijelentkezel\?
    +
    +    Azonosítási szerver
    +    Azonosítási szerverről lecsatlakozás
    +    Azonosítási szerver beállítása
    +    Azonosítási szerver megváltoztatása
    +    A felderítésekhez és hogy megtaláljanak az ismerősök ezt a szervert használod jelenleg: %1$s.
    +    Jelenleg nem használsz azonosítási szervert. A felderítéshez és, hogy az ismerősök megtalálhassanak állíts be egyet alább.
    +    Felderíthető e-mail címek
    +    Amint hozzáadtál egy e-mail címet megjelenik a felderítési beállítási lehetőség.
    +    Amint hozzáadtál egy telefonszámot megjelenik a felderítési beállítási lehetőség.
    +    Az azonosítási szerverről való lecsatlakozással nem leszel mások által megtalálható és másokat sem tudsz meghívni e-mail címmel vagy telefonszámmal.
    +    Felderíthető telefonszámok
    +    Megerősítő levelet küldtünk ide: %s, ellenőrizd az e-mailedet és kattints a megerősítő hivatkozásra
    +    Várakozás
    +
    +    Add meg az új azonosítási szervert
    +    Az azonosítási szerverhez nem lehet csatlakozni
    +    Kérlek add meg az azonosítási szerver url-jét
    +    Az azonosítási szervernek nincsen felhasználási feltétele
    +    Az általad választott azonosítási szervernek nincs felhasználási feltétele. Csak akkor lépj tovább ha megbízol a szolgáltatás tulajdonosában
    +    Szöveges üzenetet küldtünk ide: %s. Kérlek add meg az ellenőrző kódot amit az üzenet tartalmaz.
    +
    +    Az azonosítási szerverrel (%s) megosztod az e-mail címeket és telefonszámokat. Újra kell csatlakoznod ehhez: %s, hogy megállítsd a megosztást.
    +    Egyetértek az azonosítási szerver (%s) Felhasználási feltételeivel ahhoz, hogy megtalálható legyek e-mail címmel vagy telefonszámmal.
     
    diff --git a/vector/src/main/res/values-id/strings.xml b/vector/src/main/res/values-id/strings.xml
    index 849e5ecb58..56f6d4deb2 100644
    --- a/vector/src/main/res/values-id/strings.xml
    +++ b/vector/src/main/res/values-id/strings.xml
    @@ -194,14 +194,6 @@
     
         Kirim ke
         Nama Pengguna
    -    Tambahkan alamat email ke akun agar pengguna lain dapat menemukan Anda, dan kata sandi dapat disetel ulang.
    -    Tambahkan nomor telpon ke akun agar pengguna lain dapat menemukan Anda.
    -    Tambahkan alamat email dan/atau nomor telpon ke akun agar pengguna lain dapat menemukan Anda.
    -
    -Alamat email juga memungkinkan Anda menyetel ulang kata sandi.
    -    Tambahkan alamat email dan nomor telpon ke akun agar pengguna lain dapat menemukan Anda.
    -
    -Alamat email juga memungkinkan Anda menyetel ulang kata sandi Anda.
         Nama pengguna dan/atau kata sandi salah
         Nama pengguna hanya boleh memuat huruf, angka, titik, tanda hubung ( - ), dan garis bawah ( _ )
         Password belum dimasukkan
    diff --git a/vector/src/main/res/values-in/strings.xml b/vector/src/main/res/values-in/strings.xml
    index 849e5ecb58..56f6d4deb2 100644
    --- a/vector/src/main/res/values-in/strings.xml
    +++ b/vector/src/main/res/values-in/strings.xml
    @@ -194,14 +194,6 @@
     
         Kirim ke
         Nama Pengguna
    -    Tambahkan alamat email ke akun agar pengguna lain dapat menemukan Anda, dan kata sandi dapat disetel ulang.
    -    Tambahkan nomor telpon ke akun agar pengguna lain dapat menemukan Anda.
    -    Tambahkan alamat email dan/atau nomor telpon ke akun agar pengguna lain dapat menemukan Anda.
    -
    -Alamat email juga memungkinkan Anda menyetel ulang kata sandi.
    -    Tambahkan alamat email dan nomor telpon ke akun agar pengguna lain dapat menemukan Anda.
    -
    -Alamat email juga memungkinkan Anda menyetel ulang kata sandi Anda.
         Nama pengguna dan/atau kata sandi salah
         Nama pengguna hanya boleh memuat huruf, angka, titik, tanda hubung ( - ), dan garis bawah ( _ )
         Password belum dimasukkan
    diff --git a/vector/src/main/res/values-is/strings.xml b/vector/src/main/res/values-is/strings.xml
    index 9d17e8fa1b..b4a9f59a46 100644
    --- a/vector/src/main/res/values-is/strings.xml
    +++ b/vector/src/main/res/values-is/strings.xml
    @@ -793,14 +793,6 @@ Leyfa Riot nota tengiliðina ?
         Það er eins og þú sért að hrista símann ákveðið. Myndirðu vilja senda villuskýrslu?
         Forritið hrundi síðast. Myndirðu vilja senda inn villuskýrslu?
         Senda límmerki
    -    Bættu tölvupóstfangi við notandaaðganginn þinn til að gera öðrum notendum kleift að finna þig og einnig til að endurstilla lykilorðið þitt.
    -    Bættu símanúmeri við notandaaðganginn þinn til að gera öðrum notendum kleift að finna þig.
    -    Bættu tölvupóstfangi og/eða símanúmeri við notandaaðganginn þinn til að gera öðrum notendum kleift að finna þig.
    -
    -Tölvupóstfang nýtist einnig til að endurstilla lykilorðið þitt.
    -    Bættu tölvupóstfangi og símanúmeri við notandaaðganginn þinn til að gera öðrum notendum kleift að finna þig.
    -
    -Tölvupóstfang nýtist einnig til að endurstilla lykilorðið þitt.
         Tölvupósttengill sem ekki er enn búið að smella á
     
         Rangt formað auðkenni. Ætti að vera tölvupóstfang eða Matrix-auðkenni á borð við\'@sérheiti:lén\'
    diff --git a/vector/src/main/res/values-it/strings.xml b/vector/src/main/res/values-it/strings.xml
    index 3719ba0f3d..4e41ad0a6c 100644
    --- a/vector/src/main/res/values-it/strings.xml
    +++ b/vector/src/main/res/values-it/strings.xml
    @@ -144,14 +144,6 @@
         Password
         Nuova password
         Nome utente
    -    Aggiungi un indirizzo email al tuo account per farti trovare e per poter resettare la password.
    -    Aggiungi un numero di telefono al tuo account per farti trovare.
    -    Aggiungi un indirizzo email e/o un numero di telefono al tuo account per farti trovare.
    -
    -L\'indirizzo email ti permetterà anche di resettare la password.
    -    Aggiungi un indirizzo email ed un numero di telefono al tuo account per farti trovare.
    -
    -L\'indirizzo email ti permetterà anche di resettare la password.
         Indirizzo email
         Indirizzo email (facoltativo)
         Numero di telefono
    @@ -294,7 +286,7 @@ Per favore consenti l\'accesso nella prossima finestra per potere effettuare la
         Rifiuta
     
         
    -    Primo messaggio non letto.
    +    Vai al primo messaggio non letto.
     
         
         Sei stato invitato ad unirti in questa stanza da %s
    @@ -465,7 +457,7 @@ Tieni presente che questa azione riavvierà l\'app e potrebbe richiedere del tem
         Sincronizzazione in sottofondo
         Abilita sync in sottofondo
         Attesa per una richiesta di sync
    -    Attesa prima di una nuova richiesta
    +    Attesa per ogni sincronizzazione
         secondo
         secondi
     
    @@ -496,10 +488,10 @@ Tieni presente che questa azione riavvierà l\'app e potrebbe richiedere del tem
         Mostra data e ora di tutti i messaggi
         Modalità risparmio banda
     
    -    Dettagli dispositivo
    +    Informazioni dispositivo
         ID
    -    Nome
    -    Nome dispositivo
    +    Nome pubblico
    +    Aggiorna nome pubblico
         Ultima visualizzazione
         %1$s @ %2$s
         Questa operazione richiede un\'autenticazione aggiuntiva.\nPer proseguire, inserisci la tua password.
    @@ -640,9 +632,9 @@ Tieni presente che questa azione riavvierà l\'app e potrebbe richiedere del tem
         Errore decriptazione
     
         Informazioni dispositivo mittente
    -    Nome dispositivo
    -    Nome
    -    ID dispositivo
    +    Nome pubblico
    +    Nome pubblico
    +    ID
         Chiave dispositivo
         Verifica
         Impronta digitale Ed25519
    @@ -1542,7 +1534,7 @@ Per essere certo di non perdere nulla, mantieni gli aggiornamenti attivi."Evento eliminato dall\'utente
         Evento moderato da un admin della stanza
    -    Ultima modifica di %s il %s
    +    Ultima modifica di %1$s il %2$s
     
     
         Evento malformato, impossibile visualizzarlo
    @@ -1621,7 +1613,7 @@ Per essere certo di non perdere nulla, mantieni gli aggiornamenti attivi."
    +\nNon tutte le funzioni di Riot sono già implementate in RiotX. Principali funzioni mancanti (prossimamente!): • Creazione account • Impostazioni stanza (elenca membri stanza, ecc.) • Chiamate • Widget • …
     
         Non hai nulla di nuovo da vedere!
         Messaggi diretti
    @@ -1669,4 +1661,88 @@ Per essere certo di non perdere nulla, mantieni gli aggiornamenti attivi."Visualizza Modifica cronologia
     
    +    Leggi
    +    Rifiuta
    +
    +    Per continuare, devi accettare i termini di servizio.
    +
    +    Le precedenti versioni di Riot avevano un errore di sicurezza che poteva dare accesso al tuo account al tuo Server di Identità (%1$s). Se ti fidi di %2$s, puoi ignorare; altrimenti disconnettiti e riaccedi di nuovo.
    +\n
    +\nLeggi maggiori dettagli qui:
    +\nhttps://medium.com/@RiotChat/36b4792ea0d6
    +
    +    Termini di servizio
    +    Leggi i termini
    +    Diventa trovabile dagli altri
    +    Usa bot, bridge, widget e pacchetti di adesivi
    +
    +    Leggi su
    +
    +    Nessuno
    +    Revoca
    +    Disconnetti
    +    Nessun server di identità configurato.
    +
    +    Chiamata fallita per server malconfigurato
    +    Chiedi all\'amministratore del tuo homeserver (%1$s) di configurare un server TURN affinché le chiamate funzionino in modo affidabile.
    +\n
    +\nAltrimenti, puoi provare ad usare il server pubblico su %2$s, ma non è ugualmente affidabile e vedrà il tuo indirizzo IP. Puoi fare la tua scelta anche nelle Impostazioni.
    +    Prova ad usare %s
    +    Non chiedermelo più
    +
    +    Imposta un\'email per il recupero dell\'account, o più tardi per essere trovabile dalle persone che ti conoscono.
    +    Imposta un telefono, o più tardi per essere trovabile dalle persone che ti conoscono.
    +    Imposta un\'email per il recupero dell\'account. Più tardi usa email o telefono per essere trovabile dalle persone che ti conoscono.
    +    Imposta un\'email per il recupero dell\'account. Più tardi usa email o telefono per essere trovabile dalle persone che ti conoscono.
    +    Impossibile raggiungere un homeserver con questo URL, controllalo
    +    Permetti server di assistenza chiamata di riserva
    +    Userà %s come assistente quando l\'homeserver non ne offre uno (il tuo indirizzo IP sarà visibile durante le chiamate)
    +    Aggiungi un server di identità nelle impostazioni per poterlo fare.
    +    Modalità sync in background (Sperimentale)
    +    Ottimizzato per la batteria
    +    Riot sincronizzerà in background in modo da preservare le risorse limitate del dispositivo (batteria).
    +\nA seconda dello stato delle risorse del tuo dispositivo, la sincronizzazione può essere ritardata dal sistema operativo.
    +    Ottimizzato per il tempo reale
    +    Riot sincronizzerà in background periodicamente in momenti precisi (configurabile).
    +\nCiò avrà impatto sull\'uso di dati e batteria, ci sarà una notifica permanente per comunicare che Riot è in attesa di eventi.
    +    Nessun sync in background
    +    Non ti verranno notificati i messaggi in arrivo quando l\'app è in background.
    +    Aggiornamento impostazioni fallito.
    +
    +
    +    Intervallo di sync preferito
    +    %s
    +\nLa sincronizzazione può essere ritardata a seconda delle risorse (batteria) o dello stato del dispositivo (sospensione).
    +    Scoperte
    +    Gestisci le tue impostazioni di rilevamento.
    +    Nome pubblico (visibile alle persone con cui comunichi)
    +    Il nome pubblico di un dispositivo è visibile alle persone con cui comunichi
    +    Non stai usando alcun server di identità
    +    Nessun server di identità configurato, è necessario per ripristinare la tua password.
    +
    +    Pare che tu stia provando a connetterti ad un altro homeserver. Vuoi disconnetterti da qui\?
    +
    +    Server di identità
    +    Disconnetti server di identità
    +    Configura server di identità
    +    Cambia server di identità
    +    Attualmente stai usando %1$s per scoprire ed essere trovabile dai contatti esistenti che conosci.
    +    Attualmente non stai usando alcun server di identità. Per scoprire ed essere trovabile dai contatti esistenti che conosci, configurane uno qua sotto.
    +    Indirizzi email trovabili
    +    Le opzioni di scoperta appariranno dopo avere aggiunto un\'email.
    +    Le opzioni di scoperta appariranno dopo avere aggiunto un numero di telefono.
    +    Se ti disconnetti dal server di identità non potrai più essere trovato da altri utenti e non potrai invitarne altri per email o telefono.
    +    Numeri di telefono trovabili
    +    Ti abbiamo inviato un\'email di conferma a %s, controlla l\'email e clicca sul link di conferma
    +    In attesa
    +
    +    Inserisci un nuovo server di identità
    +    Impossibile connettersi al server di identità
    +    Inserisci l\'URL del server di identità
    +    Il server di identità non ha condizioni di servizio
    +    Il server di identità che hai scelto non ha alcuna condizione di servizio. Continua solo se ti fidi del proprietario del servizio
    +    È stato inviato un messaggio a %s. Inserisci il codice di verifica contenuto.
    +
    +    Attualmente stai condividendo indirizzi email o numeri di telefono sul server di identità %s. Dovrai riconnetterti a %s per fermarne la condivisione.
    +    Accetta le condizioni di servizio del server di identità (%s) per consentire di essere trovabile per email o numero di telefono.
     
    diff --git a/vector/src/main/res/values-ja/strings.xml b/vector/src/main/res/values-ja/strings.xml
    index ed6b05517b..8b83c85e39 100644
    --- a/vector/src/main/res/values-ja/strings.xml
    +++ b/vector/src/main/res/values-ja/strings.xml
    @@ -349,19 +349,11 @@
         パスワード
         新しいパスワード
         ユーザ名
    -    電子メールアドレスを登録すると, 誰かがあなたを検索をしたり, パスワード紛失時に初期化のメールを送ることができます.
    -    電話番号を登録すると, 誰かがあなたを電話番号で検索できるようになります.
         電子メールアドレス
         電子メールアドレス (任意で)
         パスワード再確認
         新しいパスワードを再確認
         パスワードが違います
    -    電子メールアドレスや電話番号を登録すると, それらを使用して誰かがあなたを検索できるようになります.
    -
    -電子メールアドレスは, パスワード紛失時に初期化の連絡をするためにも使用されます.
    -    電子メールアドレスや電話番号を登録すると, それらを使用して誰かがあなたを検索できるようになります.
    -
    -電子メールアドレスは, パスワード紛失時に初期化の連絡をするためにも使用されます.
         電子メールアドレスが違います
         電話番号が違います
         電子メールアドレスまたは電話番号が違います
    diff --git a/vector/src/main/res/values-ko/strings.xml b/vector/src/main/res/values-ko/strings.xml
    index 188dd6ce36..ad1716f70b 100644
    --- a/vector/src/main/res/values-ko/strings.xml
    +++ b/vector/src/main/res/values-ko/strings.xml
    @@ -12,7 +12,7 @@
         
         설정
         기록
    -    버그 보고서
    +    버그 신고
         스티커 보내기
     
         제 3자 라이선스
    @@ -24,18 +24,18 @@
         저장
         떠나기
         보내기
    -    복사하기
    +    복사
         다시 보내기
    -    삭제하기
    +    감추기
         인용
    -    받기
    +    다운로드
         공유
    -    퍼머링크
    -    출처 보기
    -    해독된 출처 보기
    -    지우기
    -    다시 이름짓기
    -    컨텐츠 신고
    +    고유 주소
    +    소스 보기
    +    해독된 소스 보기
    +    삭제
    +    다시 이름 짓기
    +    내용 신고하기
         음성
         영상
         전화를 걸 수 없습니다, 나중에 다시 시도해주세요
    @@ -46,10 +46,10 @@
         무시하고 보내기
         또는
         초대
    -    미접속
    +    오프라인
     
         나가기
    -    행동
    +    활동
         로그아웃
         음성 통화하기
         영상 통화하기
    @@ -80,7 +80,7 @@
         로그 보내기
         충돌 로그 보내기
         스크린샷 보내기
    -    버그 보고하기
    +    버그 신고하기
         버그에 대해 설명해주세요. 무엇을 했나요? 어떤 일이 일어나길 바라고 한 건가요? 실제로는 어떤 일이 일어났나요?
         가능하다면, 영어로 설명해주세요.
         여기에 문제를 설명해주세요
    @@ -115,8 +115,6 @@
         비밀번호
         새 비밀번호
         사용자 이름
    -    사용자가 당신을 찾고 재 설정 비밀번호를 보낼 수 있게 이메일 주소를 적어주세요.
    -    사용자가 당신을 찾을 수 있게 계정에 전화번호를 추가해주세요.
         Status.im 테마
     
         서비스 초기화 중
    @@ -127,7 +125,7 @@
         커뮤니티 정보
         키 백업
         키 백업하기
    -    인증 기기
    +    기기 확인
     
         키 백업이 끝나지 않았습니다, 기다려주세요…
         지금 로그아웃하면 암호화된 메세지가 사라집니다
    @@ -139,9 +137,9 @@
         완료
         정말 로그아웃하시겠어요\?
         읽음으로 표시
    -    Riot이 주소록에 접근할 수 없게 되어 있습니다
    +    Riot이 연락처에 접근할 수 없게 되어 있습니다
         
    -        %d 명의 사용자
    +        %d명의 사용자
         
     
         초대
    @@ -190,7 +188,7 @@
         
         가져오기
         현재 전화
    -    회의 전화 진행 중.
    +    회의 전화가 진행 중입니다.
     \n%1$s 또는 %2$s로 참가하세요
         이 방에서 회의를 시작하려면 초대할 권한이 필요합니다
         회의 전화는 암호화된 방에서 지원하지 않습니다
    @@ -208,34 +206,28 @@
         초대
         중요하지 않음
         대화
    -    로컬 주소록
    -    사용자 디렉토리
    -    Matrix 주소록만
    +    로컬 연락처
    +    사용자 목록
    +    Matrix 연락처만
         대화 안 함
    -    방 디렉토리
    +    방 목록
         방 없음
         이용할 수 있는 공공 방이 없음
         그룹 없음
     
         문제를 진단하기 위해, 이 클라이언트의 로그는 버그 보고서와 함께 전송됩니다. 이 버그 보고서에는 로그와 스크린샷이 포함되면 공개적으로 표시되지 않습니다. 위의 텍스트만 보내려면 다음을 선택 해제하세요:
         좌절감에 휴대 전화를 흔들고 있는 것 같네요. 버그 보고서 화면을 열어보겠습니까\?
    -    분노의 흔들기로 버그 보고하기
    +    분노의 흔들기로 버그 신고하기
     
         진행 (%s%%)
     
         여기로 보내기
         방 들어가기
         계속…
    -    죄송합니다, 이 행동을 완료하기 위한 외부 애플리케이션이 없습니다.
    +    죄송합니다, 이 작업을 완료하기 위한 외부 애플리케이션이 없습니다.
     
         통합 인증으로 로그인
         재 설정 이메일 보내기
    -    사용자가 당신을 찾을 수 있도록 계정에 이메일 주소 그리고/또는 전화번호를 추가하세요.
    -\n
    -\n이메일 주소로 비밀번호를 재 설정할 수도 있습니다.
    -    사용자가 당신을 찾을 수 있도록 계정에 이메일 주소 그리고 전화번호를 추가하세요.
    -\n
    -\n이메일 주소로 비밀번호를 재 설정할 수도 있습니다.
         사용자 이름은 문자, 숫자, 점, 하이픈 및 밑줄만 포함할 수 있습니다
         비밀번호가 누락되었습니다
         이 이메일 주소는 이미 정의되었습니다.
    @@ -243,17 +235,17 @@
         전화번호가 누락되었습니다
         이메일 주소나 전화번호가 누락되었습니다
         옳지 않은 토큰
    -    맞춤 서버 옵션을 사용하기 (고급)
    +    맞춤 서버 설정을 사용하기 (고급)
         등록을 계속하려면 이메일을 확인하세요
         API가 존재하기 전까지는 이메일과 전화번호로 한 번에 등록할 수 없습니다. 오직 전화번호만 고려됩니다.
     \n
     \n설정에서 프로필에 이메일을 추가할 수 있습니다.
         이 홈서버는 당신이 로봇인지 아닌 지를 확인하고 싶습니다
         ID 서버:
    -    제 이메일 주소를 인증했습니다
    +    제 이메일 주소를 확인했습니다
         계정에 연결된 이메일 주소를 입력해야 합니다.
         이메일 %s(으)로 전송했습니다. 이메일에 포함된 링크를 들어갔다면, 아래를 클릭해주세요.
    -    이메일 주소를 인증할 수 없습니다: 이메일에 있는 링크를 클릭했는 지 확인하세요
    +    이메일 주소를 확인할 수 없습니다: 이메일에 있는 링크를 클릭했는 지 확인하세요
         이 홈서버의 규칙을 숙지한 후 수락하세요:
     
         URL은 http[s]://로 시작해야 합니다
    @@ -289,7 +281,7 @@
         그룹 목록
     
         
    -        %d 구성원십 변경
    +        %d명의 구성원 변경 사항
         
     
         이렇게 보내기
    @@ -344,14 +336,14 @@
         음성 통화를 하려면 Riot은 마이크에 접근하는 권한이 필요합니다.
         "
     \n
    -\n전화를 하려면 다은 팝업에서 접근을 허용하세요."
    +\n전화를 하려면 다음 팝업에서 접근을 허용해주세요."
         영상 통화를 하려면 Riot은 카메라와 마이크에 접근하는 권한이 필요합니다.
     \n
     \n전화를 하려면 다음 팝업에서 접근을 허용해주세요.
    -    Riot은 당신의 주소록을 확인해서 이메일과 전화번호를 기반으로 다른 Matrix 사용자를 찾을 수 있습니다. 이런 이유로 주소록을 공유하는 것을 허용한다면, 다음 팝업에서 접근을 허용해주세요.
    -    "Riot은  당신의 주소록을 확인하여 이메일과 전화번호를 기반으로 다른 Matrix 사용자를 찾을 수 있습니다.
    -\n
    -\n이런 이유로 주소록을 공유하는 것을 허용하겠습니까\?"
    +    Riot은 당신의 연락처를 확인해서 이메일과 전화번호를 기반으로 다른 Matrix 사용자를 찾을 수 있습니다. 이런 이유로 연락처를 공유하는 것을 허용한다면, 다음 팝업에서 접근을 허용해주세요.
    +    "Riot은  당신의 연락처를 확인하여 이메일과 전화번호를 기반으로 다른 Matrix 사용자를 찾을 수 있습니다. 
    +\n 
    +\n이런 이유로 연락처를 공유하는 것을 허용하겠습니까\?"
     
         죄송합니다. 권한이 없어서, 작업이 수행되지 않았습니다
     
    @@ -361,7 +353,7 @@
         아니오
         계속
     
    -    삭제
    +    제거
         참가
         미리보기
         받지 않기
    @@ -407,7 +399,7 @@
         만들기
     
         온라인
    -    미접속
    +    오프라인
         휴식
         %1$s 상태
         %2$s 전 %1$s 상태였음
    @@ -424,16 +416,16 @@
         차단 해제
         추방
         일반 사용자로 재 설정
    -    중재자 만들기
    -    관리자 만들기
    +    중재자로 하기
    +    관리자로 하기
         이 사용자의 모든 메시지 숨기기
    -    이 사용자의 모든 메시지 보여주기
    +    이 사용자의 모든 메시지 보이기
         이 사용자의 모든 메시지를 보이겠습니까\?
     \n
     \n이 동작은 앱을 재시작하며 일정 시간이 걸릴 수 있습니다.
         사용자 ID, 이름 혹은 이메일
         언급
    -    기기 목록 보여주기
    +    기기 목록 보이기
         사용자를 자신과 동일한 권한 등급으로 승격시키는 것은 취소할 수 없습니다.
     \n확신합니까\?
     
    @@ -450,7 +442,7 @@
     
         ID로 초대
         로컬 연락처 (%d)
    -    사용자 디렉토리 (%s)
    +    사용자 목록 (%s)
         Matrix 사용자만
     
         ID로 사용자 초대
    @@ -460,13 +452,13 @@
         검색
         %s님이 입력 중…
         %1$s님과 %2$s님이 입력 중…
    -    %1$s님과 %2$s님 외 구성원들이 입력 중…
    -    암호화된 메시지 보내기…
    -    (암호화 안 된) 메시지 보내기…
    -    암호화된 답장 보내기…
    -    (암호화 안 된) 답장 보내기…
    +    %1$s님과 %2$s님 외 여러 명이 입력 중…
    +    암호화된 메시지를 보내세요…
    +    (암호화 안 된) 메시지를 보내세요…
    +    암호화된 답장을 보내세요…
    +    (암호화 안 된) 답장을 보내세요…
         서버와의 연결이 끊어졌습니다.
    -    메시지가 보내지지 않았습니다. %1$s, %2$s, 둘 중 어떤 것을 하겠습니까\?
    +    메시지가 보내지지 않았습니다. %1$s, %2$s, 둘 중 어느 것을 하겠습니까\?
         알 수 없는 기기가 있어 메시지가 보내지지 않았습니다. %1$s, %2$s, 둘 중 어느 것을 하겠습니까\?
         모두 다시 보내기
         모두 취소하기
    @@ -483,7 +475,7 @@
         로그아웃
         무시하기
         핑거프린트 (%s):
    -    원격 서버의 신원을 인증할 수 없습니다.
    +    원격 서버의 ID를 확인할 수 없습니다.
         이는 누군가가 당신의 트래픽을 악의적으로 가로채고 있거나, 휴대 전화가 원격 서버에서 제공한 인증서를 신뢰하지 않는 것입니다.
         서버 관리자가 이것이 예상된다고 말한다면, 아래 핑거프린트가 해당 핑거프린트와 일치하는 지 확인하세요.
         인증서가 휴대 전화가 신뢰하는 인증서에서 변경되었습니다. 이것은 매우 비정상적입니다. 새 인증서에 수락하지 않는 것을 권합니다.
    @@ -501,7 +493,7 @@
         초대받음
         참가함
     
    -    이 콘텐츠를 보고하는 이유
    +    이 내용을 신고하는 이유
         이 사용자의 모든 메시지를 숨기겠습니까\?
     \n
     \n이 동작은 앱을 다시 시작하며 일정 시간이 걸릴 수 있습니다.
    @@ -517,7 +509,7 @@
         파일
     
         참가
    -    디렉토리
    +    목록
         즐겨찾기
         
         중요하지 않음
    @@ -528,14 +520,14 @@
         방 참가하기
         방 ID나 방 별칭을 입력
     
    -    디렉토리 찾기
    +    목록 찾기
         
             %d개의 방
         
         
             %2$s 검색 결과로 %1$s개의 방을 찾음
         
    -    디렉토리 검색 중…
    +    목록 검색 중…
     
         모든 메시지 (소리)
         모든 메시지
    @@ -553,7 +545,7 @@
         버전
         버전 %s
         이용 약관
    -    제 3자 공지
    +    제 3자 고지
         저작권
         개인 정보 정책
     
    @@ -576,9 +568,9 @@
         문제 해결 진단
         테스트 실행
         실행 중… (%2$d개 중 %1$d개 째)
    -    기본 진단은 괜찮습니다. 여전히 알림을 받지 못하고 있다면, 버그를 보고해서 우리가 조사할 수 있도록 도와주세요.
    +    기본 진단은 괜찮습니다. 여전히 알림을 받지 못하고 있다면, 버그를 신고해서 우리가 조사할 수 있도록 도와주세요.
         1개 이상의 테스트가 실패했습니다, 제안된 수정을 시도하세요.
    -    1개 이상의 테스트가 실패했습니다, 버그를 보고해서 우리가 조사할 수 있도록 도와주세요.
    +    1개 이상의 테스트가 실패했습니다, 버그를 신고해서 우리가 조사할 수 있도록 도와주세요.
     
         시스템 설정.
         알림이 시스템 설정에서 켜집니다.
    @@ -669,7 +661,7 @@
         알림 소리
         이 계정에서 알림 켜기
         이 기기에서 알림 켜기
    -    3초 동안 화면을 켬
    +    3초 동안 화면을 켜기
         소리 알림 설정
         전화 알림 설정
         조용한 알림 설정
    @@ -688,14 +680,14 @@
         부팅 시 시작
         백그라운드 동기화 켜기
         동기화 요청 시간 초과
    -    각 요청 간 딜레이
    +    각 동기화 간 딜레이
         
         
     
         버전
         olm 버전
         이용 약관
    -    제 3자 공지
    +    제 3자 고지
         저작권
         개인 정보 정책
         미디어 유지
    @@ -710,8 +702,8 @@
         암호화
         암호화 키 관리
         알림 대상
    -    로컬 주소록
    -    주소록 권한
    +    로컬 연락처
    +    연락처 권한
         국제전화 나라 번호
         홈 표시
         알림을 놓친 방을 고정
    @@ -723,13 +715,13 @@
         다른 사용자가 당신이 입력 중인 것을 알게 합니다.
         마크다운 형식
         마크다운 문법을 사용해 메시지를 보내기 전에 형식을 지정합니다. 별표를 사용해 기울임 꼴 문자를 표시하는 고급 서식을 지정할 수 있습니다.
    -    모든 메시지에 타임스탬프 보여주기
    -    12시간 단위를 사용하는 타임스탬프 보여주기
    -    읽은 기록 보여주기
    +    모든 메시지에 타임스탬프 보이기
    +    12시간 단위를 사용하는 타임스탬프 보이기
    +    읽은 기록 보이기
         세부적인 목록으로 읽은 목록을 클릭하세요.
    -    참가 및 떠남 이벤트 보여주기
    +    참가 및 떠남 이벤트 보이기
         초대, 추방, 그리고 차단은 영향이 없습니다.
    -    계정 이벤트 보여주기
    +    계정 이벤트 보이기
         아바타와 표시 이름 변경도 포함합니다.
         사용자가 언급할 때 진동
         보내기 전 미디어 미리보기
    @@ -742,7 +734,7 @@
         알림 개인 정보
         Riot은 백그라운드에서 실행되어 알림을 안전하고 은밀하게 관리할 수 있습니다. 이것은 배터리 사용량에 영향을 줄 수 있습니다.
         권한 부여
    -    다른 옵션을 선택하세요
    +    다른 설정을 선택하세요
     
         백그라운드 연결
         Riot은 신뢰가 있는 알림을 위해 낮은 영향의 백그라운드 연결을 유지해야 합니다.
    @@ -758,10 +750,10 @@
         데이터 절약 모드
         데이터 절약 모드는 특정 필터를 적용하여 현재 상태 업데이트와 입력 중 알림을 걸러냅니다.
     
    -    기기 세부 사항
    +    기기 정보
         ID
    -    이름
    -    기기 이름
    +    공개 이름
    +    공개 이름 업데이트
         마지막으로 본 순간
         %1$s @ %2$s
         이 작업은 추가 인증이 필요합니다.
    @@ -779,13 +771,13 @@
         언어
         언어를 선택하세요
     
    -    인증 보류 중
    +    확인 보류 중
         이메일을 인증해서 거기에 있는 링크를 클릭하세요. 모두 끝나면, 계속하기를 클릭하세요.
    -    이메일 주소를 인증할 수 없습니다. 이메일을 인증해서 거기에 있는 링크를 클릭하세요. 모두 끝나면, 계속하기를 클릭하세요.
    +    이메일 주소를 확인할 수 없습니다. 이메일을 인증해서 거기에 있는 링크를 클릭하세요. 모두 끝나면, 계속하기를 클릭하세요.
         이 이메일 주소는 이미 사용 중입니다.
         이 이메일 주소를 찾을 수 없습니다.
         이 전화번호는 이미 사용 중입니다.
    -    이메일 주소를 인증하는 중 오류가 발생했습니다.
    +    이메일 주소를 확인하는 중 오류가 발생했습니다.
     
         비밀번호
         비밀번호 변경
    @@ -803,7 +795,7 @@
     
         이 알림 대상을 제거하겠습니까\?
     
    -    %1$s %2$s를 제거하겠습니까\?
    +    %1$s %2$s님을 제거하겠습니까\?
     
         나라를 선택하세요
     
    @@ -811,12 +803,12 @@
         나라를 선택해주세요
         전화번호
         선택한 나라에 올바르지 않은 전화번호
    -    휴대 전화 인증
    +    휴대 전화 확인
         활성 코드가 담신 SMS를 보냈습니다. 코드를 아래에 입력하세요.
         활성 코드 입력
    -    전화번호를 인증하는 중 오류
    +    전화번호를 확인하는 중 오류
         코드
    -    전화번호를 인증하는 중 오류가 발생했습니다.
    +    전화번호를 확인하는 중 오류가 발생했습니다.
         추가 정보: %s
     
         미디어
    @@ -845,7 +837,7 @@
         없음
     
         접근성 및 가시성
    -    이 방을 방 디렉토리에 놓기
    +    이 방을 방 목록에 놓기
         알림
         방 접근성
         방 기록 읽기 권한
    @@ -853,7 +845,7 @@
         누가 이 방에 접근할 수 있나요\?
     
         누구나
    -    (이 옵션을 선택한 시점부터) 구성원만
    +    (이 설정을 선택한 시점부터) 구성원만
         (초대받은 시점부터) 구성원만
         (참가한 시점부터) 구성원만
     
    @@ -872,8 +864,8 @@
         종단간 암호화
         종단간 암호화가 켜졌습니다
         암호화를 켜기 위해 로그아웃을 해야 합니다.
    -    인증된 기기로만 암호화
    -    이 기기로는 방에서 인증되지 않은 기기로 암호화된 메시지를 절대 보내지 마세요.
    +    확인된 기기로만 암호화
    +    이 기기로는 방에서 확인되지 않은 기기로 암호화된 메시지를 절대 보내지 않기.
     
         이 방은 로컬 주소가 없습니다
         새 주소 (예: #foo:matrix.org)
    @@ -899,7 +891,7 @@
         암호화 활성화
     \n(경고: 다시 비활성화할 수 없음!)
     
    -    디렉토리
    +    목록
         테마
     
         %s님이 이 방의 특정 지점을 불러오려 했으나 찾을 수 없었습니다.
    @@ -909,17 +901,17 @@
         이벤트 정보
         사용자 ID
         Curve25519 ID 키
    -    청구된 Ed25519 핑거프린트 키
    +    Ed25519 핑거프린트 키가 필요함
         알고리즘
         세션 ID
         암호 해독 오류
     
         발신자 기기 정보
    -    기기 이름
    -    이름
    -    기기 ID
    +    공개 이름
    +    공개 이름
    +    ID
         기기 키
    -    인증
    +    확인
         Ed25519 핑거프린트
     
         종단간 암호화 방 키 내보내기
    @@ -938,40 +930,40 @@
         방 키 가져오기
         로컬 파일에서 키 가져오기
         가져오기
    -    인증된 기기로만 암호화
    -    이 기기에서 인증되지 않은 기기로 절대 암호화된 메시지를 보내기 마세요.
    +    확인된 기기로만 암호화
    +    이 기기에서 확인되지 않은 기기로 절대 암호화된 메시지를 보내지 않습니다.
         %1$d/%2$d 키를 성공적으로 가져왔습니다.
     
    -    인증되지 않음
    -    인증됨
    +    확인되지 않음
    +    확인됨
         블랙리스트 대상
     
         알 수 없는 기기
         알 수 없는 ip
         없음
     
    -    인증
    -    인증하지 않음
    +    확인
    +    확인하지 않음
         블랙리스트
         블랙리스트 제외
     
    -    인증 기기
    -    이 기기가 신뢰할 수 있는 지 인증하려면, 다른 방법을 사용하려 소유자에게 문의하세요 (예: 현실에서 혹은 전화로) 그리고 이 기기의 사용자 설정에서 표시된 키가 아래에 있는 키와 맞는지 물어보세요:
    -    그것이 맞다면, 아래의 인증 버튼을 누르세요. 맞지 않다면, 다른 사람이 이 기기를 가로채고 있는 것이고 블랙리스트에 올려야 합니다. 앞으로 이 인증 절차는 더 정교해질 것입니다.
    -    키가 맞다는 것은 인증합니다
    +    기기 확인
    +    이 기기가 신뢰할 수 있는 지 확인하려면, 다른 방법을 사용하여 소유자에게 연락하세요 (예: 현실에서 혹은 전화로) 그리고 이 기기의 사용자 설정에서 표시된 키가 아래에 있는 키와 맞는지 물어보세요:
    +    그것이 맞다면, 아래의 확인 버튼을 누르세요. 맞지 않다면, 다른 사람이 이 기기를 가로채고 있는 것이고 블랙리스트에 올려야 합니다. 앞으로 이 확인 절차는 더 정교해질 것입니다.
    +    키가 맞다는 것은 확인합니다
     
         Riot은 이제 종단간 암호화를 지원하지만 활성화하려면 다시 로그인해야 합니다.
     \n
     \n지금 바로 하거나 나중에 애플리케이션 설정에서 할 수 있습니다.
     
         알 수 없는 기기가 있는 방
    -    이 방에는 인증되지 않은 알 수 없는 기기가 있습니다.
    -\n즉, 사용자에 속해 있다고 주장하는 기기라는 보장이 없습니다.
    -\n저희는 계속하기 전에 각 기기에 인증 절차를 하기를 권합니다, 하지만 원한다면 인증하지 않고 바로 메시지를 보낼 수 있습니다.
    -\n
    +    이 방에는 확인되지 않은 알 수 없는 기기가 있습니다. 
    +\n즉, 사용자에 속해 있다고 주장하는 기기라는 보장이 없습니다. 
    +\n저희는 계속하기 전에 각 기기에 확인 절차를 하기를 권합니다, 하지만 원한다면 확인하지 않고 바로 메시지를 보낼 수 있습니다. 
    +\n 
     \n알 수 없는 기기:
     
    -    방 디렉토리 선택
    +    방 목록 선택
         서버를 이용할 수 없거나 과부하 상태입니다
         공개 서버를 표시할 홈서버를 입력하세요
         홈서버 URL
    @@ -1044,22 +1036,22 @@
         맞춤 카메라 화면 대신 시스템 카메라를 실행합니다.
         키보드 엔터 키로 메시지 보내기
         음성 메시지 보내기
    -    이 옵션은 메시지를 기록하기 위한 제 3자 애플리케이션이 필요합니다.
    +    이 설정은 메시지를 기록하기 위한 제 3자 애플리케이션이 필요합니다.
     
         새 기기 \'%s\'을(를) 추가했습니다, 여기에는 암호화 키가 필요합니다.
         새 기기에는 암호화 키가 필요합니다.
     \n기기 이름: %1$s
     \n마지막으로 본 순간: %2$s
     \n다른 기기에서 로그인하지 않았다면, 이 요청을 무시하세요.
    -    인증되지 않은 기기 \'%s\'가 암호화 키를 요청했습니다.
    -    인증되지 않은 기기가 암호화 키를 요청했습니다.
    -\n기기 이름: %1$s
    -\n마지막으로 본 순간: %2$s
    +    확인되지 않은 기기 \'%s\'이(가) 암호화 키를 요청했습니다.
    +    확인되지 않은 기기가 암호화 키를 요청했습니다. 
    +\n기기 이름: %1$s 
    +\n마지막으로 본 순간: %2$s 
     \n다른 기기에서 로그인하지 않았다면, 이 요청을 무시하세요.
     
    -    인증 시작
    -    인증
    -    인증 없이 공유
    +    확인 시작
    +    확인
    +    확인하지 않고 공유
         공유
         키 공유 요청
         요청 무시하기
    @@ -1071,9 +1063,9 @@
         명령어 오류
         인식할 수 없는 명령어: %s
         \"%s\" 명령어는 더 많은 매개 변수가 필요하거나, 일부 매개 변수가 옳지 않습니다.
    -    동작 표시하기
    +    활동 표시하기
         주어진 ID로 사용자 차단하기
    -    주어진 ID로 사용자 차단 해제하기
    +    주어진 ID로 사용자 차단 풀기
         사용자의 권한 등급 정의하기
         주어진 ID로 사용자 강등하기
         주어진 ID 현재 방에 사용자 초대하기
    @@ -1121,8 +1113,8 @@
         
         커뮤니티 관리자가 이 커뮤니티에 대한 자세한 설명을 제공하지 않았습니다.
     
    -    %2$s님에 의해 %1$s에서 추방당했습니다
    -    %2$s님에 의해 %1$s에서 차단됬습니다
    +    %2$s님에 의해 %1$s 방에서 추방당했습니다
    +    %2$s님에 의해 %1$s 방에서 차단당했습니다
         이유: %1$s
         다시 참가하기
         방 잊어버리기
    @@ -1135,10 +1127,10 @@
         지금 검토하기
     
         계정 비활성화
    -    이것으로 계정은 영구적으로 사용할 수 없게 됩니다. 로그인할 수 없고 누구도 같은 사용자 ID로 다시 가입할 수 없게 됩니다. 이 계정으로 참가한 모든 방에서 떠나게 되고, ID 서버의 계정 세부 사항도 삭제됩니다. 이 행동은 돌이킬 수 없습니다.
    -\n
    -\n계정을 비활성화해도 기본적으로 보낸 메시지를 잊지 않습니다. 메시지를 잊기를 원한다면, 아래 상자를 선택하세요.
    -\n
    +    이것으로 계정은 영구적으로 사용할 수 없게 됩니다. 로그인할 수 없고 누구도 같은 사용자 ID로 다시 등록할 수 없게 됩니다. 이 계정으로 참가한 모든 방에서 떠나게 되고, ID 서버의 계정 세부 사항도 삭제됩니다. 이 행동은 돌이킬 수 없습니다. 
    +\n 
    +\n계정을 비활성화해도 기본적으로 보낸 메시지를 잊지 않습니다. 메시지를 잊기를 원한다면, 아래 상자를 선택하세요. 
    +\n 
     \nMatrix의 메시지 가시성은 이메일과 유사합니다. 우리가 메시지를 잊는 것은 보낸 메시지가 모든 새 사용자 혹은 등록하지 않은 사용자와 공유하지 않는다는 것입니다, 하지만 이 메시지에 접근한 등록된 사용자는 이 사본으로 여전히 접근할 수 있을 것입니다.
         내 계정을 비활성화하면 내가 보낸 모든 메시지는 잊어주세요 (경고: 이것은 미래 사용자가 불완전한 대화를 읽게 됩니다)
         계속하려면, 비밀번호를 입력하세요:
    @@ -1160,7 +1152,7 @@
         이 홈서버가 리소스 한도를 초과했습니다.
     
          이 홈서버가 월 간 활성 사용자 한도를 초과해서 일부 사용자는 로그인할 수 없습니다.
    -    이 홈서버가 월 간 사용자 한도를 초과했습니다.
    +    이 홈서버가 월 간 활성 사용자 한도를 초과했습니다.
     
         한도를 높이려면 %s하세요.
         이 서비스 사용을 계속하려면 %s하세요.
    @@ -1174,7 +1166,7 @@
         펼치기
         접기
     
    -    정보 영역 보여주기
    +    정보 영역 보이기
         항상
         메시지와 오류일 시
         오류만
    @@ -1188,21 +1180,21 @@
         Riot.im - 대화하세요, 나만의 방식
         우리는 항상 Riot.im을 변경하고 개선하고 있습니다. 전체 변경 내역은 여기서 찾을 수 있습니다: %1$s. 놓치지 않도록 업데이트를 켜 놓아주세요.
         전적으로 여러분의 통제 하에 있는 범용 보안 대화 앱.
    -    여러분의 통제 하에 완전히 유연한 대화 앱. Riot은 여러분이 원하는 방식으로 대화할 수 있도록 합니다. 개방형 분산 커뮤니티의 표준 - [matrix]를 위해 제작됨.
    -\n
    -\n무료 matrix.org 계정을 만들고, https://modular.im에서 자신만의 서버, 혹은 다른 Matrix 서버를 얻으세요.
    -\n
    -\n왜 Riot.im을 선택해야 하나요\?
    -\n
    -\n• 완전한 대화: 원하는 대로 팀이나 친구, 커뮤니티를 중심으로 방을 만드세요! 대화, 파일 공유, 위젯 추가와 음성 및 영상 통화 - 모두 무료입니다.
    -\n
    -\n• 강력한 통합: 여러분이 알고 사랑하는 도구와 함께 Riot.im을 사용하세요. Riot.im이라면 다른 대화 앱의 사용자와 그룹까지도 대화할 수 있습니다.
    -\n
    -\n• 개인 및 보안: 대화를 비밀로 유지하세요. 최첨단 종단간 암호화로 비밀 대화를 은밀하게 유지해줍니다.
    -\n
    -\n• 오픈 소스: Matrix로 만들어진 오픈 소스입니다. 자신의 데이터를 자신의 서버에 소유하거나, 신뢰하는 서버에 맡기세요.
    -\n
    -\n• 어디에 있든: 모든 기기나 https://riot.im에서 완전히 동기화된 메시지 기록으로 연락을 유지합니다.
    +    여러분의 통제 하에 완전히 유연한 대화 앱. Riot은 여러분이 원하는 방식으로 대화할 수 있도록 합니다. 개방형 분산 커뮤니티의 표준 - [matrix]를 위해 제작됨. 
    +\n 
    +\n무료 matrix.org 계정을 만들고, https://modular.im 에서 자신만의 서버, 혹은 다른 Matrix 서버를 얻으세요. 
    +\n 
    +\n왜 Riot.im을 선택해야 하나요\? 
    +\n 
    +\n• 완전한 대화: 원하는 대로 팀이나 친구, 커뮤니티를 중심으로 방을 만드세요! 대화, 파일 공유, 위젯 추가와 음성 및 영상 통화 - 모두 무료입니다. 
    +\n 
    +\n• 강력한 통합: 여러분이 알고 사랑하는 도구와 함께 Riot.im을 사용하세요. Riot.im이라면 다른 대화 앱의 사용자와 그룹까지도 대화할 수 있습니다. 
    +\n 
    +\n• 개인 및 보안: 대화를 비밀로 유지하세요. 최첨단 종단간 암호화로 비밀 대화를 은밀하게 유지해줍니다. 
    +\n 
    +\n• 오픈 소스: Matrix로 만들어진 오픈 소스입니다. 자신의 데이터를 자신의 서버에 소유하거나, 신뢰하는 서버에 맡기세요. 
    +\n 
    +\n• 어디에 있든: 모든 기기나 https://riot.im 에서 완전히 동기화된 메시지 기록으로 연락을 유지합니다.
     
         암호 만들기
         암호 확인
    @@ -1272,7 +1264,7 @@
         메시지 복구
     
         복구 키를 잃어버렸나요\? 설정에서 새로운 키를 만들 수 있습니다.
    -    이 암호로 백업을 해독할 수 없습니다: 올바른 복구 암호를 입력해서 인증해주세요.
    +    이 암호로 백업을 해독할 수 없습니다: 올바른 복구 암호를 입력해서 확인해주세요.
         네트워크 오류: 인터넷 연결 상태를 확인하고 다시 시도해주세요.
     
         백업 복구:
    @@ -1281,7 +1273,7 @@
         키 가져오는 중…
         기록 풀기
         복구 키를 입력하세요
    -    이 복구 키로 백업을 해독할 수 없습니다: 올바른 복구 키를 입력해서 인증하세요.
    +    이 복구 키로 백업을 해독할 수 없습니다: 올바른 복구 키를 입력해서 확인해주세요.
     
         백업이 복구되었습니다 %s !
         %1$d개의 세션 키를 복구했고, 이 기기에서 알려지지 않은 %2$d개의 새 키를 추가함
    @@ -1305,10 +1297,10 @@
     
         백업이 ID %s의 알 수 없는 기기의 서명이 있습니다.
         백업이 이 기기의 올바른 서명이 있습니다.
    -    백업이 인증된 기기 %s의 올바른 서명이 있습니다.
    -    백업이 인증되지 않은 기기 %s의 올바른 서명이 있습니다
    -    백업이 인증된 기기 %s의 올바르지 않은 서명이 있습니다
    -    백업이 인증되지 않은 기기 %s의 올바르지 않은 서명이 있습니다
    +    백업이 확인된 기기 %s의 올바른 서명이 있습니다.
    +    백업이 확인되지 않은 기기 %s의 올바른 서명이 있습니다
    +    백업이 확인된 기기 %s의 올바르지 않은 서명이 있습니다
    +    백업이 확인되지 않은 기기 %s의 올바르지 않은 서명이 있습니다
         백업에 대한 신뢰 정보를 주지 못했습니다 (%s).
     
         이 기기에서 키 백업을 사용하려면, 지금 암호나 복구 키를 복구하세요.
    @@ -1354,7 +1346,7 @@
     
         짧은 문장과 비교하여 확인하세요.
         보안을 최대화하려면, 상대방과 직접 대면하거나 신뢰할 수 있는 다른 대화 수단을 사용하는 것이 좋습니다.
    -    인증 시작
    +    확인 시작
         수신 확인 요청
         이 기기를 확인하여 신뢰할 수 있는 것으로 표시하세요. 종단간 암호화 메시지를 사용하는 경우 상대방의 기기를 신뢰하면 안심할 수 있습니다.
         이 기기를 확인하여 신뢰할 수 있는 것으로 표시하세요, 그리고 당신의 기기도 상대방에게 신뢰할 수 있는 것으로 표시하세요.
    @@ -1366,27 +1358,27 @@
         요청 보기
         상대방이 확인하기를 기다리는 중…
     
    -    인증되었습니다!
    -    성공적으로 이 기기를 인증했습니다.
    +    확인되었습니다!
    +    성공적으로 이 기기를 확인했습니다.
         이 사용자 간의 보안 메시지는 종단간 암호화 되며 제 3자가 읽을 수 없습니다.
         알겠습니다
     
    -    아무것도 안 나타나나요\? 일부 클라이언트는 아직 대화 식 인증을 지원하지 않습니다. 옛날 인증 방식을 사용하세요.
    -    옛날 인증 방식 사용하기.
    +    아무것도 안 나타나나요\? 일부 클라이언트는 아직 대화 식 확인을 지원하지 않습니다. 옛날 확인 방식을 사용하세요.
    +    옛날 확인 방식 사용하기.
     
    -    키 인증
    +    키 확인
         요청 취소됨
         상대방이 확인을 취소했습니다.
     \n%s
    -    인증이 취소되었습니다.
    +    확인이 취소되었습니다. 
     \n이유: %s
     
    -    상호작용 기기 인증
    -    인증 요청
    -    %s님이 당신의 기기를 인증하고 싶습니다
    +    상호작용 기기 확인
    +    확인 요청
    +    %s님이 당신의 기기를 확인하고 싶습니다
     
    -    사용자가 인증을 취소했습니다
    -    인증 과정 시간이 초과되었습니다
    +    사용자가 확인을 취소했습니다
    +    확인 과정 시간이 초과되었습니다
         기기는 이 처리에 대해 모릅니다
         기기는 키 계약, 해시, MAC 또는 SAS 방식에 동의할 수 없습니다
         해시 커밋이 일치하지 않습니다
    @@ -1422,9 +1414,9 @@
         리액션 보기
         리액션
     
    -    사용자가 삭제한 이벤트
    -    방 관리자가 중재한 이벤트
    -    %s님이 %s에 마지막으로 편집함
    +    사용자가 감춘 이벤트
    +    방 관리자가 감춘 이벤트
    +    %1$s님이 %2$s에 마지막으로 편집함
     
     
         잘못된 이벤트, 표시할 수 없음
    @@ -1446,22 +1438,22 @@
         방 이름
         공공
         누구나 이 방에 참가할 수 있습니다
    -    방 디렉토리
    -    이 방을 방 디렉토리에 게시
    +    방 목록
    +    이 방을 방 목록에 게시
     
         신뢰 정보를 얻는 과정에서 오류가 발생했습니다
         키 백업 데이터를 얻는 과정에서 오류가 발생했습니다
     
         베타 버전에 온 것을 환영합니다!
         RiotX가 개발 중에 있기에, 일부 기능이 부족하고 버그가 나올 수 있습니다.
    -    최신 기능 목록은 항상 %1$s에 있고, 버그를 발견했다면 홈의 왼쪽 위 메뉴에서 보고해주세요, 그러면 가능한 한 빨리 고치겠습니다.
    +    최신 기능 목록은 항상 %1$s에 있고, 버그를 발견했다면 홈의 왼쪽 위 메뉴에서 신고해주세요, 그러면 가능한 한 빨리 고치겠습니다.
         Play 스토어 설명
    -    버그를 발견했다면 홈의 왼쪽 위 메뉴에서 보고해주세요, 그러면 가능한 한 빨리 고치겠습니다.
    +    버그를 발견했다면 홈의 왼쪽 위 메뉴에서 신고해주세요, 그러면 가능한 한 빨리 고치겠습니다.
     
         파일 \"%1$s\"에서 종단간 암호화 키 가져옴.
     
         Matrix SDK 버전
    -    다른 제 3자 공지
    +    다른 제 3자 고지
         이 방을 이미 봤습니다!
     
         빠른 리액션
    @@ -1493,17 +1485,17 @@
         감사합니다, 제안을 성공적으로 보냈습니다
         제안을 보내는 데 실패함 (%s)
     
    -    타임라인에서 숨겨진 이벤트 보여주기
    +    타임라인에서 숨겨진 이벤트 보이기
     
         RiotX - 차세대 Matrix 클라이언트
         최신 안드로이드 프레임워크를 사용해 더 빠르고 가벼운 Matrix를 위한 클라이언트
    -    RiotX는 Matrix 프로토콜 (Matrix.org)을 위한 새 클라이언트입니다: 안전한 분산 통신을 위한 개방형 네트워크. RiotX는 Matrix 안드로이드 SDK의 Riot 안드로이드 클라이언트 전체 개정판을 기반으로 한 Riot 안드로이드 클라이언트의 전체 개정판입니다.
    -\n
    -\n면책 조항: 이것은 베타 버전입니다. RiotX는 현재 개발 중이고 한계가 있으며 (많지 않으면 좋겠지만) 버그가 있습니다. 모든 피드백은 환영합니다!
    -\n
    -\nRiotX 지원: • 존재하는 계정으로 로그인 • 방을 만들고 공공 방에 참가 • 초대를 수락하거나 거절 • 사용자 방 목록 • 방 세부 정보 보기 • 문자 메시지 보내기 • 첨부 파일 보내기 • 암호화된 방에서 메시지 읽고 쓰기 • 암호화: 종단간 암호화 키 백업, 고급 기기 인증, 키 공유 요청과 답장 • 푸시 알림 • 밝은 테마, 어두운 테마 그리고 검정 테마
    -\n
    -\n아직 Riot의 모든 기능이 RiotX에 구현되지 않았습니다. 주요 없는 (그리고 곧 나올!) 기능: • 계정 만들기 • 방 설정 (방 구성원 목록 등) • 다이렉트 대화 방 만들기 • 전화 • 위젯 • …
    +    RiotX는 Matrix 프로토콜 (Matrix.org)을 위한 새 클라이언트입니다: 안전한 분산 통신을 위한 개방형 네트워크. RiotX는 Matrix 안드로이드 SDK의 Riot 안드로이드 클라이언트 전체 개정판을 기반으로 한 Riot 안드로이드 클라이언트의 전체 개정판입니다. 
    +\n 
    +\n면책 조항: 이것은 베타 버전입니다. RiotX는 현재 개발 중이고 한계가 있으며 (많지 않으면 좋겠지만) 버그가 있습니다. 모든 피드백은 환영합니다! 
    +\n 
    +\nRiotX 지원: • 존재하는 계정으로 로그인 • 방을 만들고 공공 방에 참가 • 초대를 수락하거나 거절 • 사용자 방 목록 • 방 세부 정보 보기 • 문자 메시지 보내기 • 첨부 파일 보내기 • 암호화된 방에서 메시지 읽고 쓰기 • 암호화: 종단간 암호화 키 백업, 고급 기기 확인, 키 공유 요청과 답장 • 푸시 알림 • 밝은 테마, 어두운 테마 그리고 검정 테마 
    +\n 
    +\n아직 Riot의 모든 기능이 RiotX에 구현되지 않았습니다. 주요 없는 (그리고 곧 나올!) 기능: • 계정 만들기 • 방 설정 (방 구성원 목록 등) • 전화 • 위젯 • …
     
         다이렉트 메시지
     
    @@ -1529,7 +1521,7 @@
         원하는 것을 찾을 수 없나요\?
         새 방 만들기
         새 다이렉트 메시지 보내기
    -    방 디렉토리 보기
    +    방 목록 보기
     
         이름 혹은 ID (#예시:matrix.org)
     
    @@ -1547,4 +1539,88 @@
     
         편집 기록 보기
     
    +    검토
    +    끊기
    +
    +    계속 하려면 이 서비스 약관에 동의해야 합니다.
    +
    +    Riot의 이전 버전은 보안 버그로 ID 서버 (%1$s)가 계정으로 접근할 수 있었습니다. %2$s을(를) 신뢰하면 이것을 무시하고, 그렇지 않다면 로그아웃한 후 다시 로그인하세요.
    +\n
    +\n자세한 설명은 여기서 읽으세요:
    +\nhttps://medium.com/@RiotChat/36b4792ea0d6
    +
    +    서비스 약관
    +    조건 검토
    +    다른 사람이 검색할 수 있음
    +    봇, 브릿지, 위젯과 스티커 팩을 사용하세요
    +
    +    읽은 시간
    +
    +    없음
    +    취소
    +    연결 해제
    +    설정된 ID 서버가 없습니다.
    +
    +    잘놋 설정된 서버로 인해 전화에 실패함
    +    전화가 정상적으로 작동하도록 TURN 서버를 설정하려면 홈서버 (%1$s)의 관리자에게 연락해주세요.
    +\n
    +\n아니면 %2$s에서 공개 서버를 사용할 수 있습니다, 하지만 신뢰할 수 없고 서버와 IP 주소를 공유하게 됩니다. 설정에서 이것을 관리할 수도 있습니다.
    +    %s 사용하기
    +    다시 묻지 않기
    +
    +    계정 복구 용 이메일을 설정합니다, 그리고 이후 알고 있는 사람들이 당신을 찾을 수 있는지 여부를 선택할 수 있습니다.
    +    전화를 설정합니다, 그리고 이후 알고 있는 사람들이 당신을 찾을 수 있는지 여부를 선택할 수 있습니다.
    +    계정 복구 용 이메일을 설정합니다. 이메일이나 전화로 이후 알고 있는 사람들이 당신을 찾을 수 있는지 여부를 선택하는데 사용됩니다.
    +    계정 복구 용 이메일을 설정합니다. 이메일이나 전화로 이후 알고 있는 사람들이 당신을 찾을 수 있는지 여부를 선택하는데 사용됩니다.
    +    이 URL로는 홈서버에 접근할 수 없습니다, 확인해주세요
    +    대체 전화 지원 서버 허용
    +    홈서버가 전화를 지원하지 않는다면 %s을(를) 지원 서버로 사용합니다 (전화하는 동안 IP 주소가 공유됩니다)
    +    이 작업을 하려면 설정에서 ID 서버를 추가하세요.
    +    백그라운드 동기화 모드 (실험적)
    +    배터리에 최적화됨
    +    Riot은 기기의 제한된 자원 (배터리)을 유지하기 위해 백그라운드에서 동기화합니다.
    +\n기기 자원 상태에 따라 운영체제에 의해 동기화는 지연될 수 있습니다.
    +    실시간으로 최적화됨
    +    Riot은 (설정할 수 있는) 특정 시간에 주기적으로 백그라운드에거 동기화됩니다.
    +\n이는 라디오와 배터리 사용에 영향을 주며 Riot이 이벤트를 수신하고 있는 상태라는 알림이 영구적으로 표시됩니다.
    +    백그라운드 동기화 없음
    +    앱이 백그라운드에 있을 때 오는 메시지의 알림을 받지 않습니다.
    +    설정을 업데이트하는데 실패했습니다.
    +
    +
    +    원하는 동기화 간격
    +    %s
    +\n동기화는 자원 (배터리)이나 기기의 상태 (수면)에 따라 지연됩니다.
    +    탐색
    +    탐색 설정을 관리합니다.
    +    공개 이름 (대화하는 사람들에게 보여집니다)
    +    기기의 공개 이름은 대화하는 사람들에게 보여집니다
    +    ID 서버를 사용하고 있지 않습니다
    +    설정된 ID 서버가 없습니다, 비밀번호를 초기화하려면 ID 서버가 필요합니다.
    +
    +    다른 홈서버로 연결을 시도합니다. 로그아웃하겠습니까\?
    +
    +    ID 서버
    +    ID 서버 연결 해제
    +    ID 서버 설정
    +    ID 서버 바꾸기
    +    알고 있는 연락처 사람들을 찾거나 연락처 사람들이 당신을 찾도록 현재 %1$s을(를) 사용하고 있습니다.
    +    현재 ID 서버를 사용하고 있지 않습니다. 알고 있는 연락처 사람들을 찾거나 연락처 사람들이 당신을 찾도록 하려면, 아래에서 하나를 설정하세요.
    +    이메일 주소로 찾을 수 있음
    +    이메일을 추가하면 탐색 설정이 나타납니다.
    +    전화번호를 추가하면 탐색 설정이 나타납니다.
    +    ID 서버에서 연결을 해제하면 이메일이나 전화로 다른 사용자가 당신을 찾을 수 없게 되고 다른 사람들을 초대할 수 없게 됩니다.
    +    전화번호로 찾을 수 있음
    +    %s(으)로 확인 이메일을 보냈습니다, 이메일을 확인하고 확인 링크를 클릭하세요
    +    보류 중
    +
    +    새 ID 서버를 입력
    +    ID 서버에 연결할 수 없음
    +    ID 서버 URL을 입력해주세요
    +    ID 서버가 서비스 약관이 없습니다
    +    선택한 ID 서버가 서비스 약관이 없습니다. 서비스의 소유자를 신뢰하는 경우에만 계속하세요
    +    %s(으)로 문자 메시지를 보냈습니다. 문자에 있는 확인 코드를 입력해주세요.
    +
    +    현재 이메일 주소나 전화번호를 ID 서버 %s와 공유하고 있습니다. 공유하기를 중지하려면 %s(으)로 다시 연결해야 합니다.
    +    ID 서버 (%s)의 서비스 약관에 동의하면 다른 사용자가 당신을 이메일 주소나 전화번호로 찾을 수 있게 됩니다.
     
    diff --git a/vector/src/main/res/values-lv/strings.xml b/vector/src/main/res/values-lv/strings.xml
    index 4257e9612c..be08be4dd1 100644
    --- a/vector/src/main/res/values-lv/strings.xml
    +++ b/vector/src/main/res/values-lv/strings.xml
    @@ -134,11 +134,6 @@
         Parole
         Jauna parole
         Lietotājvārds
    -    Pievieno epasta adresi savam kontam, lai pārējiem dalībniekiem būtu iespējams Tevi atrast, kā arī lai Tev būtu iespēja mainīt paroli.
    -    Pievieno telefona numuru savam kontam, lai ļautu pārējiem dalībniekiem Tevi atrast.
    -    Pievieno savam kontam epasta adresi un/vai telefona numuru, lai ļautu pārējiem dalībniekiem Tevi atrast.
    -
    -Kā arī epasta adrese ļaus Tev atiestatīt paroli.
         Gaiša ādiņa
         Tumša ādiņa
         Melna ādiņa
    @@ -169,9 +164,6 @@ Kā arī epasta adrese ļaus Tev atiestatīt paroli.
         Uzņemt foto
         Uzņemt video
     
    -    Pievieno kontam epasta adresi un telefona numuru, lai citi lietotāji var Tevi atrast.
    -
    -Epasta adrese arī ļaus Tev nepieciešamības gadījumā atjaunot paroli.
         Epasta adrese
         Epasta adrese (papildus)
         Tālruņa #
    diff --git a/vector/src/main/res/values-nl/strings.xml b/vector/src/main/res/values-nl/strings.xml
    index 778746b007..bacc675f86 100755
    --- a/vector/src/main/res/values-nl/strings.xml
    +++ b/vector/src/main/res/values-nl/strings.xml
    @@ -140,14 +140,6 @@
         Wachtwoord
         Nieuw wachtwoord
         Gebruikersnaam
    -    Voeg een e-mailadres aan uw account toe zodat gebruikers u kunnen vinden en zodat u het wachtwoord kunt veranderen.
    -    Voeg een telefoonnummer aan uw account toe zodat gebruikers u kunnen vinden.
    -    Voeg een e-mailadres en/of telefoonnummer aan uw account toe zodat gebruikers u kunnen vinden.
    -\n
    -\nHet e-mailadres maakt het ook mogelijk om uw wachtwoord te veranderen.
    -    Voeg een e-mailadres en een telefoonnummer aan uw account toe zodat gebruikers u kunnen vinden.
    -\n
    -\nHet e-mailadres maakt het ook mogelijk om uw wachtwoord te veranderen.
         E-mailadres
         E-mailadres (optioneel)
         Telefoonnummer
    @@ -1518,4 +1510,13 @@
         Gebruikers komen niet overeen
         Onbekende fout
     
    +    Geen
    +    Intrekken
    +    Verbinding verbreken
    +    Nakijken
    +    Weigeren
    +
    +    Geen identiteitsserver geconfigureerd.
    +
    +    Oproep mislukt door verkeerd geconfigureerde server
     
    diff --git a/vector/src/main/res/values-nn/strings.xml b/vector/src/main/res/values-nn/strings.xml
    index 9827aef6e0..1a7331efa4 100644
    --- a/vector/src/main/res/values-nn/strings.xml
    +++ b/vector/src/main/res/values-nn/strings.xml
    @@ -173,10 +173,6 @@
         Åtgangsord
         Nytt åtgangsord
         Brukarnamn
    -    Knyt ei epostadressa til brukaren din slik at folk kann finna deg, og du kan attendestilla åtgangsordet ditt.
    -    Knyt eit telefonnummer til brukaren din slik at andre brukarar kann finna deg.
    -    Knyt ei epostaddresse og/eller eit telefonnummer til brukaren din slik at andre brukarar kann finna deg. Epostadressa lèt deg au attendestilla åtgangsordet.
    -    Knyt ei epostadresse og eit telefonnummer til brukaren din slik at andre brukarar kann finna deg. Epostadressa lèt deg og attendestilla åtgangsordet.
         Epostadressa
         Epostadressa (valfritt)
         Telefonnummer
    diff --git a/vector/src/main/res/values-pl/strings.xml b/vector/src/main/res/values-pl/strings.xml
    index 2b2198cd17..37e402911e 100644
    --- a/vector/src/main/res/values-pl/strings.xml
    +++ b/vector/src/main/res/values-pl/strings.xml
    @@ -133,8 +133,6 @@
         Hasło
         Nowe hasło
         Nazwa użytkownika
    -    Dodaj do konta adres e-mail, aby użytkownicy mogli Cię odnaleźć, oraz abyś mógł przywrócić swoje hasło.
    -    Dodaj numer telefonu, aby użytkownicy mogli Cię odnaleźć.
         Adres e-mail
         Adres e-mail (nieobowiązkowy)
         Numer telefonu
    @@ -572,12 +570,6 @@ Zauważ, że ta czynność spowoduje ponowne uruchomienie aplikacji i może to t
         Zrób zdjęcie
         Nagraj film
     
    -    Dodaj adres e-mail i/lub nr telefonu do swojego konta, aby umożliwić innym użytkownikom odnalezienie ciebie.
    -\n
    -\nAdres e-mail pozwoli Ci również zresetować hasło.
    -    Dodaj adres e-mail i nr telefonu do swojego konta, aby umożliwić innym użytkownikom odnalezienie ciebie.
    -\n
    -\nAdres e-mail pozwoli Ci również zresetować hasło.
         Rejestracja jednocześnie za pomocą numeru telefonu i adresu e-mail nie jest obsługiwana dopóki nie pojawi się odpowiednie API. Tylko numer telefonu będzie brany pod uwagę.
     
     Możesz dodać adres e-mail do swojego profilu w ustawieniach.
    diff --git a/vector/src/main/res/values-pt-rBR/strings.xml b/vector/src/main/res/values-pt-rBR/strings.xml
    index e1937505db..04367b99a0 100644
    --- a/vector/src/main/res/values-pt-rBR/strings.xml
    +++ b/vector/src/main/res/values-pt-rBR/strings.xml
    @@ -140,10 +140,6 @@
         Senha
         Nova senha
         Nome de usuário
    -    "Adicione um endereço de e-mail à sua conta para permitir que outras pessoas achem você, e para que seja possível você redefinir sua senha."
    -    "Adicione um número de telefone à sua conta para permitir que outras pessoas achem você."
    -    "Adicione um endereço de e-mail e/ou um número de telefone à sua conta para que outras pessoas possam encontrar você.\n\nO e-mail também permite que você possa redefinir sua senha."
    -    "Adicione um endereço de e-mail e um número de telefone à sua conta para que outras pessoas possam encontrar você.\n\nO e-mail também permite que você possa redefinir sua senha."
         Endereço de email
         Endereço de email (opcional)
         Número de telefone
    diff --git a/vector/src/main/res/values-pt/strings.xml b/vector/src/main/res/values-pt/strings.xml
    index 80ad31888e..3508d1fd9b 100755
    --- a/vector/src/main/res/values-pt/strings.xml
    +++ b/vector/src/main/res/values-pt/strings.xml
    @@ -139,14 +139,6 @@
         Palavra-passe
         Nova palavra-passe
         Nome de utilizador
    -    Adicione um endereço de e-mail à sua conta para permitir que outros utlizadores o encontrem, assim como para que seja possível redefinir a sua palavra-passe.
    -    Adicione um número de telefone à sua conta para permitir que outros utilizadores o encontrem.
    -    Adicione um endereço de e-mail e/ou um número de telefone à sua conta para permitir que outros utilizadores o encontrem.
    -
    -O endereço de e-mail também lhe permitirá redefinir sua senha.
    -    Adicione um endereço de e-mail e um número de telefone à sua conta para permitir que outros utilizadores o encontrem.
    -
    -O endereço de e-mail também lhe permitirá redefinir sua senha.
         Endereço de e-mail
         Endereço de email (opcional)
         Número de telefone
    diff --git a/vector/src/main/res/values-ru/strings.xml b/vector/src/main/res/values-ru/strings.xml
    index f62a760a9c..145bb548c8 100644
    --- a/vector/src/main/res/values-ru/strings.xml
    +++ b/vector/src/main/res/values-ru/strings.xml
    @@ -142,14 +142,6 @@
         Пароль
         Новый пароль
         Логин
    -    Добавьте адрес электронной почты к вашему аккаунту, чтобы другие пользователи могли найти вас, а также, чтобы иметь возможность сбросить пароль.
    -    "Добавьте телефон к вашему аккаунту, чтобы другие пользователи могли найти вас."
    -    Добавьте электронную почту и/или телефон к вашему аккаунту, чтобы другие пользователи могли найти вас.
    -
    -Электронная почта также позволит вам при необходимости восстановить пароль.
    -    Добавьте email и телефон к вашему аккаунту, чтобы другие пользователи могли найти вас.
    -
    -Email также позволит вам при необходимости восстановить пароль.
         Адрес электронной почты
         Адрес электронной почты (не обязательно)
         Номер телефона
    @@ -454,7 +446,7 @@ Email также позволит вам при необходимости во
         Синхронизация
         Включить фоновую синхронизацию
         Таймаут синхронизации
    -    Задержка между запросами
    +    Задержка между каждой синхронизацией
         секунда
         секунд
     
    @@ -482,10 +474,10 @@ Email также позволит вам при необходимости во
         Прикрепить комнаты с отключенными уведомлениями
         Прикрепить комнаты с непрочитанными сообщениями
         Устройства
    -    Подробности о устройстве
    +    Информация об устройстве
         ID
    -    Имя
    -    Имя устройства
    +    Общеизвестное имя
    +    Обновить публичное имя
         Последнее подключение
         %1$s @ %2$s
         Для этой операции требуется дополнительная проверка подлинности.
    @@ -619,9 +611,9 @@ Email также позволит вам при необходимости во
         Ошибка дешифровки
     
         Информация об устройстве отправителя
    -    Имя устройства
    -    Имя
    -    ID устройства
    +    Публичное имя
    +    Публичное имя
    +    ID
         Ключ устройства
         Проверка
         Ed25519 отпечаток
    @@ -1593,7 +1585,7 @@ Email также позволит вам при необходимости во
     
         Удаленное пользователем событие
         Мероприятие, модерируемое администратором помещения
    -    Последний раз редактировался %s на %s
    +    Последний раз редактировался %1$s на %2$s
     
     
         Некорректное событие, не может быть отображено
    @@ -1637,7 +1629,7 @@ Email также позволит вам при необходимости во
         Установки
         Безопасность & Конфиденциальность
         Профессионал
    -    Нажать Правила
    +    Пуш-Правила
         app_id:
         push_key:
         app_display_name:
    @@ -1661,15 +1653,15 @@ Email также позволит вам при необходимости во
     
         RiotX - Matrix клиент следующего поколения
         Быстрый и легкий клиент для Matrix с новейшими фреймворками Android
    -    RiotX - это новый клиент для матричного протокола (Matrix.org): открытой сети для безопасной децентрализованной связи. RiotX - это полная перезапись Riot Android клиента, основанная на полной перезаписи Matrix Android SDK.
    -\n
    -\nОговорка: Это бета-версия. В настоящее время RiotX находится в активной разработке и содержит ограничения и (надеемся, не слишком много) ошибки. Мы будем рады любым отзывам!
    -\n
    -\nПоддержка RiotX: - Войти в существующую учетную запись - Создать комнату и присоединиться к общей комнате - Принять и отклонить приглашения - Список комнат пользователей - Просмотр сведений о комнате - Отправить текстовые сообщения - Отправить вложение - Читать и писать сообщения в зашифрованных комнатах - Криптографически: Резервное копирование клавиш E2E, предварительная проверка устройства, запрос и ответ на общий доступ к ключам - Нажмите уведомление - Светлые, темные и черные темы
    -\n
    -\nНе все функции RiotX пока реализованы в RiotX. Основные отсутствующие (и скоро появятся!) особенности: - Создание учетной записи - Настройки комнат (список членов комнат и т.д.) - Создание прямых чатов - Вызовы - Виджеты - .…
    +    RiotX - это новый клиент для матричного протокола (Matrix.org): открытой сети для безопасной децентрализованной связи. RiotX - это полная перезапись Riot Android клиента, основанная на полной перезаписи Matrix Android SDK. 
    +\n 
    +\nОговорка: Это бета-версия. В настоящее время RiotX находится в активной разработке и содержит ограничения и (надеемся, не слишком много) ошибки. Мы будем рады любым отзывам! 
    +\n 
    +\nПоддержка RiotX: - Войти в существующую учетную запись - Создать комнату и присоединиться к общедоступным комнатам - Принять и отклонить приглашения - Список комнат пользователей - Просмотр сведений о комнате - Отправить текстовые сообщения - Отправить вложение - Читать и писать сообщения в зашифрованных комнатах - Криптография: Резервное копирование клавиш E2E, предварительная проверка устройства, запрос и ответ на общий доступ к ключам - Нажмите уведомление - Светлые и черные темы 
    +\n 
    +\nНе все функции Riot пока реализованы в RiotX. Основные отсутствующие (и скоро появятся!) свойства: - Создание учетной записи - Настройки комнат (список членов комнат и т.д.) - Вызовы - Виджеты - .…
     
    -    Предварительный просмотр открытой комнаты в RiotX пока не поддерживается.
    +    Предварительный просмотр открытой комнаты в RiotX пока не поддерживается
     
         Прямые сообщения
     
    @@ -1699,11 +1691,11 @@ Email также позволит вам при необходимости во
     
         Имя или ID (#example:matrix.org)
     
    -    Включить свайп, для ответа в хронологии
    +    Включить свайп, для ответа по хронологии
     
         Ссылка скопирована в буфер обмена
     
    -    Добавить по matrix ID
    +    Добавить по Matrix ID
         Создание комнаты…
         "Результат не найден, используйте добавить matrix ID  для поиска на сервере."
         Начните печатать, чтобы получить результат
    @@ -1713,4 +1705,91 @@ Email также позволит вам при необходимости во
     
         Просмотреть историю изменений
     
    +    Обзор
    +    Отклонить
    +
    +    Для продолжения Вам необходимо принять Условия данного сервиса.
    +
    +    В предыдущих версиях Riot была ошибка безопасности, которая могла дать вашему серверу идентификации (%1$s) доступ к вашей учетной записи. Если вы доверяете %2$s, вы можете проигнорировать это; в противном случае, пожалуйста, выйдите из системы и войдите снова.
    +\n
    +\nПодробнее об этом читайте здесь:
    +\nhttps://medium.com/@RiotChat/36b4792ea0d6
    +
    +    Пуш-правила не определены
    +    Нет зарегистрированных пуш-шлюзов
    +
    +    Условия предоставления услуг
    +    Условия просмотра
    +    Быть доступным для других
    +    Используйте ботов, мосты, виджеты и стикеры
    +
    +    Читать в
    +
    +    Никто
    +    Отмена
    +    Отключить
    +    Сервер идентификации не настроен.
    +
    +    Звонок не состоялся из-за неправильно настроенного сервера
    +    Попросите администратора вашего домашнего сервера (%1$s) настроить TURN сервер, чтобы звонки работали надежно.
    +\n
    +\nКроме того, вы можете попробовать использовать публичный сервер по %2$s, но это будет не так надежно, и он предоставит ваш IP-адрес этому серверу. Вы также можете управлять этим в настройках.
    +    Попробуйте использовать %s
    +    Не спрашивай меня больше
    +
    +    Установите адрес электронной почты для восстановления учетной записи, и позже она может будет найдена участниками, которые вас знают.
    +    Установите телефон, и позже его могут опционально обнаруживать люди, которые вас знают.
    +    Установите адрес электронной почты для восстановления аккаунта. Позже используйте электронную почту или телефон, чтобы их могли найти люди, которые вас знают.
    +    Установите адрес электронной почты для восстановления аккаунта. Позже используйте электронную почту или телефон, чтобы их могли найти люди, которые вас знают.
    +    Не удается связаться с домашним сервером по этому URL, пожалуйста, проверьте его
    +    Разрешить резервный сервер помощи при вызове
    +    Оптимизирован для батареи
    +    Оптимизирован для работы в реальном времени
    +    Без фоновой синхронизации
    +    Не удалось обновить настройки.
    +
    +
    +    Предпочтительный интервал синхронизации
    +    Обнаружение
    +    Будет использовать%s в качестве помощника, если ваш домашний сервер не предлагает его (ваш IP-адрес будет доступен во время разговора)
    +    Добавьте идентификационный сервер в свои настройки, чтобы выполнить это действие.
    +    Режим фоновой синхронизации (Экспериментальный)
    +    Riot будет синхронизироваться в фоновом режиме таким образом, чтобы сохранить ограниченные ресурсы устройства (батарея).
    +\nВ зависимости от состояния ресурса вашего устройства, синхронизация может быть отложена операционной системой.
    +    Riot будет синхронизироваться в фоновом режиме периодически в точное время (настраивается).
    +\nЭто повлияет на использование радио и батареи, появится постоянное уведомление о том, что Riot прислушивается к событиям.
    +    Вы не будете уведомлены о входящих сообщениях, когда приложение находится в фоновом режиме.
    +    %s
    +\nСинхронизация может быть отложена в зависимости от ресурсов (батареи) или состояния устройства (спящий режим).
    +    Управляйте настройками обнаружения.
    +    Публичное имя (видимое для людей, с которыми вы общаетесь)
    +    Публичное имя устройства видны людям, с которыми вы общаетесь
    +    Вы не используете какой-либо сервер идентификации
    +    Идентификационный сервер не настроен, требуется сброс пароля.
    +
    +    Похоже, вы пытаетесь подключиться к другому домашнему серверу. Вы хотите выйти\?
    +
    +    Сервер идентификации
    +    Отключить идентификационный сервер
    +    Настроить идентификационный сервер
    +    Изменить идентификационный сервер
    +    В настоящее время вы используете %1$s для обнаружения и быть найденным вашими контактами.
    +    "Вы в настоящее время не используете идентификационный сервер. Чтобы обнаружить и быть найденным  вашими существующими контактами, настройте один из них ниже."
    +    Видимые адреса электронной почты
    +    Доступные номера телефонов
    +    В ожидании
    +
    +    Введите новый сервер идентификации
    +    Не удалось подключиться к серверу идентификации
    +    Пожалуйста, введите URL сервера идентификации
    +    Сервер идентификации не имеет условий предоставления услуг
    +    Параметры обнаружения появятся после добавления электронной почты.
    +    Параметры поиска появятся после добавления номера телефона.
    +    Отключение от сервера идентификации будет означать, что другие пользователи не смогут обнаружить вас, и вы не сможете приглашать других по электронной почте или по телефону.
    +    Мы отправили вам электронное письмо с подтверждением на %s, проверьте вашу электронную почту и нажмите на ссылку для подтверждения
    +    Выбранный сервер идентификации не имеет условий обслуживания. Продолжить, только если вы доверяете владельцу службы
    +    Текстовое сообщение отправлено %s. Введите код проверки, который он содержит.
    +
    +    В настоящее время вы делитесь адресами электронной почты или телефонными номерами на сервере идентификации %s. Вам нужно повторно подключиться к %s, чтобы прекратить делиться ими.
    +    Примите Условия обслуживания сервера идентификации (%s), чтобы разрешить обнаружение по адресу электронной почты или номеру телефона.
     
    diff --git a/vector/src/main/res/values-sk/strings.xml b/vector/src/main/res/values-sk/strings.xml
    index 44653d2aa7..5dd0fcc0da 100644
    --- a/vector/src/main/res/values-sk/strings.xml
    +++ b/vector/src/main/res/values-sk/strings.xml
    @@ -140,14 +140,6 @@
         Heslo
         Nové heslo
         Používateľské meno
    -    Pridajte si k účtu emailovú adresu, aby vás ostatní ľahšie mohli nájsť, alebo aby ste si mohli obnoviť zabudnuté heslo.
    -    Pridajte si k účtu telefónne číslo, aby vás ostatní mohli ľahšie nájsť.
    -    Pridajte si k účtu emailovú adresu a/alebo číslo telefónu,aby vás ostatní mohli ľahšie nájsť.
    -
    -Emailová adresa vám tiež umožní obnoviť si zabudnuté heslo.
    -    Pridajte si k účtu emailovú adresu a číslo telefónu,aby vás ostatní mohli ľahšie nájsť.
    -
    -Emailová adresa vám tiež umožní obnoviť si zabudnuté heslo.
         Emailová adresa
         Emailová adresa (nepovinné)
         Číslo telefónu
    diff --git a/vector/src/main/res/values-sq/strings.xml b/vector/src/main/res/values-sq/strings.xml
    index 0b6381aa6f..ea8a599718 100644
    --- a/vector/src/main/res/values-sq/strings.xml
    +++ b/vector/src/main/res/values-sq/strings.xml
    @@ -138,7 +138,6 @@
         Fjalëkalim
         Fjalëkalim i ri
         Emër përdoruesi
    -    Shtoni te llogaria juaj një numër telefoni, që përdoruesit të mund t’ju zbulojnë.
         Adresë email
         Adresë email (në daçi)
         Numër telefoni
    @@ -409,7 +408,7 @@
         Njëkohësim në prapaskenë
         Aktivizo njëkohësim në prapaskenë
         Mbarim kohe për kërkesë njëkohësimi
    -    Vonesë mes çdo kërkese
    +    Vonesë mes çdo Njëkohësimi
         sekondë
         sekonda
     
    @@ -460,10 +459,10 @@
     
         Mënyrë ruajtjeje të dhënash
     
    -    Hollësi pajisjeje
    +    Të dhëna pajisjeje
         ID
    -    Emër
    -    Emër Pajisjeje
    +    Emër Publik
    +    Përditësoni Emër Publik
         Parë së fundi më
         %1$s @ %2$s
         Mirëfilltësim
    @@ -573,9 +572,9 @@
         Gabim shfshehtëzimi
     
         Të dhëna pajisjeje dërguesi
    -    Emër pajisjeje
    -    Emër
    -    ID Pajisjeje
    +    Emër publik
    +    Emër publik
    +    ID
         Kyç pajisjeje
         Verifikim
         Shenja gishtash Ed25519
    @@ -745,13 +744,6 @@
         Dilni
         Dërgoni mesazh zanor
     
    -    Shtoni te llogaria juaj një adresë email, për t’u dhënë mundësinë përdoruesve t’ju zbulojnë dhe që t’ju lejojë të ricaktoni fjalëkalimin.
    -    Shtoni te llogaria juaj një adresë email dhe/ose një numër telefoni, për t’u dhënë mundësinë përdoruesve t’ju zbulojnë.
    -\n
    -\nAdresa email do t’ju lejojë edhe të ricaktoni fjalëkalimin tuaj.
    -    Shtoni te llogaria juaj një adresë email dhe një numër telefoni, për t’u dhënë mundësinë përdoruesve t’ju zbulojnë.
    -\n
    -\nAdresa email do t’ju lejojë edhe të ricaktoni fjalëkalimin tuaj.
         Regjistrimi me email dhe me numër telefoni njëherazi nuk mbulohet ende, deri sa të ketë API. Do të merret parasysh vetëm numri i telefonit.
     \n
     \nEmail-in tuaj mund ta shtoni te profili juaj, te rregullimet.
    @@ -1456,7 +1448,7 @@ Që të garantoni se s’ju shpëton gjë, thjesht mbajeni të aktivizuar mekani
     
         Veprimtari e fshirë nga përdorues
         Veprimtari e moderuar nga përgjegjës dhome
    -    Përpunuar së fundi nga %s më %s
    +    Përpunuar së fundi nga %1$s më %2$s
     
     
         Veprimtari e keqformuar, s’mund të shfaqet
    @@ -1529,7 +1521,7 @@ Që të garantoni se s’ju shpëton gjë, thjesht mbajeni të aktivizuar mekani
     \n
     \nRiotX-i mbulon: • Hyrje në një llogari ekzistuese • Krijim dhome dhe pjesëmarrje në dhoma publike • Pranim dhe hedhje poshtë ftesash • Njohje të dhomave të përdoruesve • Parje hollësish dhome • Dërgim mesazhesh tekst • Dërgim bashkëngjitjesh • Lexim dhe shkrim mesazhesh në dhoma të fshehtëzuara • Kriptografi: kopjeruajtje kyçesh E2E, verifikim i thelluar pajisjesh, kërkesa dhe përgjigje për ndarje kyçesh • Njoftime push • Tema të Çelëta, të Errëta dhe të Zeza
     \n
    -\nNë RiotX s’janë sendërtuar ende krejt veçoritë e Riot-it. Veçori kryesore që mungojnë (dhe që do të vijnë së shpejti!): • Krijim llogarish • Rregullime dhome (shfaqje anëtarësh dhome, etj.) • Krijim dhomash fjalosjeje të drejtpërdrejtë • Thirrje • Widget-es • …
    +\nNë RiotX s’janë sendërtuar ende krejt veçoritë e Riot-it. Veçori kryesore që mungojnë (dhe që do të vijnë së shpejti!): • Krijim llogarish • Rregullime dhome (shfaqje anëtarësh dhome, etj.) • Thirrje • Widget-es • …
     
         Përgjegjës Integrimesh
     
    @@ -1578,4 +1570,88 @@ Që të garantoni se s’ju shpëton gjë, thjesht mbajeni të aktivizuar mekani
     
         Shihni Historik Përpunimesh
     
    +    Shqyrtojeni
    +    Hidhe poshtw
    +
    +    Që të vazhdohet, lypset të pranoni Kushtet e këtij shërbimi.
    +
    +    Versione të dikurshëm të Riot-it përmbanin një të metë sigurie, e cila mund t’i lejonte Shërbyesit tuaj të Identiteteve (%1$s) hyrje në llogarinë tuaj. Nëse i besoni %2$s, mund ta shpërfillni këtë sinjalizim; përndryshe dilni nga llogaria dhe ribëni hyrjen.
    +\n
    +\nLexoni më tepër hollësi këtu:
    +\nhttps://medium.com/@RiotChat/36b4792ea0d6
    +
    +    Kushte Shërbimi
    +    Shqyrtoni Kushtet
    +    Jini i zbulueshëm nga të tjerët
    +    Përdorni robotë, ura, widget-e dhe paketa ngjitësish
    +
    +    Lexoni te
    +
    +    Asnjë
    +    Shfuqizoje
    +    Shkëputu
    +    S’ka shërbyes identitetesh të formësuar.
    +
    +    Thirrja dështoi për shkak shërbyesi të keqformësuar
    +    Që thirrjet të funksionojnë në mënyrë të qëndrueshme, ju lutemi, kërkojini përgjegjësit të shërbyesit tuaj Home (%1$s) të formësojë një shërbyes TURN.
    +\n
    +\nNdryshe, mund të provoni të përdorni shërbyesin publik te %2$s, por kjo s’do të jetë edhe aq e qëndrueshme, dhe adresa juaj IP do të jetë e ditur për atë shërbyes. Këtë mund ta administroni edhe që nga Rregullimet.
    +    Provoni të përdorni %s
    +    Mos më pyet sërish
    +
    +    Caktoni një email për rimarrje llogarie, dhe më vonë të jetë i zbulueshëm (opsionale) nga persona që ju njohin.
    +    Caktoni një telefon, dhe më vonë të jetë i zbulueshëm (opsionale) nga persona që ju njohin.
    +    Caktoni një email për rimarrje llogarie. Përdoni email ose telefon që të jeni i zbulueshëm (opsionale) nga persona që ju njohin.
    +    Caktoni një email për rimarrje llogarie. Përdorni email ose telefon që të jeni i zbulueshëm (opsionale) nga persona që ju njohin.
    +    S’kapet dot shërbyes Home te kjo URL, ju lutemi, kontrollojeni
    +    Do të përdoret %s si ndihmë kur shërbyesi juaj Home nuk ofron të tillë (gjatë thirrjes, adresa juaj IP do të ndahet me të tjerë)
    +    Që të kryhet ky veprim, shtoni një shërbyes identitetesh, që nga rregullimet tuaja.
    +    Mënyrë Njëkohësimi Në Prapaskenë (Eksperimentale)
    +    E optimizuar për baterinë
    +    Riot-i do të bëjë njëkohësim në prapaskenë, në një mënyrë që kursen burimet e kufizuara të pajisjes (baterinë).
    +\nNë varësi të gjendjes së burimeve tuaja, njëkohësimi mund të shtyhet për më vonë nga sistemi operativ.
    +    I optimizuar për kohë të njëmendtë
    +    Riot0-i do të bëjë njëkohësim në prapaskenë periodikisht në një kohë të caktuar (e formësueshme).
    +\nKjo do të ketë ndikim mbi përdorimin e baterisë dhe të transmetimit, do të shfaqet një njoftim i pandërprerë që pohon se Riot-i po përgjon për akte.
    +    Pa njëkohësim në prapraskenë
    +    S’do të njoftoheni për mesazhe ardhës, kur aplikacioni gjendet në prapaskenë.
    +    S’u arrit të përditësohen rregullime.
    +
    +
    +    Interval i Parapëlqyer Njëkohësimesh
    +    %s
    +\nNjëkohësimi mund të shtyhet për më vonë, në varësi të burimeve (baterisë) ose gjendjes së pajisjes (dremitje).
    +    Administroni rregullimet tuaja për zbulime.
    +    Emër publik (i dukshëm për persona me të cilët komunikoni)
    +    Emri publik i një pajisjeje është i dukshëm për persona me të cilët komunikoni
    +    S’po përdorni ndonjë Shërbyes Identitetesh
    +    S’ka shërbyes identitetesh të formësuar, kjo është e domosdoshme për ricaktimin e fjalëkalimit tuaj.
    +
    +    Duket se po rrekeni të lidheni me një tjetër shërbyes Home. Doni të bëhet dalja\?
    +
    +    Që të përgjigjeni te rrjedha kohore, aktivizoni fërkimin
    +
    +    Shërbyes identitetesh
    +    Shkëpute shërbyesin e identiteteve
    +    Formësoni shërbyes identitetesh
    +    Ndryshoni shërbyes identitetesh
    +    Po përdorni %1$s për të zbuluar dhe për të qenë i zbulueshëm nga kontakte ekzistues që njihni.
    +    S’po përdorni ndonjë shërbyes identitetesh. Që të zbuloni dhe të jini i zbulueshëm nga kontakte ekzistuese që njihni, formësoni një të tillë më poshtë.
    +    Adresa email të zbulueshme
    +    Mundësitë rreth zbulimesh do të shfaqen sapo të keni shtuar një email.
    +    Mundësi zbulimesh do të shfaqen sapo të keni shtuar një numër telefoni.
    +    Shkëputja prej shërbyesit tuaj të identiteteve do të thotë se s’do të jeni i zbulueshëm prej përdoruesish të tjerë dhe s’do të jeni në gjendje të ftoni të tjerë me email ose telefon.
    +    Numra telefoni të zbulueshëm
    +    Ju dërguam një email ripohimi te %s, hapeni dhe klikoni mbi lidhjen e ripohimit
    +    Pezull
    +
    +    Jepni një shërbyes të ri identitetesh
    +    S’u lidh dot te shërbyes identitetesh
    +    Ju lutemi, jepni URL-në e shërbyesit të identiteteve
    +    Shërbyesi i identiteteve s’ka kushte shërbimi
    +    Shërbyesi i identiteteve që keni zgjedhur nuk ka ndonjë kusht shërbimi. Vazhdoni vetëm nëse i zini besë të zotit të shërbimit
    +    Te %s u dërgua një mesazh tekst. Ju lutemi, jepni kodin e verifikimit që përmban ai.
    +
    +    Hëpërhë, ndani me të tjerë adresa email ose numra telefoni te shërbyesi i identiteteve %s. Do të duhet të rilidheni me %s që të ndalni ndarjen e tyre.
    +    Që të lejoni veten të jetë e zbulueshme nga adresë email apo numër telefoni, pajtohuni me Kushtet e Shërbimit të shërbyesit të identiteteve (%s).
     
    diff --git a/vector/src/main/res/values-te/strings.xml b/vector/src/main/res/values-te/strings.xml
    index 797a6ed147..df79f70824 100644
    --- a/vector/src/main/res/values-te/strings.xml
    +++ b/vector/src/main/res/values-te/strings.xml
    @@ -252,7 +252,6 @@
         సందేశాలు పంపబడలేదు. ఇప్పుడు %1$s లేదా %2$s?
         తెలియని పరికరాల కారణంగా ఉన్న సందేశాలు పంపబడలేదు. ఇప్పుడు %1$s లేదా %2$s?
         అన్నీ మళ్లీ పంపు
    -    వాడుకరులు మిమ్మల్ని కనుగొనడానికి మీ ఖాతాకు ఒక ఫోన్ నంబర్ను జోడించండి.
         మ్యాట్రిక్స్ వాడుకరులు మాత్రమే
         అన్నింటినీ రద్దు చేయండి
         పంపని సందేశాలను తిరిగి పంపండి
    @@ -460,7 +459,6 @@
     
         రీసెట్ ఈమెయిల్ పంపండి
         లాగిన్ తెరకి తిరిగి వెళ్లండి
    -    ఒక ఈమెయిల్ చిరునామా ని జతచేస్తే వేరేవాళ్లు మిమ్మల్ని కనుక్కోవచ్చు, మరియు గుప్తపదాన్ని మార్చుకోవచ్చు
         విశ్వాన్వేషణ
         అటు వైపు ఎవరు కాల్ ఎత్తలేదు
         కాల్ ఎక్కడో ఎత్తారు
    @@ -480,12 +478,6 @@
         బగ్ నివేదిక విజయవంతంగా పంపబడింది
         లోపపు నివేదిక పంపబడదు (%s)
         గుర్తించిన సేవిక యు ఆర్ ఎల్
    -    వినియోగదారులు మిమ్మల్ని కనుగొనడానికి మిమ్మల్ని అనుమతించడానికి మీ ఖాతాకు ఇమెయిల్ చిరునామా మరియు / లేదా ఫోన్ నంబర్ను జోడించండి.
    -\n
    -\nఇమెయిల్ చిరునామా మీ పాస్ వర్డ్ ను మళ్ళీ రీసెట్ చేస్తుంది.
    -    వినియోగదారులు మిమ్మల్ని కనుగొనడానికి మిమ్మల్ని అనుమతించడానికి మీ ఖాతాకు ఇమెయిల్ చిరునామా మరియు / లేదా ఫోన్ నంబర్ను జోడించండి.
    -\n
    -\nఇమెయిల్ చిరునామా మీ పాస్ వర్డ్ ను మళ్ళీ రీసెట్ చేస్తుంది.
         కస్టమ్ సర్వర్ ఎంపికలు (ఆధునిక) ఉపయోగించండి
         %s కు ఒక ఇ-తపాలా పంపబడింది. మీరు కలిగి ఉన్న లింగికను మీరు అనుసరించిన తర్వాత, క్రింద నొక్కండి .
         "మీ సాంకేతిక పదము  పునః ప్రారంభం చెయ్యబడింది.
    diff --git a/vector/src/main/res/values-tr/strings.xml b/vector/src/main/res/values-tr/strings.xml
    index 3131121473..58256833c9 100644
    --- a/vector/src/main/res/values-tr/strings.xml
    +++ b/vector/src/main/res/values-tr/strings.xml
    @@ -176,8 +176,6 @@
         Şifre
         Yeni şifre
         Kullanıcı adı
    -    Kullanıcıların seni rahatça bulabilmesi ve şifreni yeniden oluşturabilmen için bir e-posta hesabı ekle.
    -    Kullanıcıların seni rahatça bulabilmesi için bir telefon numarası ekle.
         Status.im Teması
     
         Anahtar Yedekleme
    @@ -233,12 +231,6 @@ Eğer yeni kurtarma yöntemini siz ayarlamadıysanız, bir saldırgan hesabını
     
         Gönder
         Kimlik Sunucusu URL\'si
    -    Kullanıcıların sizi bulmasını sağlamak için hesabınıza bir e-posta adresi ve/veya telefon numarası ekleyin.
    -
    -Ayrıca e-posta adresi, şifrenizi sıfırlamanıza da olanak tanır.
    -    Kullanıcıların sizi bulmasını sağlamak için hesabınıza bir e-posta adresi ve/veya telefon numarası ekleyin.
    -
    -Ayrıca e-posta adresi, şifrenizi sıfırlamanıza da olanak tanır.
         E-posta adresi
         E-posta adresi (isteğe bağlı)
         Telefon numarası
    diff --git a/vector/src/main/res/values-uk/strings.xml b/vector/src/main/res/values-uk/strings.xml
    index 565aef07f2..c5f2dc7022 100755
    --- a/vector/src/main/res/values-uk/strings.xml
    +++ b/vector/src/main/res/values-uk/strings.xml
    @@ -154,10 +154,6 @@
         Пароль
         Новий пароль
         Логін
    -    "Додайте email адресу, щоб дати користувачам знайти себе та отримати змогу скинути свій пароль."
    -    "Додайте номер телефону, щоб інші користувачі могли вас знайти."
    -    "Додайте email адресу та/або номер телефону до вашого облікового запису, щоб інші користувачі могли вас знайти.\n\nEmail адреса також дасть вам змогу скидати пароль."
    -    "Додайте email адресу та номер телефону до вашого облікового запису, щоб інші користувачі могли вас знайти.\n\nEmail адреса також дасть вам змогу скидати пароль."
         Email адреса
         Email адреса (oдодаткова)
         Номер телефону
    diff --git a/vector/src/main/res/values-zh-rCN/strings.xml b/vector/src/main/res/values-zh-rCN/strings.xml
    index 32610986c1..0a4da5b085 100644
    --- a/vector/src/main/res/values-zh-rCN/strings.xml
    +++ b/vector/src/main/res/values-zh-rCN/strings.xml
    @@ -204,14 +204,6 @@
     
         拍摄照片或视频
     
    -    为您的账户添加一个电子邮箱地址以便其他用户发现您,同时可让您重置密码。
    -    为您的账户添加一个手机号码以便其他用户发现您。
    -    添加一个电子邮箱地址和/或手机号码到您的账户来让用户发现您。
    -
    -电子邮箱地址也可以让您重设您的密码。
    -    添加一个电子邮箱地址和手机号码到您的账户来让用户发现您。
    -
    -电子邮箱地址也可以让您重设您的密码。
         重复密码
         使用自定义服务器选项(高级)
         请检查您的电子邮箱以继续注册
    diff --git a/vector/src/main/res/values-zh-rTW/strings.xml b/vector/src/main/res/values-zh-rTW/strings.xml
    index 332e7a82a1..cd81aea785 100644
    --- a/vector/src/main/res/values-zh-rTW/strings.xml
    +++ b/vector/src/main/res/values-zh-rTW/strings.xml
    @@ -159,14 +159,6 @@
         密碼
         新密碼
         使用者名稱
    -    輸入電子郵件位址來讓其他人找到您,以及方便您重設密碼。
    -    新增電話號碼來讓其他人找到您。
    -    新增電子郵件位址或電話號碼來讓其他人找到您。
    -
    -電子郵件位址也可以用來重設密碼。
    -    新增電子郵件位址或電話號碼來讓其他人找到您。
    -
    -電子郵件位址也可以用來重設密碼。
         電子郵件地址
         電子郵件位址(選擇性)
         電話號碼
    @@ -520,7 +512,7 @@
         後臺同步
         開啓後臺同步
         同步請求超時
    -    每次同步請求的延遲
    +    每次同步間的延遲
         
         
     
    @@ -569,10 +561,10 @@
     
         節省流量模式
     
    -    裝置資料
    +    裝置資訊
         ID
    -    名稱
    -    裝置名稱
    +    公開名稱
    +    更新公開名稱
         上次上線
         %1$s @ %2$s
         此操作需要額外的身份驗證。
    @@ -711,9 +703,9 @@
         解密錯誤
     
         發送者的裝置訊息
    -    裝置名稱
    -    名稱
    -    裝置 ID
    +    公開名稱
    +    公開名稱
    +    ID
         裝置金鑰
         驗證
         Ed25519 指紋
    @@ -1451,7 +1443,7 @@ Matrix 中的消息可見度類似于電子郵件。我們忘記您的郵件意
     
         由使用者刪除的活動
         由聊天室管理員審核的活動
    -    最後編輯由 %s 於 %s
    +    最後編輯由 %1$s 於 %2$s
     
     
         活動格式錯誤,無法顯示
    @@ -1530,7 +1522,7 @@ Matrix 中的消息可見度類似于電子郵件。我們忘記您的郵件意
     \n
     \nRiotX 支援:• 登入到既有的帳號 • 建立聊天室與加入公開聊天室 • 接受與回絕邀請 • 列出使用者聊天室 • 檢視聊天室詳細資訊 • 傳送文字訊息 • 傳送附件 • 讀取與編寫已加密的聊天室 • 加密:E2E 金鑰備份、進階裝置驗證、金鑰分享請求與回應 • 推送通知 • 亮、暗與黑色主題
     \n
    -\n不是所有 Riot 的功能都已在 RiotX 中實作。主要缺少(會在稍後到來!)的功能:• 建立帳號 • 聊天室設定(列出聊天室成員等) • 直接聊天室建立 • 通話 • 小工具 • …
    +\n不是所有 Riot 的功能都已在 RiotX 中實作。主要缺少(會在稍後到來!)的功能:• 建立帳號 • 聊天室設定(列出聊天室成員等) • 通話 • 小工具 • …
     
         直接訊息
     
    @@ -1577,4 +1569,88 @@ Matrix 中的消息可見度類似于電子郵件。我們忘記您的郵件意
     
         檢視編輯歷史
     
    +    審閱
    +    婉拒
    +
    +    要繼續,您必須接受此服務的條款。
    +
    +    先前版本的 Riot 有一個安全性問題,讓您的身份識別伺服器 (%1$s) 存取您的帳號。如果您信任 %2$s,您可以忽略這個;否則請登出再登入。
    +\n
    +\n在此閱讀更多詳細資訊:
    +\nhttps://medium.com/@RiotChat/36b4792ea0d6
    +
    +    服務條款
    +    審閱條款
    +    可被其他人探索
    +    使用機器人、橋接、小工具與貼紙包
    +
    +    閱讀於
    +
    +    
    +    撤銷
    +    斷線
    +    未設定身份識別伺服器。
    +
    +    因為錯誤設定的伺服器導致通話失敗
    +    請要求您家伺服器 (%1$s) 的管理員設定 TURN 伺服器以讓通話正常運作。
    +\n
    +\n或者您也可以試用看看位於 %2$s 的公開伺服器,但這可能不太可靠,而且也會將您的 IP 位置與伺服器分享。您也可以在設定中管理這個。
    +    嘗試使用 %s
    +    不要再問我
    +
    +    設定電子郵件以供帳號復原,然後就可以讓認識您的人選擇性探索到您。
    +    設定電話,然後就可以讓認識您的人選擇性探索到您。
    +    設定電子郵件以供帳號復原。然後就可以讓認識您的人用電子郵件或電話選擇性探索到您。
    +    設定電子郵件以供帳號復原。然後就可以讓認識您的人用電子郵件或電話選擇性探索到您。
    +    無法在此 URL 找到家伺服器,請檢查
    +    允許汰退呼叫協助伺服器
    +    當您的家伺服器未提供時,將會使用 %s 做為協助(您的 IP 位置將會在通話時被分享)
    +    在您的設定中新增一臺身份識別伺服器以執行此動作。
    +    背景同步模式(實驗性)
    +    為電池最佳化
    +    Riot 將會在背景同步以節省裝置的有限資源(電池)。
    +\n取決於您裝置的資源狀態,作業系統可能會延遲同步。
    +    為即時作業最佳化
    +    Riot 將會精準地定期在背景同步(可設定)。
    +\n這會影響到網路與電池的使用,並會顯示指出 Riot 正在監聽某事件的永久通知。
    +    無背景同步
    +    當應用程式在背景時,您將不會收到訊息通知。
    +    更新設定失敗。
    +
    +
    +    偏好同步間隔
    +    %s
    +\n取決於資源(電池)或裝置狀態(休眠),同步可能會被延遲。
    +    探索
    +    管理您的探索設定。
    +    公開名稱(您通訊的對象可見)
    +    裝置的公開名稱對您通訊的對象可見
    +    您未使用任何身份識別伺服器
    +    未設定身份識別伺服器,這在重設您的密碼時是必要的。
    +
    +    看起來您正在嘗試連線到其它家伺服器。您想要登出嗎?
    +
    +    身份識別伺服器
    +    取消連線到身份識別伺服器
    +    設定身份識別伺服器
    +    變更身份識別伺服器
    +    您正在使用 %1$s 來探索與被您認識的既有聯絡人探索。
    +    您目前並未使用身份識別伺服器。要探索與被您認識的聯絡人探索,請在下方設定一個。
    +    可探索的電子郵件地址
    +    在您新增電子郵件後,探索選項將會出現。
    +    在您新增電話號碼後,探索選項將會出現。
    +    與您的身份識別伺服器斷線代鰾您無法被其他使用者探索,且您將無法透過電子郵件或電話邀請其他人。
    +    可探索的電話號碼
    +    我們將會傳送確認電子郵件到 %s 給您,請檢查您的電子郵件並在確認連結上點選
    +    擱置中
    +
    +    輸入新的身份識別伺服器
    +    無法連線到身份識別伺服器
    +    請輸入身份識別伺服器 URL
    +    身份識別伺服器無服務條款
    +    您選擇的身份識別伺服器沒有任何服務條款。僅在您信任服務擁有者時才繼續
    +    文字訊息已傳送給 %s。請輸入其中包含的驗證碼。
    +
    +    您目前正在身份識別伺服器 %s 上分享電子郵件地址或電話號碼。您將必須重新連線到 %s 以停止分享它們。
    +    同意身份識別伺服器 (%s) 的服務條款以允許您被透過電子郵件地址或電話號碼探索。
     
    diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
    index a4b0a61104..8d5e211fbd 100644
    --- a/vector/src/main/res/values/strings.xml
    +++ b/vector/src/main/res/values/strings.xml
    @@ -5,6 +5,8 @@
     
         en
         US
    +    
    +    Latn
     
         
         Light Theme
    @@ -72,6 +74,9 @@
         View Decrypted Source
         Delete
         Rename
    +    None
    +    Revoke
    +    Disconnect
         Report content
         Active call
         
    @@ -147,6 +152,7 @@
         No conversations
         You didn’t allow Riot to access your local contacts
         No results
    +    No identity server configured.
     
         
         Rooms
    @@ -200,6 +206,10 @@
         Are you sure that you want to start a new chat with %s?
         Are you sure that you want to start a voice call?
         Are you sure that you want to start a video call?
    +    Call failed due to misconfigured server
    +    Please ask the administrator of your homeserver (%1$s) to configure a TURN server in order for calls to work reliably.\n\nAlternatively, you can try to use the public server at %2$s, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings."
    +    Try using %s
    +    Do not ask me again
     
         Send files
         Send sticker
    @@ -226,10 +236,10 @@
         Password
         New password
         User name
    -    "Add an email address to your account to let users discover you, and let you reset password."
    -    "Add a phone number to your account to let users discover you."
    -    "Add an email address and/or a phone number to your account to let users discover you.\n\nEmail address will also let you reset your password."
    -    "Add an email address and a phone number to your account to let users discover you.\n\nEmail address will also let you reset your password."
    +    "Set an email for account recovery, and later to be optionally discoverable by people who know you."
    +    "Set a phone, and later to be optionally discoverable by people who know you."
    +    "Set an email for account recovery. Use later email or phone to be optionally discoverable by people who know you."
    +    "Set an email for account recovery. Use later email or phone to be optionally discoverable by people who know you."
         Email address
         Email address (optional)
         Phone number
    @@ -274,6 +284,7 @@
         Unable to register : email ownership failure
         Please enter a valid URL
         This URL is not reachable, please check it
    +    Cannot reach a homeserver at this URL, please check it
         Your device is using an outdated TLS security protocol, vulnerable to attack, for your security you will not be able to connect
         Mobile
     
    @@ -334,6 +345,8 @@
         
         Calls
         Use default Riot ringtone for incoming calls
    +    Allow fallback call assist server
    +    Will use "%s" as assist when your home server does not offers one (your IP address will be shared during a call)
         Incoming call ringtone
         Select ringtone for calls:
     
    @@ -369,9 +382,6 @@
         Riot can check your address book to find other Matrix users based on their email and phone numbers. If you agree to share your address book for this purpose, please allow access on the next pop-up.
         Riot can check your address book to find other Matrix users based on their email and phone numbers.\n\nDo you agree to share your address book for this purpose?
     
    -
    -
    -
         Sorry. Action not performed, due to missing permissions
     
         
    @@ -399,7 +409,7 @@
         You are trying to access %s. Would you like to join in order to participate in the discussion?
         a room
         This is a preview of this room. Room interactions have been disabled.
    -
    +    Add an identity server in your settings to perform this action.
         
         New Chat
         Add member
    @@ -726,10 +736,22 @@
         Messages sent by bot
     
         Background synchronization
    +    Background Sync Mode (Experimental)
    +    Optimized for battery
    +    Riot will sync in background in way that preserves the device’s limited resources (battery).\nDepending on your device resource state, the sync may be deferred by the operating system.
    +    Optimized for real time
    +    Riot will sync in background periodically at precise time (configurable).\nThis will impact radio and battery usage, there will be a permanent notification displayed stating that riot is listening for events.
    +    No background sync
    +    You will not be notified of incoming messages when the app is in background.
    +    Failed to update settings.
    +
    +
         Start on boot
         Enable background sync
         Sync request timeout
    -    Delay between each request
    +    Preferred Sync Interval
    +    %s\nThe sync might be deferred depending on the resources (battery) or state of the device (sleep).
    +    Delay between each Sync
         second
         seconds
     
    @@ -779,8 +801,8 @@
     
         Deactivate account
         Deactivate my account
    -
    -    
    +    Discovery
    +    Manage your discovery settings.
         Notification Privacy
         Riot can run in the background to manage your notifications securely and privately. This might affect battery usage.
         Grant permission
    @@ -800,10 +822,10 @@
         Data save mode
         Data save mode applies a specific filter so presence updates and typing notifications are filtered out.
     
    -    Device details
    +    Device information
         ID
    -    Name
    -    Device Name
    +    Public Name
    +    Update Public Name
         Last seen
         %1$s @ %2$s
         This operation requires additional authentication.\nTo continue, please enter your password.
    @@ -968,9 +990,11 @@
         Decryption error
     
         Sender device information
    -    Device name
    -    Name
    -    Device ID
    +    Public name
    +    Public name (visible to people you communicate with)
    +    "A device's public name is visible to people you communicate with"
    +    Public name
    +    ID
         Device key
         Verification
         Ed25519 fingerprint
    @@ -1472,9 +1496,14 @@ Why choose Riot.im?
         User mismatch
         Unknown Error
     
    +    
    +    You are not using any Identity Server
    +    No identity server is configured, it is required to reset your password.
     
         "Previous versions of Riot had a security bug which could give your Identity Server (%1$s) access to your account. If you trust %2$s, you can ignore this; otherwise please logout and login again.\n\nRead more details here:\nhttps://medium.com/@RiotChat/36b4792ea0d6"
     
    +    It looks like you’re trying to connect to another homeserver. Do you want to sign out?
    +
         
         Edit
         Reply
    @@ -1502,7 +1531,7 @@ Why choose Riot.im?
     
         Event deleted by user
         Event moderated by room admin
    -    Last edited by %s on %s
    +    Last edited by %1$s on %2$s
     
     
         Malformed event, cannot display
    @@ -1599,7 +1628,6 @@ RiotX supports:
     Not all features in Riot are implemented in RiotX yet. Main missing (and coming soon!) features:
     • Account creation
     • Room settings (list room members, etc.)
    -• Creation of direct chat rooms
     • Calls
     • Widgets
     • …"
    @@ -1656,4 +1684,89 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
     
         Read at
     
    +
    +    Identity server
    +    Disconnect identity server
    +    Configure identity server
    +    Change identity server
    +    You are currently using %1$s to discover and be discoverable by existing contacts you know.
    +    You are not currently using an identity server. To discover and be discoverable by existing contacts you know, configure one below.
    +    Discoverable email addresses
    +    Discovery options will appear once you have added an email.
    +    Discovery options will appear once you have added a phone number.
    +    Disconnecting from your identity server will mean you won’t be discoverable by other users and you won’t be able to invite others by email or phone.
    +    Discoverable phone numbers
    +    We sent you a confirm email to %s, check your email and click on the confirmation link
    +    Pending
    +
    +    Enter a new identity server
    +    Could not connect to identity server
    +    Please enter the identity server url
    +    Identity server has no terms of services
    +    The identity server you have chosen does not have any terms of services. Only continue if you trust the owner of the service
    +    A text message has been sent to %s. Please enter the verification code it contains.
    +
    +    You are currently sharing email addresses or phone numbers on the identity server %s. You will need to reconnect to %s to stop sharing them.
    +    Agree to the identity server (%s) Terms of Service to allow yourself to be discoverable by email address or phone number.
    +
    +    Enable verbose logs.
    +    Verbose logs will help developers by providing more logs when you send a RageShake. Even when enabled, the application does not log message contents or any other private data.
    +
    +
    +    Please retry once you have accepted the terms and conditions of your homeserver.
    +
    +    Looks like the server is taking to long to respond, this can be caused by either poor connectivity or an error with our servers. Please try again in a while.
    +
    +    Send attachment
    +
    +    Open the navigation drawer
    +    Open the create room menu
    +    Close the create room menu…
    +    Create a new direct conversation
    +    Create a new room
    +    Close keys backup banner
    +    Show password
    +    Hide password
    +    Jump to bottom
    +
    +    
    +    %1$s, %2$s and %3$d others read
    +    %1$s, %2$s and %3$s read
    +    %1$s and %2$s read
    +    %s read
    +    
    +        1 user read
    +        %d users read
    +    
    +
    +    "The file '%1$s' (%2$s) is too large to upload. The limit is %3$s."
    +
    +    "An error occurred while retrieving the attachment."
    +    "File"
    +    "Contact"
    +    "Camera"
    +    "Audio"
    +    "Gallery"
    +    "Sticker"
    +    Couldn\'t handle share data
    +
    +    "It's spam"
    +    "It's inappropriate"
    +    "Custom report"
    +    "Report this content"
    +    "Reason for reporting this content"
    +    "REPORT"
    +    "BLOCK USER"
    +
    +    "Content reported"
    +    "This content was reported.\n\nIf you don't want to see any more content from this user, you can block him to hide his messages"
    +    "Reported as spam"
    +    "This content was reported as spam.\n\nIf you don't want to see any more content from this user, you can block him to hide his messages"
    +    "Reported as inappropriate"
    +    "This content was reported as inappropriate.\n\nIf you don't want to see any more content from this user, you can block him to hide his messages"
    +
    +    Riot needs permission to save your E2E keys on disk.\n\nPlease allow access on the next pop-up to be able to export your keys manually.
    +
    +    There is no network connection right now
    +
     
    diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml
    index 2c72e3031a..71fbc22acf 100644
    --- a/vector/src/main/res/values/strings_riotX.xml
    +++ b/vector/src/main/res/values/strings_riotX.xml
    @@ -2,65 +2,6 @@
     
     
         
    -    Enable verbose logs.
    -    Verbose logs will help developers by providing more logs when you send a RageShake. Even when enabled, the application does not log message contents or any other private data.
     
     
    -    Please retry once you have accepted the terms and conditions of your homeserver.
    -
    -    
    -    It looks like you’re trying to connect to another homeserver. Do you want to sign out?
    -
    -    Looks like the server is taking to long to respond, this can be caused by either poor connectivity or an error with our servers. Please try again in a while.
    -
    -    Send attachment
    -
    -    Open the navigation drawer
    -    Open the create room menu
    -    Close the create room menu…
    -    Create a new direct conversation
    -    Create a new room
    -    Close keys backup banner
    -    Show password
    -    Hide password
    -    Jump to bottom
    -
    -    
    -    %1$s, %2$s and %3$d others read
    -    %1$s, %2$s and %3$s read
    -    %1$s and %2$s read
    -    %s read
    -    
    -        1 user read
    -        %d users read
    -    
    -
    -    "The file '%1$s' (%2$s) is too large to upload. The limit is %3$s."
    -
    -    "An error occurred while retrieving the attachment."
    -    "File"
    -    "Contact"
    -    "Camera"
    -    "Audio"
    -    "Gallery"
    -    "Sticker"
    -    Couldn\'t handle share data
    -
    -    "It's spam"
    -    "It's inappropriate"
    -    "Custom report"
    -    "Report this content"
    -    "Reason for reporting this content"
    -    "REPORT"
    -    "BLOCK USER"
    -
    -    "Content reported"
    -    "This content was reported.\n\nIf you don't want to see any more content from this user, you can block him to hide his messages"
    -    "Reported as spam"
    -    "This content was reported as spam.\n\nIf you don't want to see any more content from this user, you can block him to hide his messages"
    -    "Reported as inappropriate"
    -    "This content was reported as inappropriate.\n\nIf you don't want to see any more content from this user, you can block him to hide his messages"
    -
    -    Riot needs permission to save your E2E keys on disk.\n\nPlease allow access on the next pop-up to be able to export your keys manually.
    -
     
    
    From bdee5e06875c6238b6c9bd5d639749fbcf8fe667 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Wed, 23 Oct 2019 15:17:21 +0200
    Subject: [PATCH 190/197] Fix warning on Strings
    
    ---
     vector/src/main/res/values-eu/strings.xml     | 2 +-
     vector/src/main/res/values-fi/strings.xml     | 2 +-
     vector/src/main/res/values-fr/strings.xml     | 2 +-
     vector/src/main/res/values-hu/strings.xml     | 2 +-
     vector/src/main/res/values-it/strings.xml     | 2 +-
     vector/src/main/res/values-ko/strings.xml     | 2 +-
     vector/src/main/res/values-ru/strings.xml     | 2 +-
     vector/src/main/res/values-sq/strings.xml     | 2 +-
     vector/src/main/res/values-zh-rTW/strings.xml | 2 +-
     vector/src/main/res/values/strings.xml        | 2 +-
     10 files changed, 10 insertions(+), 10 deletions(-)
    
    diff --git a/vector/src/main/res/values-eu/strings.xml b/vector/src/main/res/values-eu/strings.xml
    index 71b5a6ab30..9e02bf8103 100644
    --- a/vector/src/main/res/values-eu/strings.xml
    +++ b/vector/src/main/res/values-eu/strings.xml
    @@ -1695,6 +1695,6 @@ Abisua: Fitxategi hau ezabatu daiteke aplikazioa desinstalatzen bada.
         Hautatu duzun identitate-zerbitzariak ez du erabilera baldintzarik. Jarraitu soilik zerbitzuaren jabea fidagarritzat jotzen baduzu
         SMS mezu bat bidali zaizu %s zenbakira. Sartu hemen mezu horrek daukan egiaztatze-kodea.
     
    -    Orain e-mail helbideak edo telefono zenbakiak partekatzen dituzu %s zerbitzarian. %s zerbitzarira konektatu beharko zara partekatzeari uzteko.
    +    Orain e-mail helbideak edo telefono zenbakiak partekatzen dituzu %1$s zerbitzarian. %2$s zerbitzarira konektatu beharko zara partekatzeari uzteko.
         Onartu %s identitate-zerbitzariaren erabilera baldintzak besteek zu e-mail helbidea edo telefonoa erabiliz aurkitzea ahalbidetzeko.
     
    diff --git a/vector/src/main/res/values-fi/strings.xml b/vector/src/main/res/values-fi/strings.xml
    index 74680e0fb2..3b037a9ead 100644
    --- a/vector/src/main/res/values-fi/strings.xml
    +++ b/vector/src/main/res/values-fi/strings.xml
    @@ -1655,6 +1655,6 @@ Jotta et menetä mitään, automaattiset päivitykset kannattaa pitää käytös
         Syötä identiteettipalvelimen URL-osoite
         Identiteettipalvelimella ei ole käyttöehtoja
         Valitsemallasi identiteettipalvelimella ei ole käyttöehtoja. Jatka vain, jos luotat palvelun omistajaan
    -    Jaat sähköpostiosoitteita tai puhelinnumeroita identiteettipalvelimella %s. Sinun täytyy yhdistää uudelleen palvelimeen %s, jotta voit lopettaa niiden jakamisen.
    +    Jaat sähköpostiosoitteita tai puhelinnumeroita identiteettipalvelimella %1$s. Sinun täytyy yhdistää uudelleen palvelimeen %2$s, jotta voit lopettaa niiden jakamisen.
         Hyväksy identiteettipalvelimen (%s) käyttöehdot salliaksesi, että sinut voi löytää sähköpostiosoitteen tai puhelinnumeron perusteella.
     
    diff --git a/vector/src/main/res/values-fr/strings.xml b/vector/src/main/res/values-fr/strings.xml
    index 3ca4f03934..1fe493d9d4 100644
    --- a/vector/src/main/res/values-fr/strings.xml
    +++ b/vector/src/main/res/values-fr/strings.xml
    @@ -1699,6 +1699,6 @@ Si vous n’avez pas configuré de nouvelle méthode de récupération, un attaq
         Le serveur d’identité qui vous avez choisi n’a pas de conditions de service. Continuez uniquement si vous faites confiance au propriétaire de ce service
         Un SMS a été envoyé à %s. Saisissez le code de vérification qu’il contient.
     
    -    Vous partagez actuellement des adresse e-mails et des numéros de téléphone sur le serveur d’identité %s. Vous devrez vous reconnecter à %s pour arrêter de les partager.
    +    Vous partagez actuellement des adresse e-mails et des numéros de téléphone sur le serveur d’identité %1$s. Vous devrez vous reconnecter à %2$s pour arrêter de les partager.
         Acceptez les conditions de service du serveur d’identité (%s) pour vous permettre d’être découvrable avec une adresse e-mail ou un numéro de téléphone.
     
    diff --git a/vector/src/main/res/values-hu/strings.xml b/vector/src/main/res/values-hu/strings.xml
    index 255887c340..2e13eb7531 100644
    --- a/vector/src/main/res/values-hu/strings.xml
    +++ b/vector/src/main/res/values-hu/strings.xml
    @@ -1698,6 +1698,6 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró
         Az általad választott azonosítási szervernek nincs felhasználási feltétele. Csak akkor lépj tovább ha megbízol a szolgáltatás tulajdonosában
         Szöveges üzenetet küldtünk ide: %s. Kérlek add meg az ellenőrző kódot amit az üzenet tartalmaz.
     
    -    Az azonosítási szerverrel (%s) megosztod az e-mail címeket és telefonszámokat. Újra kell csatlakoznod ehhez: %s, hogy megállítsd a megosztást.
    +    Az azonosítási szerverrel (%1$s) megosztod az e-mail címeket és telefonszámokat. Újra kell csatlakoznod ehhez: %2$s, hogy megállítsd a megosztást.
         Egyetértek az azonosítási szerver (%s) Felhasználási feltételeivel ahhoz, hogy megtalálható legyek e-mail címmel vagy telefonszámmal.
     
    diff --git a/vector/src/main/res/values-it/strings.xml b/vector/src/main/res/values-it/strings.xml
    index 4e41ad0a6c..1c879ad794 100644
    --- a/vector/src/main/res/values-it/strings.xml
    +++ b/vector/src/main/res/values-it/strings.xml
    @@ -1743,6 +1743,6 @@ Per essere certo di non perdere nulla, mantieni gli aggiornamenti attivi."Il server di identità che hai scelto non ha alcuna condizione di servizio. Continua solo se ti fidi del proprietario del servizio
         È stato inviato un messaggio a %s. Inserisci il codice di verifica contenuto.
     
    -    Attualmente stai condividendo indirizzi email o numeri di telefono sul server di identità %s. Dovrai riconnetterti a %s per fermarne la condivisione.
    +    Attualmente stai condividendo indirizzi email o numeri di telefono sul server di identità %1$s. Dovrai riconnetterti a %2$s per fermarne la condivisione.
         Accetta le condizioni di servizio del server di identità (%s) per consentire di essere trovabile per email o numero di telefono.
     
    diff --git a/vector/src/main/res/values-ko/strings.xml b/vector/src/main/res/values-ko/strings.xml
    index ad1716f70b..7f76e47ea7 100644
    --- a/vector/src/main/res/values-ko/strings.xml
    +++ b/vector/src/main/res/values-ko/strings.xml
    @@ -1621,6 +1621,6 @@
         선택한 ID 서버가 서비스 약관이 없습니다. 서비스의 소유자를 신뢰하는 경우에만 계속하세요
         %s(으)로 문자 메시지를 보냈습니다. 문자에 있는 확인 코드를 입력해주세요.
     
    -    현재 이메일 주소나 전화번호를 ID 서버 %s와 공유하고 있습니다. 공유하기를 중지하려면 %s(으)로 다시 연결해야 합니다.
    +    현재 이메일 주소나 전화번호를 ID 서버 %1$s와 공유하고 있습니다. 공유하기를 중지하려면 %2$s(으)로 다시 연결해야 합니다.
         ID 서버 (%s)의 서비스 약관에 동의하면 다른 사용자가 당신을 이메일 주소나 전화번호로 찾을 수 있게 됩니다.
     
    diff --git a/vector/src/main/res/values-ru/strings.xml b/vector/src/main/res/values-ru/strings.xml
    index 145bb548c8..eaad9bf4aa 100644
    --- a/vector/src/main/res/values-ru/strings.xml
    +++ b/vector/src/main/res/values-ru/strings.xml
    @@ -1790,6 +1790,6 @@
         Выбранный сервер идентификации не имеет условий обслуживания. Продолжить, только если вы доверяете владельцу службы
         Текстовое сообщение отправлено %s. Введите код проверки, который он содержит.
     
    -    В настоящее время вы делитесь адресами электронной почты или телефонными номерами на сервере идентификации %s. Вам нужно повторно подключиться к %s, чтобы прекратить делиться ими.
    +    В настоящее время вы делитесь адресами электронной почты или телефонными номерами на сервере идентификации %1$s. Вам нужно повторно подключиться к %2$s, чтобы прекратить делиться ими.
         Примите Условия обслуживания сервера идентификации (%s), чтобы разрешить обнаружение по адресу электронной почты или номеру телефона.
     
    diff --git a/vector/src/main/res/values-sq/strings.xml b/vector/src/main/res/values-sq/strings.xml
    index ea8a599718..f299ad4385 100644
    --- a/vector/src/main/res/values-sq/strings.xml
    +++ b/vector/src/main/res/values-sq/strings.xml
    @@ -1652,6 +1652,6 @@ Që të garantoni se s’ju shpëton gjë, thjesht mbajeni të aktivizuar mekani
         Shërbyesi i identiteteve që keni zgjedhur nuk ka ndonjë kusht shërbimi. Vazhdoni vetëm nëse i zini besë të zotit të shërbimit
         Te %s u dërgua një mesazh tekst. Ju lutemi, jepni kodin e verifikimit që përmban ai.
     
    -    Hëpërhë, ndani me të tjerë adresa email ose numra telefoni te shërbyesi i identiteteve %s. Do të duhet të rilidheni me %s që të ndalni ndarjen e tyre.
    +    Hëpërhë, ndani me të tjerë adresa email ose numra telefoni te shërbyesi i identiteteve %1$s. Do të duhet të rilidheni me %2$s që të ndalni ndarjen e tyre.
         Që të lejoni veten të jetë e zbulueshme nga adresë email apo numër telefoni, pajtohuni me Kushtet e Shërbimit të shërbyesit të identiteteve (%s).
     
    diff --git a/vector/src/main/res/values-zh-rTW/strings.xml b/vector/src/main/res/values-zh-rTW/strings.xml
    index cd81aea785..b54c05b784 100644
    --- a/vector/src/main/res/values-zh-rTW/strings.xml
    +++ b/vector/src/main/res/values-zh-rTW/strings.xml
    @@ -1651,6 +1651,6 @@ Matrix 中的消息可見度類似于電子郵件。我們忘記您的郵件意
         您選擇的身份識別伺服器沒有任何服務條款。僅在您信任服務擁有者時才繼續
         文字訊息已傳送給 %s。請輸入其中包含的驗證碼。
     
    -    您目前正在身份識別伺服器 %s 上分享電子郵件地址或電話號碼。您將必須重新連線到 %s 以停止分享它們。
    +    您目前正在身份識別伺服器 %1$s 上分享電子郵件地址或電話號碼。您將必須重新連線到 %2$s 以停止分享它們。
         同意身份識別伺服器 (%s) 的服務條款以允許您被透過電子郵件地址或電話號碼探索。
     
    diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
    index 8d5e211fbd..83ce65783f 100644
    --- a/vector/src/main/res/values/strings.xml
    +++ b/vector/src/main/res/values/strings.xml
    @@ -1706,7 +1706,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
         The identity server you have chosen does not have any terms of services. Only continue if you trust the owner of the service
         A text message has been sent to %s. Please enter the verification code it contains.
     
    -    You are currently sharing email addresses or phone numbers on the identity server %s. You will need to reconnect to %s to stop sharing them.
    +    You are currently sharing email addresses or phone numbers on the identity server %1$s. You will need to reconnect to %2$s to stop sharing them.
         Agree to the identity server (%s) Terms of Service to allow yourself to be discoverable by email address or phone number.
     
         Enable verbose logs.
    
    From 8b1411f533830543915ee70bac0bd34e92536e2d Mon Sep 17 00:00:00 2001
    From: ganfra 
    Date: Wed, 23 Oct 2019 16:13:35 +0200
    Subject: [PATCH 191/197] Read marker: test if local echo before hitting the
     SDK to change read marker id + reduce a bit delay
    
    ---
     .../android/api/session/events/model/LocalEcho.kt |  9 +++++++++
     .../internal/database/query/ReadQueries.kt        |  3 ++-
     .../session/room/EventRelationsAggregationTask.kt |  2 +-
     .../internal/session/room/prune/PruneEventTask.kt |  3 ++-
     .../session/room/read/SetReadMarkersTask.kt       |  5 +++--
     .../session/room/send/LocalEchoEventFactory.kt    |  6 +-----
     .../vector/riotx/core/ui/views/ReadMarkerView.kt  |  2 +-
     .../home/room/detail/RoomDetailFragment.kt        | 15 +++++++++++----
     8 files changed, 30 insertions(+), 15 deletions(-)
     create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/LocalEcho.kt
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/LocalEcho.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/LocalEcho.kt
    new file mode 100644
    index 0000000000..a34791856d
    --- /dev/null
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/LocalEcho.kt
    @@ -0,0 +1,9 @@
    +package im.vector.matrix.android.api.session.events.model
    +
    +object LocalEcho {
    +
    +    const val PREFIX = "local."
    +
    +    fun isLocalEchoId(eventId: String): Boolean = eventId.startsWith(PREFIX)
    +
    +}
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt
    index b52f35fa92..65f20a5d41 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt
    @@ -16,6 +16,7 @@
     package im.vector.matrix.android.internal.database.query
     
     import com.zhuinden.monarchy.Monarchy
    +import im.vector.matrix.android.api.session.events.model.LocalEcho
     import im.vector.matrix.android.internal.database.model.ChunkEntity
     import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
     import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory
    @@ -27,7 +28,7 @@ internal fun isEventRead(monarchy: Monarchy,
         if (userId.isNullOrBlank() || roomId.isNullOrBlank() || eventId.isNullOrBlank()) {
             return false
         }
    -    if (LocalEchoEventFactory.isLocalEchoId(eventId)) {
    +    if (LocalEcho.isLocalEchoId(eventId)) {
             return true
         }
         var isEventRead = false
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt
    index 00d6619ce7..f181494e1d 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt
    @@ -71,7 +71,7 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
                         Timber.w("Event has no room id ${event.eventId}")
                         return@forEach
                     }
    -                val isLocalEcho = LocalEchoEventFactory.isLocalEchoId(event.eventId ?: "")
    +                val isLocalEcho = LocalEcho.isLocalEchoId(event.eventId ?: "")
                     when (event.type) {
                         EventType.REACTION  -> {
                             // we got a reaction!!
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt
    index c55007c54a..06fb403f86 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt
    @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.session.room.prune
     import com.zhuinden.monarchy.Monarchy
     import im.vector.matrix.android.api.session.events.model.Event
     import im.vector.matrix.android.api.session.events.model.EventType
    +import im.vector.matrix.android.api.session.events.model.LocalEcho
     import im.vector.matrix.android.api.session.events.model.UnsignedData
     import im.vector.matrix.android.internal.database.helper.updateSenderData
     import im.vector.matrix.android.internal.database.mapper.ContentMapper
    @@ -59,7 +60,7 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M
             // Check that we know this event
             EventEntity.where(realm, eventId = redactionEvent.eventId ?: "").findFirst() ?: return
     
    -        val isLocalEcho = LocalEchoEventFactory.isLocalEchoId(redactionEvent.eventId ?: "")
    +        val isLocalEcho = LocalEcho.isLocalEchoId(redactionEvent.eventId ?: "")
             Timber.v("Redact event for ${redactionEvent.redacts} localEcho=$isLocalEcho")
     
             val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst()
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt
    index 3ed322b483..1fd771cf0b 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt
    @@ -17,6 +17,7 @@
     package im.vector.matrix.android.internal.session.room.read
     
     import com.zhuinden.monarchy.Monarchy
    +import im.vector.matrix.android.api.session.events.model.LocalEcho
     import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
     import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
     import im.vector.matrix.android.internal.database.model.TimelineEventEntity
    @@ -73,7 +74,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
             }
     
             if (fullyReadEventId != null && isReadMarkerMoreRecent(params.roomId, fullyReadEventId)) {
    -            if (LocalEchoEventFactory.isLocalEchoId(fullyReadEventId)) {
    +            if (LocalEcho.isLocalEchoId(fullyReadEventId)) {
                     Timber.w("Can't set read marker for local event $fullyReadEventId")
                 } else {
                     markers[READ_MARKER] = fullyReadEventId
    @@ -82,7 +83,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
     
             if (readReceiptEventId != null
                 && !isEventRead(monarchy, userId, params.roomId, readReceiptEventId)) {
    -            if (LocalEchoEventFactory.isLocalEchoId(readReceiptEventId)) {
    +            if (LocalEcho.isLocalEchoId(readReceiptEventId)) {
                     Timber.w("Can't set read receipt for local event $readReceiptEventId")
                 } else {
                     markers[READ_RECEIPT] = readReceiptEventId
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt
    index a4ce62e1f0..1d3f4def19 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt
    @@ -281,7 +281,7 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use
         }
     
         private fun dummyEventId(): String {
    -        return "$LOCAL_ID_PREFIX${UUID.randomUUID()}"
    +        return "${LocalEcho.PREFIX}${UUID.randomUUID()}"
         }
     
         fun createReplyTextEvent(roomId: String, eventReplied: TimelineEvent, replyText: String, autoMarkdown: Boolean): Event? {
    @@ -407,8 +407,6 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use
         }
     
         companion object {
    -        const val LOCAL_ID_PREFIX = "local."
    -
             // 
             //     
    // In reply to @@ -419,7 +417,5 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use // // No whitespace because currently breaks temporary formatted text to Span const val REPLY_PATTERN = """
    %s%s
    %s
    %s""" - - fun isLocalEchoId(eventId: String): Boolean = eventId.startsWith(LOCAL_ID_PREFIX) } } diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt index ba21d250d8..0fb8b55250 100644 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt @@ -26,7 +26,7 @@ import android.view.animation.AnimationUtils import im.vector.riotx.R import kotlinx.coroutines.* -private const val DELAY_IN_MS = 1_500L +private const val DELAY_IN_MS = 1_000L class ReadMarkerView @JvmOverloads constructor( context: Context, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 9aa2f3cccd..6d80b4a3b7 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -61,6 +61,7 @@ import com.otaliastudios.autocomplete.CharPolicy import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.LocalEcho import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.send.SendState @@ -1023,10 +1024,16 @@ class RoomDetailFragment : return } val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() - val firstVisibleItem = timelineEventController.adapter.getModelAtPosition(firstVisibleItemPosition) - val nextReadMarkerId = when (firstVisibleItem) { - is BaseEventItem -> firstVisibleItem.getEventIds().firstOrNull() - else -> null + var nextReadMarkerId: String? = null + for (itemPosition in firstVisibleItemPosition until lastVisibleItemPosition) { + val timelineItem = timelineEventController.adapter.getModelAtPosition(itemPosition) + if (timelineItem is BaseEventItem) { + val eventId = timelineItem.getEventIds().firstOrNull() ?: continue + if (!LocalEcho.isLocalEchoId(eventId)) { + nextReadMarkerId = eventId + break + } + } } if (nextReadMarkerId != null) { roomDetailViewModel.process(RoomDetailActions.SetReadMarkerAction(nextReadMarkerId)) From 2f5fdbb7e27735777db8c2195ece60ae0255ebc0 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 23 Oct 2019 16:20:38 +0200 Subject: [PATCH 192/197] Clean and fix lint --- .../vector/matrix/android/api/session/events/model/LocalEcho.kt | 1 - .../vector/matrix/android/internal/database/query/ReadQueries.kt | 1 - .../internal/session/room/EventRelationsAggregationTask.kt | 1 - .../matrix/android/internal/session/room/prune/PruneEventTask.kt | 1 - .../android/internal/session/room/read/SetReadMarkersTask.kt | 1 - 5 files changed, 5 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/LocalEcho.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/LocalEcho.kt index a34791856d..0a8a4a3ec3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/LocalEcho.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/LocalEcho.kt @@ -5,5 +5,4 @@ object LocalEcho { const val PREFIX = "local." fun isLocalEchoId(eventId: String): Boolean = eventId.startsWith(PREFIX) - } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt index 65f20a5d41..0a925ac1ab 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt @@ -19,7 +19,6 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.events.model.LocalEcho import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ReadReceiptEntity -import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory internal fun isEventRead(monarchy: Monarchy, userId: String?, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt index f181494e1d..224b3bcfeb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt @@ -28,7 +28,6 @@ import im.vector.matrix.android.internal.database.mapper.EventMapper import im.vector.matrix.android.internal.database.model.* import im.vector.matrix.android.internal.database.query.create import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.awaitTransaction import io.realm.Realm diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt index 06fb403f86..c303e1c215 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt @@ -28,7 +28,6 @@ import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.query.findWithSenderMembershipEvent import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.di.MoshiProvider -import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.awaitTransaction import io.realm.Realm diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt index 1fd771cf0b..7e5de176bb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt @@ -27,7 +27,6 @@ import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.room.RoomAPI -import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory import im.vector.matrix.android.internal.session.sync.ReadReceiptHandler import im.vector.matrix.android.internal.session.sync.RoomFullyReadHandler import im.vector.matrix.android.internal.task.Task From 187e2a26db283cba01f337015d4a0a2b84886744 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 23 Oct 2019 17:26:02 +0200 Subject: [PATCH 193/197] Clean after Benoit's review --- .../api/session/events/model/LocalEcho.kt | 24 +++++++++++++++++-- .../room/send/LocalEchoEventFactory.kt | 10 +++----- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/LocalEcho.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/LocalEcho.kt index 0a8a4a3ec3..ca75871cda 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/LocalEcho.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/LocalEcho.kt @@ -1,8 +1,28 @@ +/* + * Copyright 2019 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.matrix.android.api.session.events.model +import java.util.* + object LocalEcho { - const val PREFIX = "local." + private const val PREFIX = "local." - fun isLocalEchoId(eventId: String): Boolean = eventId.startsWith(PREFIX) + fun isLocalEchoId(eventId: String) = eventId.startsWith(PREFIX) + + fun createLocalEchoId() = "${PREFIX}${UUID.randomUUID()}" } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index 1d3f4def19..49c813ece6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -160,7 +160,7 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use reaction ) ) - val localId = dummyEventId() + val localId = LocalEcho.createLocalEchoId() return Event( roomId = roomId, originServerTs = dummyOriginServerTs(), @@ -264,7 +264,7 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use } private fun createEvent(roomId: String, content: Any? = null): Event { - val localID = dummyEventId() + val localID = LocalEcho.createLocalEchoId() return Event( roomId = roomId, originServerTs = dummyOriginServerTs(), @@ -280,10 +280,6 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use return System.currentTimeMillis() } - private fun dummyEventId(): String { - return "${LocalEcho.PREFIX}${UUID.randomUUID()}" - } - fun createReplyTextEvent(roomId: String, eventReplied: TimelineEvent, replyText: String, autoMarkdown: Boolean): Event? { // Fallbacks and event representation // TODO Add error/warning logs when any of this is null @@ -383,7 +379,7 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use } */ fun createRedactEvent(roomId: String, eventId: String, reason: String?): Event { - val localID = dummyEventId() + val localID = LocalEcho.createLocalEchoId() return Event( roomId = roomId, originServerTs = dummyOriginServerTs(), From 8be8cc9ef792496f87b10d204d169f5822d9c87a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 24 Oct 2019 10:45:53 +0200 Subject: [PATCH 194/197] Filter rooms when sharing element --- .../home/room/list/RoomSummaryController.kt | 62 ++++++++++++------- .../features/share/IncomingShareActivity.kt | 18 +++++- 2 files changed, 54 insertions(+), 26 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt index 6d786cde5e..0288590833 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt @@ -20,6 +20,8 @@ import androidx.annotation.StringRes import com.airbnb.epoxy.EpoxyController import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomSummary +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.noResultItem import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.home.room.filtered.FilteredRoomFooterItem import im.vector.riotx.features.home.room.filtered.filteredRoomFooterItem @@ -47,24 +49,28 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri override fun buildModels() { val nonNullViewState = viewState ?: return - if (nonNullViewState.displayMode == RoomListFragment.DisplayMode.FILTERED) { - buildFilteredRooms(nonNullViewState) - } else { - val roomSummaries = nonNullViewState.asyncFilteredRooms() - roomSummaries?.forEach { (category, summaries) -> - if (summaries.isEmpty()) { - return@forEach - } else { - val isExpanded = nonNullViewState.isCategoryExpanded(category) - buildRoomCategory(nonNullViewState, summaries, category.titleRes, nonNullViewState.isCategoryExpanded(category)) { - listener?.onToggleRoomCategory(category) - } - if (isExpanded) { - buildRoomModels(summaries, - nonNullViewState.joiningRoomsIds, - nonNullViewState.joiningErrorRoomsIds, - nonNullViewState.rejectingRoomsIds, - nonNullViewState.rejectingErrorRoomsIds) + when (nonNullViewState.displayMode) { + RoomListFragment.DisplayMode.FILTERED, + RoomListFragment.DisplayMode.SHARE -> { + buildFilteredRooms(nonNullViewState) + } + else -> { + val roomSummaries = nonNullViewState.asyncFilteredRooms() + roomSummaries?.forEach { (category, summaries) -> + if (summaries.isEmpty()) { + return@forEach + } else { + val isExpanded = nonNullViewState.isCategoryExpanded(category) + buildRoomCategory(nonNullViewState, summaries, category.titleRes, nonNullViewState.isCategoryExpanded(category)) { + listener?.onToggleRoomCategory(category) + } + if (isExpanded) { + buildRoomModels(summaries, + nonNullViewState.joiningRoomsIds, + nonNullViewState.joiningErrorRoomsIds, + nonNullViewState.rejectingRoomsIds, + nonNullViewState.rejectingErrorRoomsIds) + } } } } @@ -80,12 +86,15 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri .filter { it.membership == Membership.JOIN && roomListNameFilter.test(it) } buildRoomModels(filteredSummaries, - viewState.joiningRoomsIds, - viewState.joiningErrorRoomsIds, - viewState.rejectingRoomsIds, - viewState.rejectingErrorRoomsIds) + viewState.joiningRoomsIds, + viewState.joiningErrorRoomsIds, + viewState.rejectingRoomsIds, + viewState.rejectingErrorRoomsIds) - addFilterFooter(viewState) + when { + viewState.displayMode == RoomListFragment.DisplayMode.FILTERED -> addFilterFooter(viewState) + filteredSummaries.isEmpty() -> addEmptyFooter() + } } private fun addFilterFooter(viewState: RoomListViewState) { @@ -96,6 +105,13 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri } } + private fun addEmptyFooter() { + noResultItem { + id("no_result") + text(stringProvider.getString(R.string.no_result_placeholder)) + } + } + private fun buildRoomCategory(viewState: RoomListViewState, summaries: List, @StringRes titleRes: Int, diff --git a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt index 0d2f9ee040..0ce28f1f4b 100644 --- a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt @@ -20,6 +20,7 @@ import android.content.ClipDescription import android.content.Intent import android.os.Bundle import android.widget.Toast +import androidx.appcompat.widget.SearchView import com.kbeanie.multipicker.utils.IntentUtils import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.riotx.R @@ -39,7 +40,7 @@ class IncomingShareActivity : VectorBaseActivity(), AttachmentsHelper.Callback { @Inject lateinit var sessionHolder: ActiveSessionHolder - private lateinit var roomListFragment: RoomListFragment + private var roomListFragment: RoomListFragment? = null private lateinit var attachmentsHelper: AttachmentsHelper override fun getLayoutRes(): Int { @@ -77,12 +78,23 @@ class IncomingShareActivity : } else { cannotManageShare() } + + incomingShareSearchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String): Boolean { + return true + } + + override fun onQueryTextChange(newText: String): Boolean { + roomListFragment?.filterRoomsWith(newText) + return true + } + }) } override fun onContentAttachmentsReady(attachments: List) { val roomListParams = RoomListParams(RoomListFragment.DisplayMode.SHARE, sharedData = SharedData.Attachments(attachments)) roomListFragment = RoomListFragment.newInstance(roomListParams) - replaceFragment(roomListFragment, R.id.shareRoomListFragmentContainer) + .also { replaceFragment(it, R.id.shareRoomListFragmentContainer) } } override fun onAttachmentsProcessFailed() { @@ -102,7 +114,7 @@ class IncomingShareActivity : } else { val roomListParams = RoomListParams(RoomListFragment.DisplayMode.SHARE, sharedData = SharedData.Text(sharedText)) roomListFragment = RoomListFragment.newInstance(roomListParams) - replaceFragment(roomListFragment, R.id.shareRoomListFragmentContainer) + .also { replaceFragment(it, R.id.shareRoomListFragmentContainer) } true } } From b6594599c433b514d2b18f27ba452ce5b31506f5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 24 Oct 2019 10:53:19 +0200 Subject: [PATCH 195/197] Rename member --- vector/src/main/java/im/vector/riotx/AppStateHandler.kt | 4 ++-- .../riotx/features/home/room/list/RoomListViewModel.kt | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/AppStateHandler.kt b/vector/src/main/java/im/vector/riotx/AppStateHandler.kt index d0301e2c9f..76cbb9ef94 100644 --- a/vector/src/main/java/im/vector/riotx/AppStateHandler.kt +++ b/vector/src/main/java/im/vector/riotx/AppStateHandler.kt @@ -42,7 +42,7 @@ import javax.inject.Singleton @Singleton class AppStateHandler @Inject constructor( private val sessionObservableStore: ActiveSessionObservableStore, - private val homeRoomListStore: HomeRoomListObservableStore, + private val homeRoomListObservableStore: HomeRoomListObservableStore, private val selectedGroupStore: SelectedGroupStore) : LifecycleObserver { private val compositeDisposable = CompositeDisposable() @@ -92,7 +92,7 @@ class AppStateHandler @Inject constructor( } ) .subscribe { - homeRoomListStore.post(it) + homeRoomListObservableStore.post(it) } .addTo(compositeDisposable) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt index b7a10edd49..686693c776 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt @@ -37,7 +37,7 @@ import timber.log.Timber class RoomListViewModel @AssistedInject constructor(@Assisted initialState: RoomListViewState, private val session: Session, - private val homeRoomListObservableSource: HomeRoomListObservableStore, + private val homeRoomListObservableStore: HomeRoomListObservableStore, private val alphabeticalRoomComparator: AlphabeticalRoomComparator, private val chronologicalRoomComparator: ChronologicalRoomComparator) : VectorViewModel(initialState) { @@ -101,7 +101,7 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room } private fun observeRoomSummaries() { - homeRoomListObservableSource + homeRoomListObservableStore .observe() .observeOn(Schedulers.computation()) .map { @@ -111,7 +111,7 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room copy(asyncRooms = asyncRooms) } - homeRoomListObservableSource + homeRoomListObservableStore .observe() .observeOn(Schedulers.computation()) .map { buildRoomSummaries(it) } From 8fa676d03437bd2d8ec0cf1f687134c539682766 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 24 Oct 2019 12:17:57 +0200 Subject: [PATCH 196/197] Share Activity: display rooms of all communities --- .../vector/riotx/core/di/ScreenComponent.kt | 15 +++- .../vector/riotx/core/di/VectorComponent.kt | 6 ++ .../features/home/room/list/RoomListModule.kt | 27 +++++++ .../home/room/list/RoomListViewModel.kt | 20 +++-- .../room/list/RoomListViewModelFactory.kt | 39 ++++++++++ .../features/share/IncomingShareActivity.kt | 3 + .../features/share/IncomingShareViewModel.kt | 73 +++++++++++++++++++ .../share/ShareRoomListObservableStore.kt | 25 +++++++ 8 files changed, 195 insertions(+), 13 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListModule.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModelFactory.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewModel.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/share/ShareRoomListObservableStore.kt diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index da9daac68d..0efbc0e173 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -41,11 +41,12 @@ import im.vector.riotx.features.home.createdirect.CreateDirectRoomKnownUsersFrag import im.vector.riotx.features.home.group.GroupListFragment import im.vector.riotx.features.home.room.detail.RoomDetailFragment import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet -import im.vector.riotx.features.home.room.detail.timeline.action.* +import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet import im.vector.riotx.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet import im.vector.riotx.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity import im.vector.riotx.features.home.room.list.RoomListFragment +import im.vector.riotx.features.home.room.list.RoomListModule import im.vector.riotx.features.invite.VectorInviteView import im.vector.riotx.features.link.LinkHandlerActivity import im.vector.riotx.features.login.LoginActivity @@ -70,7 +71,17 @@ import im.vector.riotx.features.settings.push.PushGatewaysFragment import im.vector.riotx.features.share.IncomingShareActivity import im.vector.riotx.features.ui.UiStateRepository -@Component(dependencies = [VectorComponent::class], modules = [AssistedInjectModule::class, ViewModelModule::class, HomeModule::class]) +@Component( + dependencies = [ + VectorComponent::class + ], + modules = [ + AssistedInjectModule::class, + ViewModelModule::class, + HomeModule::class, + RoomListModule::class + ] +) @ScreenScope interface ScreenComponent { diff --git a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt index a59620aacb..2dfbb5f799 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt @@ -23,6 +23,7 @@ import dagger.Component import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.auth.Authenticator import im.vector.matrix.android.api.session.Session +import im.vector.riotx.ActiveSessionObservableStore import im.vector.riotx.EmojiCompatFontProvider import im.vector.riotx.EmojiCompatWrapper import im.vector.riotx.VectorApplication @@ -42,6 +43,7 @@ import im.vector.riotx.features.rageshake.VectorFileLogger import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler import im.vector.riotx.features.session.SessionListener import im.vector.riotx.features.settings.VectorPreferences +import im.vector.riotx.features.share.ShareRoomListObservableStore import im.vector.riotx.features.ui.UiStateRepository import javax.inject.Singleton @@ -85,8 +87,12 @@ interface VectorComponent { fun homeRoomListObservableStore(): HomeRoomListObservableStore + fun shareRoomListObservableStore(): ShareRoomListObservableStore + fun selectedGroupStore(): SelectedGroupStore + fun activeSessionObservableStore(): ActiveSessionObservableStore + fun incomingVerificationRequestHandler(): IncomingVerificationRequestHandler fun incomingKeyRequestHandler(): KeyRequestHandler diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListModule.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListModule.kt new file mode 100644 index 0000000000..4541b5d2b5 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListModule.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 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.riotx.features.home.room.list + +import dagger.Binds +import dagger.Module + +@Module +abstract class RoomListModule { + + @Binds + abstract fun providesRoomListViewModelFactory(roomListViewModelFactory: RoomListViewModelFactory): RoomListViewModel.Factory +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt index 686693c776..c413a09c8a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt @@ -21,8 +21,6 @@ import androidx.lifecycle.MutableLiveData import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.room.model.Membership @@ -31,18 +29,18 @@ import im.vector.matrix.android.api.session.room.model.tag.RoomTag import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.utils.LiveEvent -import im.vector.riotx.features.home.HomeRoomListObservableStore +import im.vector.riotx.core.utils.RxStore import io.reactivex.schedulers.Schedulers import timber.log.Timber +import javax.inject.Inject -class RoomListViewModel @AssistedInject constructor(@Assisted initialState: RoomListViewState, - private val session: Session, - private val homeRoomListObservableStore: HomeRoomListObservableStore, - private val alphabeticalRoomComparator: AlphabeticalRoomComparator, - private val chronologicalRoomComparator: ChronologicalRoomComparator) +class RoomListViewModel @Inject constructor(initialState: RoomListViewState, + private val session: Session, + private val roomSummariesStore: RxStore>, + private val alphabeticalRoomComparator: AlphabeticalRoomComparator, + private val chronologicalRoomComparator: ChronologicalRoomComparator) : VectorViewModel(initialState) { - @AssistedInject.Factory interface Factory { fun create(initialState: RoomListViewState): RoomListViewModel } @@ -101,7 +99,7 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room } private fun observeRoomSummaries() { - homeRoomListObservableStore + roomSummariesStore .observe() .observeOn(Schedulers.computation()) .map { @@ -111,7 +109,7 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room copy(asyncRooms = asyncRooms) } - homeRoomListObservableStore + roomSummariesStore .observe() .observeOn(Schedulers.computation()) .map { buildRoomSummaries(it) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModelFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModelFactory.kt new file mode 100644 index 0000000000..5895aa4e52 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModelFactory.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 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.riotx.features.home.room.list + +import im.vector.matrix.android.api.session.Session +import im.vector.riotx.features.home.HomeRoomListObservableStore +import im.vector.riotx.features.share.ShareRoomListObservableStore +import javax.inject.Inject +import javax.inject.Provider + +class RoomListViewModelFactory @Inject constructor(private val session: Provider, + private val homeRoomListObservableStore: Provider, + private val shareRoomListObservableStore: Provider, + private val alphabeticalRoomComparator: Provider, + private val chronologicalRoomComparator: Provider) : RoomListViewModel.Factory { + + override fun create(initialState: RoomListViewState): RoomListViewModel { + return RoomListViewModel( + initialState, + session.get(), + if (initialState.displayMode == RoomListFragment.DisplayMode.SHARE) shareRoomListObservableStore.get() else homeRoomListObservableStore.get(), + alphabeticalRoomComparator.get(), + chronologicalRoomComparator.get()) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt index 0ce28f1f4b..5e471cf78b 100644 --- a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt @@ -21,6 +21,7 @@ import android.content.Intent import android.os.Bundle import android.widget.Toast import androidx.appcompat.widget.SearchView +import com.airbnb.mvrx.viewModel import com.kbeanie.multipicker.utils.IntentUtils import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.riotx.R @@ -40,8 +41,10 @@ class IncomingShareActivity : VectorBaseActivity(), AttachmentsHelper.Callback { @Inject lateinit var sessionHolder: ActiveSessionHolder + @Inject lateinit var incomingShareViewModelFactory: IncomingShareViewModel.Factory private var roomListFragment: RoomListFragment? = null private lateinit var attachmentsHelper: AttachmentsHelper + private val incomingShareViewModel: IncomingShareViewModel by viewModel() override fun getLayoutRes(): Int { return R.layout.activity_incoming_share diff --git a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewModel.kt b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewModel.kt new file mode 100644 index 0000000000..51485ecbf9 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewModel.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2019 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.riotx.features.share + +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.rx.rx +import im.vector.riotx.ActiveSessionObservableStore +import im.vector.riotx.core.platform.VectorViewModel +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import java.util.concurrent.TimeUnit + +data class IncomingShareState(private val dummy: Boolean = false) : MvRxState + +/** + * View model used to observe the room list and post update to the ShareRoomListObservableStore + */ +class IncomingShareViewModel @AssistedInject constructor(@Assisted initialState: IncomingShareState, + private val sessionObservableStore: ActiveSessionObservableStore, + private val shareRoomListObservableStore: ShareRoomListObservableStore) + : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: IncomingShareState): IncomingShareViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: IncomingShareState): IncomingShareViewModel? { + val activity: IncomingShareActivity = (viewModelContext as ActivityViewModelContext).activity() + return activity.incomingShareViewModelFactory.create(state) + } + } + + init { + observeRoomSummaries() + } + + private fun observeRoomSummaries() { + sessionObservableStore.observe() + .observeOn(AndroidSchedulers.mainThread()) + .switchMap { + it.orNull()?.rx()?.liveRoomSummaries() + ?: Observable.just(emptyList()) + } + .throttleLast(300, TimeUnit.MILLISECONDS) + .subscribe { + shareRoomListObservableStore.post(it) + } + .disposeOnClear() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/share/ShareRoomListObservableStore.kt b/vector/src/main/java/im/vector/riotx/features/share/ShareRoomListObservableStore.kt new file mode 100644 index 0000000000..c46ec42d64 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/share/ShareRoomListObservableStore.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 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.riotx.features.share + +import im.vector.matrix.android.api.session.room.model.RoomSummary +import im.vector.riotx.core.utils.RxStore +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ShareRoomListObservableStore @Inject constructor() : RxStore>() From af08759af689a6d61dd2f71ab20ffc52d846ec4d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 24 Oct 2019 14:37:28 +0200 Subject: [PATCH 197/197] Prepare release 0.7.0 --- CHANGES.md | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 8d536dbd97..48d0be34cd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,17 +1,16 @@ -Changes in RiotX 0.7.0 (2019-XX-XX) +Changes in RiotX 0.7.0 (2019-10-24) =================================================== Features: - - + - Share elements from other app to RiotX (#58) + - Read marker (#84) + - Add ability to report content (#515) Improvements: - Persist active tab between sessions (#503) - Do not upload file too big for the homeserver (#587) - - Handle read markers (#84) - Attachments: start using system pickers (#52) - - Attachments: start handling incoming share (#58) - Mark all messages as read (#396) - - Add ability to report content (#515) Other changes: - Accessibility improvements to read receipts in the room timeline and reactions emoji chooser @@ -25,12 +24,6 @@ Bugfix: - Invitation notifications are not dismissed automatically if room is joined from another client (#347) - Opening links from RiotX reuses browser tab (#599) -Translations: - - - -Build: - - - Changes in RiotX 0.6.1 (2019-09-24) ===================================================