From b4ce8748cb3aff6f06bfcba37e4a34e0d144307a Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 8 Aug 2019 14:32:11 +0200 Subject: [PATCH 01/40] First step in handling read receipts --- .../api/session/room/model/ReadReceipt.kt | 5 +- .../session/room/timeline/TimelineEvent.kt | 10 ++- .../database/helper/ChunkEntityHelper.kt | 36 +++++++-- .../mapper/ReadReceiptsSummaryMapper.kt | 40 ++++++++++ .../database/mapper/RoomSummaryMapper.kt | 7 +- .../database/mapper/TimelineEventMapper.kt | 15 ++-- .../database/model/ReadReceiptEntity.kt | 13 +++- .../model/ReadReceiptsSummaryEntity.kt | 31 ++++++++ .../database/model/SessionRealmModule.kt | 45 +++++------ .../database/model/TimelineEventEntity.kt | 3 +- .../query/ReadReceiptsSummaryEntityQueries.kt | 28 +++++++ .../internal/session/room/RoomFactory.kt | 6 +- .../session/room/timeline/DefaultTimeline.kt | 10 ++- .../room/timeline/DefaultTimelineService.kt | 14 ++-- .../session/sync/ReadReceiptHandler.kt | 42 ++++++---- .../internal/session/sync/RoomSyncHandler.kt | 19 +++-- .../riotx/core/platform/VectorBaseActivity.kt | 1 - .../timeline/factory/MessageItemFactory.kt | 76 +++++++++++-------- .../detail/timeline/item/AbsMessageItem.kt | 35 +++++++++ .../timeline/item/MessageInformationData.kt | 12 ++- .../util/MessageInformationDataFactory.kt | 26 +++++-- .../res/layout/item_timeline_event_base.xml | 63 ++++++++++++++- .../main/res/layout/view_read_receipts.xml | 11 +++ 23 files changed, 422 insertions(+), 126 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/ReadReceiptsSummaryMapper.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadReceiptsSummaryEntity.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadReceiptsSummaryEntityQueries.kt create mode 100644 vector/src/main/res/layout/view_read_receipts.xml diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReadReceipt.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReadReceipt.kt index 0516b1af..e168dc1e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReadReceipt.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReadReceipt.kt @@ -16,8 +16,9 @@ package im.vector.matrix.android.api.session.room.model +import im.vector.matrix.android.api.session.user.model.User + data class ReadReceipt( - val userId: String, - val eventId: String, + val user: User, val originServerTs: Long ) \ 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 5d04d2f5..36ca360e 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 @@ -20,6 +20,7 @@ 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.EventAnnotationsSummary +import im.vector.matrix.android.api.session.room.model.ReadReceipt import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.isReply import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply @@ -37,7 +38,8 @@ data class TimelineEvent( val senderName: String?, val isUniqueDisplayName: Boolean, val senderAvatar: String?, - val annotations: EventAnnotationsSummary? = null + val annotations: EventAnnotationsSummary? = null, + val readReceipts: List = emptyList() ) { val metadata = HashMap() @@ -65,8 +67,8 @@ data class TimelineEvent( "$name (${root.senderId})" } } - ?: root.senderId - ?: "" + ?: root.senderId + ?: "" } /** @@ -94,7 +96,7 @@ fun TimelineEvent.hasBeenEdited() = annotations?.editSummary != null * Get last MessageContent, after a possible edition */ fun TimelineEvent.getLastMessageContent(): MessageContent? = annotations?.editSummary?.aggregatedContent?.toModel() - ?: root.getClearContent().toModel() + ?: root.getClearContent().toModel() fun TimelineEvent.getTextEditableContent(): String? { 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 3bda568d..5a76741e 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,8 @@ 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.ReadReceiptEntity +import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity 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.find @@ -133,6 +135,23 @@ internal fun ChunkEntity.add(roomId: String, } val localId = TimelineEventEntity.nextId(realm) + val eventId = event.eventId ?: "" + val senderId = event.senderId ?: "" + + val currentReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst() + ?: ReadReceiptsSummaryEntity(eventId) + + // Update RR for the sender of a new message + if (direction == PaginationDirection.FORWARDS && !isUnlinked) { + ReadReceiptEntity.where(realm, roomId = roomId, userId = senderId).findFirst()?.also { + val previousEventId = it.eventId + val previousReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId = previousEventId).findFirst() + it.eventId = eventId + previousReceiptsSummary?.readReceipts?.remove(it) + currentReceiptsSummary.readReceipts.add(it) + } + } + val eventEntity = TimelineEventEntity(localId).also { it.root = event.toEntity(roomId).apply { this.stateIndex = currentStateIndex @@ -140,9 +159,10 @@ internal fun ChunkEntity.add(roomId: String, this.displayIndex = currentDisplayIndex this.sendState = SendState.SYNCED } - it.eventId = event.eventId ?: "" + it.eventId = eventId it.roomId = roomId - it.annotations = EventAnnotationsSummaryEntity.where(realm, it.eventId).findFirst() + it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() + it.readReceipts = currentReceiptsSummary } val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size timelineEvents.add(position, eventEntity) @@ -150,14 +170,14 @@ internal fun ChunkEntity.add(roomId: String, internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { return when (direction) { - PaginationDirection.FORWARDS -> forwardsDisplayIndex - PaginationDirection.BACKWARDS -> backwardsDisplayIndex - } ?: defaultValue + PaginationDirection.FORWARDS -> forwardsDisplayIndex + PaginationDirection.BACKWARDS -> backwardsDisplayIndex + } ?: defaultValue } internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { return when (direction) { - PaginationDirection.FORWARDS -> forwardsStateIndex - PaginationDirection.BACKWARDS -> backwardsStateIndex - } ?: defaultValue + PaginationDirection.FORWARDS -> forwardsStateIndex + PaginationDirection.BACKWARDS -> backwardsStateIndex + } ?: defaultValue } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/ReadReceiptsSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/ReadReceiptsSummaryMapper.kt new file mode 100644 index 00000000..3887f3ac --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/ReadReceiptsSummaryMapper.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.internal.database.mapper + +import im.vector.matrix.android.api.session.room.model.ReadReceipt +import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity +import im.vector.matrix.android.internal.database.model.UserEntity +import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.di.SessionDatabase +import io.realm.Realm +import io.realm.RealmConfiguration +import javax.inject.Inject + +internal class ReadReceiptsSummaryMapper @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration){ + + fun map(readReceiptsSummaryEntity: ReadReceiptsSummaryEntity): List { + return Realm.getInstance(realmConfiguration).use { realm -> + readReceiptsSummaryEntity.readReceipts.mapNotNull { + val user = UserEntity.where(realm, it.userId).findFirst() + ?: return@mapNotNull null + ReadReceipt(user.asDomain(), it.originServerTs.toLong()) + } + } + } + +} 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 45328992..95d4d8bc 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,7 +26,8 @@ import java.util.* import javax.inject.Inject internal class RoomSummaryMapper @Inject constructor( - val cryptoService: CryptoService + val cryptoService: CryptoService, + val timelineEventMapper: TimelineEventMapper ) { fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary { @@ -34,7 +35,9 @@ internal class RoomSummaryMapper @Inject constructor( RoomTag(it.tagName, it.tagOrder) } - val latestEvent = roomSummaryEntity.latestEvent?.asDomain() + val latestEvent = roomSummaryEntity.latestEvent?.let { + timelineEventMapper.map(it) + } if (latestEvent?.root?.isEncrypted() == true && latestEvent.root.mxDecryptionResult == null) { //TODO use a global event decryptor? attache to session and that listen to new sessionId? //for now decrypt sync 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 61d5a601..5290692c 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 @@ -17,29 +17,30 @@ package im.vector.matrix.android.internal.database.mapper import im.vector.matrix.android.api.session.events.model.Event + import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.internal.database.model.TimelineEventEntity +import javax.inject.Inject -internal object TimelineEventMapper { +internal class TimelineEventMapper @Inject constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper){ fun map(timelineEventEntity: TimelineEventEntity): TimelineEvent { return TimelineEvent( root = timelineEventEntity.root?.asDomain() - ?: Event("", timelineEventEntity.eventId), + ?: Event("", timelineEventEntity.eventId), annotations = timelineEventEntity.annotations?.asDomain(), localId = timelineEventEntity.localId, displayIndex = timelineEventEntity.root?.displayIndex ?: 0, senderName = timelineEventEntity.senderName, isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName, - senderAvatar = timelineEventEntity.senderAvatar + senderAvatar = timelineEventEntity.senderAvatar, + readReceipts = timelineEventEntity.readReceipts?.let { + readReceiptsSummaryMapper.map(it) + } ?: emptyList() ) } } -internal fun TimelineEventEntity.asDomain(): TimelineEvent { - return TimelineEventMapper.map(this) -} - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadReceiptEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadReceiptEntity.kt index d702fdc6..b0e6a440 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadReceiptEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadReceiptEntity.kt @@ -17,13 +17,18 @@ 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 ReadReceiptEntity(@PrimaryKey var primaryKey: String = "", - var userId: String = "", - var eventId: String = "", - var roomId: String = "", - var originServerTs: Double = 0.0 + var eventId: String = "", + var roomId: String = "", + var userId: String = "", + var originServerTs: Double = 0.0 ) : RealmObject() { companion object + + @LinkingObjects("readReceipts") + val summary: RealmResults? = null } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadReceiptsSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadReceiptsSummaryEntity.kt new file mode 100644 index 00000000..e0fe970f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadReceiptsSummaryEntity.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.database.model + +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class ReadReceiptsSummaryEntity( + @PrimaryKey + var eventId: String = "", + var readReceipts: RealmList = RealmList() +) : 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 0e4dc1ae..1d27bf07 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 @@ -22,26 +22,27 @@ import io.realm.annotations.RealmModule * Realm module for Session */ @RealmModule(library = true, - classes = [ - ChunkEntity::class, - EventEntity::class, - TimelineEventEntity::class, - FilterEntity::class, - GroupEntity::class, - GroupSummaryEntity::class, - ReadReceiptEntity::class, - RoomEntity::class, - RoomSummaryEntity::class, - RoomTagEntity::class, - SyncEntity::class, - UserEntity::class, - EventAnnotationsSummaryEntity::class, - ReactionAggregatedSummaryEntity::class, - EditAggregatedSummaryEntity::class, - PushRulesEntity::class, - PushRuleEntity::class, - PushConditionEntity::class, - PusherEntity::class, - PusherDataEntity::class - ]) + classes = [ + ChunkEntity::class, + EventEntity::class, + TimelineEventEntity::class, + FilterEntity::class, + GroupEntity::class, + GroupSummaryEntity::class, + ReadReceiptEntity::class, + RoomEntity::class, + RoomSummaryEntity::class, + RoomTagEntity::class, + SyncEntity::class, + UserEntity::class, + EventAnnotationsSummaryEntity::class, + ReactionAggregatedSummaryEntity::class, + EditAggregatedSummaryEntity::class, + PushRulesEntity::class, + PushRuleEntity::class, + PushConditionEntity::class, + PusherEntity::class, + PusherDataEntity::class, + ReadReceiptsSummaryEntity::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 a1e58c90..429b2291 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 @@ -30,7 +30,8 @@ internal open class TimelineEventEntity(var localId: Long = 0, var senderName: String? = null, var isUniqueDisplayName: Boolean = false, var senderAvatar: String? = null, - var senderMembershipEvent: EventEntity? = null + var senderMembershipEvent: EventEntity? = null, + var readReceipts: ReadReceiptsSummaryEntity? = null ) : RealmObject() { @LinkingObjects("timelineEvents") 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 new file mode 100644 index 00000000..e6c1e685 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadReceiptsSummaryEntityQueries.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.internal.database.query + +import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity +import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun ReadReceiptsSummaryEntity.Companion.where(realm: Realm, eventId: String): RealmQuery { + return realm.where() + .equalTo(ReadReceiptsSummaryEntityFields.EVENT_ID, eventId) +} \ No newline at end of file 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 1c64c91b..dd5d2d3b 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 @@ -22,6 +22,7 @@ import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper +import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask @@ -47,6 +48,7 @@ internal class RoomFactory @Inject constructor(private val context: Context, private val monarchy: Monarchy, private val eventFactory: LocalEchoEventFactory, private val roomSummaryMapper: RoomSummaryMapper, + private val timelineEventMapper: TimelineEventMapper, private val taskExecutor: TaskExecutor, private val loadRoomMembersTask: LoadRoomMembersTask, private val inviteTask: InviteTask, @@ -61,13 +63,13 @@ internal class RoomFactory @Inject constructor(private val context: Context, private val leaveRoomTask: LeaveRoomTask) { fun create(roomId: String): Room { - val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, cryptoService, paginationTask) + val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, cryptoService, paginationTask, timelineEventMapper) val sendService = DefaultSendService(context, credentials, roomId, eventFactory, cryptoService, monarchy) val stateService = DefaultStateService(roomId, monarchy.realmConfiguration, taskExecutor, sendStateTask) val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask) val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask, credentials) val relationService = DefaultRelationService(context, - credentials, roomId, eventFactory, cryptoService, findReactionEventForUndoTask, fetchEditHistoryTask, monarchy, taskExecutor) + credentials, roomId, eventFactory, cryptoService, findReactionEventForUndoTask, fetchEditHistoryTask, monarchy, taskExecutor) return DefaultRoom( roomId, 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 10f4874f..921c65ea 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 @@ -23,6 +23,7 @@ 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.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.model.EventEntity @@ -53,7 +54,8 @@ internal class DefaultTimeline( private val taskExecutor: TaskExecutor, private val contextOfEventTask: GetContextOfEventTask, private val paginationTask: PaginationTask, - cryptoService: CryptoService, + private val cryptoService: CryptoService, + private val timelineEventMapper: TimelineEventMapper, private val allowedTypes: List? ) : Timeline { @@ -132,7 +134,7 @@ internal class DefaultTimeline( builtEventsIdMap[eventId]?.let { builtIndex -> //Update the relation of existing event builtEvents[builtIndex]?.let { te -> - builtEvents[builtIndex] = eventEntity.asDomain() + builtEvents[builtIndex] = timelineEventMapper.map(eventEntity) hasChanged = true } } @@ -331,7 +333,7 @@ internal class DefaultTimeline( roomEntity?.sendingTimelineEvents ?.filter { allowedTypes?.contains(it.root?.type) ?: false } ?.forEach { - sendingEvents.add(it.asDomain()) + sendingEvents.add(timelineEventMapper.map(it)) } } return sendingEvents @@ -463,7 +465,7 @@ internal class DefaultTimeline( nextDisplayIndex = offsetIndex + 1 } offsetResults.forEach { eventEntity -> - val timelineEvent = eventEntity.asDomain() + val timelineEvent = timelineEventMapper.map(eventEntity) if (timelineEvent.isEncrypted() && timelineEvent.root.mxDecryptionResult == null) { 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 d70f1b92..94fc433a 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 @@ -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.TimelineService import im.vector.matrix.android.internal.database.RealmLiveData +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.TimelineEventEntity import im.vector.matrix.android.internal.database.query.where @@ -36,7 +37,8 @@ internal class DefaultTimelineService @Inject constructor(private val roomId: St private val taskExecutor: TaskExecutor, private val contextOfEventTask: GetContextOfEventTask, private val cryptoService: CryptoService, - private val paginationTask: PaginationTask + private val paginationTask: PaginationTask, + private val timelineEventMapper: TimelineEventMapper ) : TimelineService { override fun createTimeline(eventId: String?, allowedTypes: List?): Timeline { @@ -47,7 +49,9 @@ internal class DefaultTimelineService @Inject constructor(private val roomId: St contextOfEventTask, paginationTask, cryptoService, - allowedTypes) + timelineEventMapper, + allowedTypes + ) } override fun getTimeLineEvent(eventId: String): TimelineEvent? { @@ -55,7 +59,7 @@ internal class DefaultTimelineService @Inject constructor(private val roomId: St .fetchCopyMap({ TimelineEventEntity.where(it, eventId = eventId).findFirst() }, { entity, realm -> - entity.asDomain() + timelineEventMapper.map(entity) }) } @@ -63,8 +67,8 @@ internal class DefaultTimelineService @Inject constructor(private val roomId: St val liveData = RealmLiveData(monarchy.realmConfiguration) { TimelineEventEntity.where(it, eventId = eventId) } - return Transformations.map(liveData) { - it.firstOrNull()?.asDomain() + 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/sync/ReadReceiptHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/ReadReceiptHandler.kt index 9ada6e71..055334b1 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 @@ -17,6 +17,8 @@ package im.vector.matrix.android.internal.session.sync 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.query.where import io.realm.Realm import timber.log.Timber import javax.inject.Inject @@ -36,27 +38,35 @@ internal class ReadReceiptHandler @Inject constructor() { return } try { - val readReceipts = mapContentToReadReceiptEntities(roomId, content) - realm.insertOrUpdate(readReceipts) + handleReadReceiptContent(realm, roomId, content) } catch (exception: Exception) { Timber.e("Fail to handle read receipt for room $roomId") } } - private fun mapContentToReadReceiptEntities(roomId: String, content: ReadReceiptContent): List { - return content - .flatMap { (eventId, receiptDict) -> - receiptDict - .filterKeys { it == "m.read" } - .flatMap { (_, userIdsDict) -> - userIdsDict.map { (userId, paramsDict) -> - val ts = paramsDict.filterKeys { it == "ts" } - .values - .firstOrNull() ?: 0.0 - val primaryKey = roomId + userId - ReadReceiptEntity(primaryKey, userId, eventId, roomId, ts) - } - } + private fun handleReadReceiptContent(realm: Realm, roomId: String, content: ReadReceiptContent) { + for ((eventId, receiptDict) in content) { + val userIdsDict = receiptDict["m.read"] ?: continue + val readReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst() + ?: realm.createObject(ReadReceiptsSummaryEntity::class.java, eventId) + + for ((userId, paramsDict) in userIdsDict) { + val ts = paramsDict["ts"] ?: 0.0 + val primaryKey = "${roomId}_$userId" + val receiptEntity = ReadReceiptEntity.where(realm, roomId, userId).findFirst() + ?: realm.createObject(ReadReceiptEntity::class.java, primaryKey) + + ReadReceiptsSummaryEntity.where(realm, receiptEntity.eventId).findFirst()?.also { + it.readReceipts.remove(receiptEntity) } + receiptEntity.apply { + this.eventId = eventId + this.roomId = roomId + this.userId = userId + this.originServerTs = ts + } + readReceiptsSummary.readReceipts.add(receiptEntity) + } + } } } \ 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 9da3db76..a16cae18 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 @@ -117,7 +117,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch Timber.v("Handle join sync for room $roomId") val roomEntity = RoomEntity.where(realm, roomId).findFirst() - ?: realm.createObject(roomId) + ?: realm.createObject(roomId) if (roomEntity.membership == Membership.INVITE) { roomEntity.chunks.deleteAllFromRealm() @@ -127,7 +127,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch // State event if (roomSync.state != null && roomSync.state.events.isNotEmpty()) { val minStateIndex = roomEntity.untimelinedStateEvents.where().min(EventEntityFields.STATE_INDEX)?.toInt() - ?: Int.MIN_VALUE + ?: Int.MIN_VALUE val untimelinedStateIndex = minStateIndex + 1 roomSync.state.events.forEach { event -> roomEntity.addStateEvent(event, filterDuplicates = true, stateIndex = untimelinedStateIndex) @@ -167,7 +167,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch InvitedRoomSync): RoomEntity { Timber.v("Handle invited sync for room $roomId") val roomEntity = RoomEntity.where(realm, roomId).findFirst() - ?: realm.createObject(roomId) + ?: realm.createObject(roomId) roomEntity.membership = Membership.INVITE if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) { val chunkEntity = handleTimelineEvents(realm, roomEntity, roomSync.inviteState.events) @@ -181,7 +181,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch roomId: String, roomSync: RoomSync): RoomEntity { val roomEntity = RoomEntity.where(realm, roomId).findFirst() - ?: realm.createObject(roomId) + ?: realm.createObject(roomId) roomEntity.membership = Membership.LEAVE roomEntity.chunks.deleteAllFromRealm() @@ -233,17 +233,20 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch } + @Suppress("UNCHECKED_CAST") private fun handleEphemeral(realm: Realm, roomId: String, ephemeral: RoomSyncEphemeral) { - ephemeral.events - .filter { it.getClearType() == EventType.RECEIPT } - .map { it.content.toModel() } - .forEach { readReceiptHandler.handle(realm, roomId, it) } + for (event in ephemeral.events) { + if (event.type != EventType.RECEIPT) continue + val readReceiptContent = event.content as? ReadReceiptContent ?: continue + readReceiptHandler.handle(realm, roomId, readReceiptContent) + } } 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) } diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt index 6145d5a7..1214bfa0 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt @@ -161,7 +161,6 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector { override fun onDestroy() { super.onDestroy() - unBinder?.unbind() unBinder = null 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 f81ef9d3..5ed7bcb3 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 @@ -28,8 +28,15 @@ import im.vector.matrix.android.api.permalinks.MatrixLinkify import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan import im.vector.matrix.android.api.session.events.model.RelationType import im.vector.matrix.android.api.session.events.model.toModel -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.model.message.MessageAudioContent +import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageEmoteContent +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.MessageNoticeContent +import im.vector.matrix.android.api.session.room.model.message.MessageTextContent +import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent +import im.vector.matrix.android.api.session.room.model.message.getFileUrl 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 @@ -47,7 +54,19 @@ import im.vector.riotx.features.home.room.detail.timeline.TimelineEventControlle 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.* +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.html.EventHtmlRenderer import im.vector.riotx.features.media.ImageContentRenderer @@ -84,11 +103,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 ) { // ignore replace event, the targeted id is already edited if (userPreferencesProvider.shouldShowHiddenEvents()) { @@ -116,15 +135,13 @@ class MessageItemFactory @Inject constructor( // val ev = all.toModel() return when (messageContent) { is MessageEmoteContent -> buildEmoteMessageItem(messageContent, - informationData, - highlight, - callback) - is MessageTextContent -> buildTextMessageItem(event.root.sendState, - messageContent, - informationData, - highlight, - callback - ) + informationData, + highlight, + callback) + 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) @@ -158,7 +175,7 @@ class MessageItemFactory @Inject constructor( })) .longClickListener { view -> return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false + ?: false } } @@ -182,7 +199,7 @@ class MessageItemFactory @Inject constructor( })) .longClickListener { view -> return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false + ?: false } .clickListener( DebouncedClickListener(View.OnClickListener { _ -> @@ -190,7 +207,7 @@ class MessageItemFactory @Inject constructor( })) .longClickListener { view -> return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false + ?: false } } @@ -240,7 +257,7 @@ class MessageItemFactory @Inject constructor( })) .longClickListener { view -> return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false + ?: false } } @@ -253,7 +270,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, @@ -288,12 +305,11 @@ class MessageItemFactory @Inject constructor( .clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) } .longClickListener { view -> return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false + ?: false } } - private fun buildTextMessageItem(sendState: SendState, - messageContent: MessageTextContent, + private fun buildTextMessageItem(messageContent: MessageTextContent, informationData: MessageInformationData, highlight: Boolean, callback: TimelineEventController.Callback?): MessageTextItem? { @@ -328,7 +344,7 @@ class MessageItemFactory @Inject constructor( })) .longClickListener { view -> return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false + ?: false } } @@ -358,9 +374,9 @@ class MessageItemFactory @Inject constructor( //nop } }, - editStart, - editEnd, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + editStart, + editEnd, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE) return spannable } @@ -397,7 +413,7 @@ class MessageItemFactory @Inject constructor( })) .longClickListener { view -> return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false + ?: false } } @@ -433,7 +449,7 @@ class MessageItemFactory @Inject constructor( })) .longClickListener { view -> return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false + ?: false } } @@ -452,7 +468,7 @@ class MessageItemFactory @Inject constructor( })) .longClickListener { view -> return@longClickListener callback?.onEventLongClicked(informationData, null, view) - ?: false + ?: 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 670cf471..1462e842 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 @@ -16,6 +16,7 @@ package im.vector.riotx.features.home.room.detail.timeline.item +import android.annotation.SuppressLint import android.graphics.Typeface import android.os.Build import android.view.View @@ -39,6 +40,7 @@ import im.vector.riotx.features.home.room.detail.timeline.TimelineEventControlle import im.vector.riotx.features.reactions.widget.ReactionButton import im.vector.riotx.features.ui.getMessageTextColor +private const val MAX_RECEIPT_DISPLAYED = 5 abstract class AbsMessageItem : BaseEventItem() { @@ -123,6 +125,29 @@ abstract class AbsMessageItem : BaseEventItem() { holder.memberNameView.setOnLongClickListener(null) } + if (informationData.readReceipts.isNotEmpty()) { + holder.readReceiptsView.isVisible = true + for (index in 0 until MAX_RECEIPT_DISPLAYED) { + val receiptData = informationData.readReceipts.getOrNull(index) + if (receiptData == null) { + holder.receiptAvatars[index].isVisible = false + } else { + holder.receiptAvatars[index].isVisible = true + avatarRenderer.render(receiptData.avatarUrl, receiptData.userId, receiptData.displayName, holder.receiptAvatars[index]) + } + } + if (informationData.readReceipts.size > MAX_RECEIPT_DISPLAYED) { + holder.receiptMoreView.isVisible = true + holder.receiptMoreView.text = holder.view.context.getString( + R.string.x_plus, informationData.readReceipts.size - MAX_RECEIPT_DISPLAYED + ) + } else { + holder.receiptMoreView.isVisible = false + } + } else { + holder.readReceiptsView.isVisible = false + } + if (!shouldShowReactionAtBottom() || informationData.orderedReactionList.isNullOrEmpty()) { holder.reactionWrapper?.isVisible = false } else { @@ -173,6 +198,16 @@ 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 readReceiptsView by bind(R.id.readReceiptsView) + val receiptAvatar1 by bind(R.id.message_avatar_receipt_1) + val receiptAvatar2 by bind(R.id.message_avatar_receipt_2) + val receiptAvatar3 by bind(R.id.message_avatar_receipt_3) + val receiptAvatar4 by bind(R.id.message_avatar_receipt_4) + val receiptAvatar5 by bind(R.id.message_avatar_receipt_5) + val receiptMoreView by bind(R.id.message_more_than_expected) + val receiptAvatars: List by lazy { + listOf(receiptAvatar1, receiptAvatar2, receiptAvatar3, receiptAvatar4, receiptAvatar5) + } 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 31b92c0e..679bfbba 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 @@ -17,6 +17,7 @@ package im.vector.riotx.features.home.room.detail.timeline.item import android.os.Parcelable +import im.vector.matrix.android.api.session.room.model.ReadReceipt import im.vector.matrix.android.api.session.room.send.SendState import kotlinx.android.parcel.Parcelize @@ -32,7 +33,8 @@ data class MessageInformationData( /*List of reactions (emoji,count,isSelected)*/ val orderedReactionList: List? = null, val hasBeenEdited: Boolean = false, - val hasPendingEdits: Boolean = false + val hasPendingEdits: Boolean = false, + val readReceipts: List = emptyList() ) : Parcelable @@ -43,3 +45,11 @@ data class ReactionInfoData( val addedByMe: Boolean, val synced: Boolean ) : Parcelable + +@Parcelize +data class ReadReceiptData( + val userId: String, + val avatarUrl: String?, + val displayName: String?, + val timestamp: Long +) : Parcelable \ No newline at end of file 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 fe15c5d2..2f6a4320 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 @@ -16,6 +16,7 @@ package im.vector.riotx.features.home.room.detail.timeline.util +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.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited @@ -26,13 +27,15 @@ import im.vector.riotx.features.home.getColorFromUserId import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFormatter 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 import me.gujun.android.span.span import javax.inject.Inject /** * This class compute if data of an event (such has avatar, display name, ...) should be displayed, depending on the previous event in the timeline */ -class MessageInformationDataFactory @Inject constructor(private val timelineDateFormatter: TimelineDateFormatter, +class MessageInformationDataFactory @Inject constructor(private val session: Session, + private val timelineDateFormatter: TimelineDateFormatter, private val colorProvider: ColorProvider) { fun create(event: TimelineEvent, nextEvent: TimelineEvent?): MessageInformationData { @@ -43,21 +46,21 @@ class MessageInformationDataFactory @Inject constructor(private val timelineDate val nextDate = nextEvent?.root?.localDateTime() val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate() val isNextMessageReceivedMoreThanOneHourAgo = nextDate?.isBefore(date.minusMinutes(60)) - ?: false + ?: false val showInformation = addDaySeparator - || event.senderAvatar != nextEvent?.senderAvatar - || event.getDisambiguatedDisplayName() != nextEvent?.getDisambiguatedDisplayName() - || (nextEvent?.root?.getClearType() != EventType.MESSAGE && nextEvent?.root?.getClearType() != EventType.ENCRYPTED) - || isNextMessageReceivedMoreThanOneHourAgo + || event.senderAvatar != nextEvent?.senderAvatar + || event.getDisambiguatedDisplayName() != nextEvent?.getDisambiguatedDisplayName() + || (nextEvent?.root?.getClearType() != EventType.MESSAGE && nextEvent?.root?.getClearType() != EventType.ENCRYPTED) + || isNextMessageReceivedMoreThanOneHourAgo val time = timelineDateFormatter.formatMessageHour(date) val avatarUrl = event.senderAvatar val memberName = event.getDisambiguatedDisplayName() val formattedMemberName = span(memberName) { textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId - ?: "")) + ?: "")) } return MessageInformationData( @@ -74,7 +77,14 @@ class MessageInformationDataFactory @Inject constructor(private val timelineDate ReactionInfoData(it.key, it.count, it.addedByMe, it.localEchoEvents.isEmpty()) }, hasBeenEdited = event.hasBeenEdited(), - hasPendingEdits = event.annotations?.editSummary?.localEchos?.any() ?: false + hasPendingEdits = event.annotations?.editSummary?.localEchos?.any() ?: false, + readReceipts = event.readReceipts + .filter { + it.user.userId != session.myUserId + } + .map { + ReadReceiptData(it.user.userId, it.user.avatarUrl, it.user.displayName, it.originServerTs) + } ) } } \ 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 7f15c60b..a8b81606 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -114,7 +114,7 @@ android:inflatedId="@+id/messageBottomInfo" android:layout="@layout/item_timeline_event_bottom_reactions_stub" android:visibility="gone" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@+id/readReceiptsView" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/messageStartGuideline" app:layout_constraintVertical_chainStyle="packed" @@ -123,4 +123,65 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/view_read_receipts.xml b/vector/src/main/res/layout/view_read_receipts.xml new file mode 100644 index 00000000..4f65a82a --- /dev/null +++ b/vector/src/main/res/layout/view_read_receipts.xml @@ -0,0 +1,11 @@ + + + + + + + From d98567045cea85a4a55d334729a108bdde88cc29 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 8 Aug 2019 15:03:36 +0200 Subject: [PATCH 02/40] Read receipts: use a simpler strategy when it's initialSync --- .../session/sync/ReadReceiptHandler.kt | 46 +++++++++++++++++-- .../internal/session/sync/RoomSyncHandler.kt | 22 +++++---- .../session/sync/SyncResponseHandler.kt | 2 +- 3 files changed, 54 insertions(+), 16 deletions(-) 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 055334b1..35acb527 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 @@ -31,27 +31,62 @@ import javax.inject.Inject // dict value ts value typealias ReadReceiptContent = Map>>> +private const val READ_KEY = "m.read" +private const val TIMESTAMP_KEY = "ts" + internal class ReadReceiptHandler @Inject constructor() { - fun handle(realm: Realm, roomId: String, content: ReadReceiptContent?) { + fun handle(realm: Realm, roomId: String, content: ReadReceiptContent?, isInitialSync: Boolean) { if (content == null) { return } try { - handleReadReceiptContent(realm, roomId, content) + handleReadReceiptContent(realm, roomId, content, isInitialSync) } catch (exception: Exception) { Timber.e("Fail to handle read receipt for room $roomId") } } - private fun handleReadReceiptContent(realm: Realm, roomId: String, content: ReadReceiptContent) { + private fun handleReadReceiptContent(realm: Realm, roomId: String, content: ReadReceiptContent, isInitialSync: Boolean) { + if (isInitialSync) { + initialSyncStrategy(realm, roomId, content) + } else { + incrementalSyncStrategy(realm, roomId, content) + } + } + + + private fun initialSyncStrategy(realm: Realm, roomId: String, content: ReadReceiptContent) { + val readReceiptSummaries = ArrayList() for ((eventId, receiptDict) in content) { - val userIdsDict = receiptDict["m.read"] ?: continue + val userIdsDict = receiptDict[READ_KEY] ?: continue + val readReceiptsSummary = ReadReceiptsSummaryEntity(eventId = eventId) + + for ((userId, paramsDict) in userIdsDict) { + val ts = paramsDict[TIMESTAMP_KEY] ?: 0.0 + val primaryKey = "${roomId}_$userId" + val receiptEntity = ReadReceiptEntity().apply { + this.primaryKey = primaryKey + this.eventId = eventId + this.roomId = roomId + this.userId = userId + this.originServerTs = ts + } + readReceiptsSummary.readReceipts.add(receiptEntity) + } + readReceiptSummaries.add(readReceiptsSummary) + } + realm.insertOrUpdate(readReceiptSummaries) + } + + private fun incrementalSyncStrategy(realm: Realm, roomId: String, content: ReadReceiptContent) { + for ((eventId, receiptDict) in content) { + val userIdsDict = receiptDict[READ_KEY] ?: continue val readReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst() ?: realm.createObject(ReadReceiptsSummaryEntity::class.java, eventId) for ((userId, paramsDict) in userIdsDict) { - val ts = paramsDict["ts"] ?: 0.0 + val ts = paramsDict[TIMESTAMP_KEY] ?: 0.0 val primaryKey = "${roomId}_$userId" val receiptEntity = ReadReceiptEntity.where(realm, roomId, userId).findFirst() ?: realm.createObject(ReadReceiptEntity::class.java, primaryKey) @@ -69,4 +104,5 @@ internal class ReadReceiptHandler @Inject constructor() { } } } + } \ 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 a16cae18..4a243adf 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 @@ -62,11 +62,11 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch data class LEFT(val data: Map) : HandlingStrategy() } - fun handle(roomsSyncResponse: RoomsSyncResponse, reporter: DefaultInitialSyncProgressService? = null) { + fun handle(roomsSyncResponse: RoomsSyncResponse, isInitialSync: Boolean, reporter: DefaultInitialSyncProgressService? = null) { monarchy.runTransactionSync { realm -> - handleRoomSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), reporter) - handleRoomSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), reporter) - handleRoomSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), reporter) + handleRoomSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), isInitialSync, reporter) + handleRoomSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), isInitialSync, reporter) + handleRoomSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), isInitialSync, reporter) } //handle event for bing rule checks @@ -89,12 +89,12 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch // PRIVATE METHODS ***************************************************************************** - private fun handleRoomSync(realm: Realm, handlingStrategy: HandlingStrategy, reporter: DefaultInitialSyncProgressService?) { + private fun handleRoomSync(realm: Realm, handlingStrategy: HandlingStrategy, isInitialSync: Boolean, reporter: DefaultInitialSyncProgressService?) { val rooms = when (handlingStrategy) { is HandlingStrategy.JOINED -> handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_joined_rooms, 0.6f) { - handleJoinedRoom(realm, it.key, it.value) + handleJoinedRoom(realm, it.key, it.value, isInitialSync) } is HandlingStrategy.INVITED -> handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_invited_rooms, 0.4f) { @@ -112,7 +112,8 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch private fun handleJoinedRoom(realm: Realm, roomId: String, - roomSync: RoomSync): RoomEntity { + roomSync: RoomSync, + isInitalSync: Boolean): RoomEntity { Timber.v("Handle join sync for room $roomId") @@ -152,7 +153,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch roomSummaryUpdater.update(realm, roomId, Membership.JOIN, roomSync.summary, roomSync.unreadNotifications) if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) { - handleEphemeral(realm, roomId, roomSync.ephemeral) + handleEphemeral(realm, roomId, roomSync.ephemeral, isInitalSync) } if (roomSync.accountData != null && roomSync.accountData.events.isNullOrEmpty().not()) { @@ -236,11 +237,12 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch @Suppress("UNCHECKED_CAST") private fun handleEphemeral(realm: Realm, roomId: String, - ephemeral: RoomSyncEphemeral) { + ephemeral: RoomSyncEphemeral, + isInitalSync: Boolean) { for (event in ephemeral.events) { if (event.type != EventType.RECEIPT) continue val readReceiptContent = event.content as? ReadReceiptContent ?: continue - readReceiptHandler.handle(realm, roomId, readReceiptContent) + readReceiptHandler.handle(realm, roomId, readReceiptContent, isInitalSync) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt index fafa758c..991e5a9a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt @@ -66,7 +66,7 @@ internal class SyncResponseHandler @Inject constructor(private val roomSyncHandl reportSubtask(reporter, R.string.initial_sync_start_importing_account_rooms, 100, 0.7f) { if (syncResponse.rooms != null) { - roomSyncHandler.handle(syncResponse.rooms, reporter) + roomSyncHandler.handle(syncResponse.rooms, isInitialSync, reporter) } } }.also { From 347dcb469ab6b868c6cc6eb429e7b8144b10dfc1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 8 Aug 2019 16:47:13 +0200 Subject: [PATCH 03/40] Version++ --- CHANGES.md | 21 +++++++++++++++++++++ vector/build.gradle | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index c5f686b2..b8fa02b9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,24 @@ +Changes in RiotX 0.4.0 (2019-XX-XX) +=================================================== + +Features: + - + +Improvements: + - + +Other changes: + - + +Bugfix: + - + +Translations: + - + +Build: + - + Changes in RiotX 0.3.0 (2019-08-08) =================================================== diff --git a/vector/build.gradle b/vector/build.gradle index 74e6d132..60a4542d 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -73,7 +73,7 @@ android { // Note: versionCode is depending on the build variant - versionName "${versionMajor}.${versionMinor}.${versionPatch}" + versionName "${versionMajor}.${versionMinor}.${versionPatch}-dev" buildConfigField "String", "GIT_REVISION", "\"${gitRevision()}\"" resValue "string", "git_revision", "\"${gitRevision()}\"" From 3f792c7a840da45365b7b176a794f9deb019a1b7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 8 Aug 2019 16:57:03 +0200 Subject: [PATCH 04/40] Automatic "-dev" version suffix on non master branch --- vector/build.gradle | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/vector/build.gradle b/vector/build.gradle index 60a4542d..8393462c 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -47,6 +47,14 @@ static def gitBranchName() { return cmd.execute().text.trim() } +static def getVersionSuffix() { + if (gitBranchName() == "master") { + return "" + } else { + return "-dev" + } +} + project.android.buildTypes.all { buildType -> buildType.javaCompileOptions.annotationProcessorOptions.arguments = [ @@ -73,7 +81,7 @@ android { // Note: versionCode is depending on the build variant - versionName "${versionMajor}.${versionMinor}.${versionPatch}-dev" + versionName "${versionMajor}.${versionMinor}.${versionPatch}${getVersionSuffix()}" buildConfigField "String", "GIT_REVISION", "\"${gitRevision()}\"" resValue "string", "git_revision", "\"${gitRevision()}\"" From 39f58d048b61576ce336c359bbd11a416090bc01 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 8 Aug 2019 17:49:31 +0200 Subject: [PATCH 05/40] Read receipts: fix dummy being overrided --- .../database/helper/ChunkEntityHelper.kt | 28 +++++++++------ .../query/ReadReceiptEntityQueries.kt | 20 ++++++++++- .../query/ReadReceiptsSummaryEntityQueries.kt | 2 +- .../session/sync/ReadReceiptHandler.kt | 34 +++++++------------ .../internal/session/sync/RoomSyncHandler.kt | 16 ++++----- 5 files changed, 58 insertions(+), 42 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 5a76741e..a541e8cf 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 @@ -28,6 +28,7 @@ import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntit 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.find +import im.vector.matrix.android.internal.database.query.getOrCreate import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.extensions.assertIsManaged import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection @@ -138,20 +139,25 @@ internal fun ChunkEntity.add(roomId: String, val eventId = event.eventId ?: "" val senderId = event.senderId ?: "" - val currentReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst() - ?: ReadReceiptsSummaryEntity(eventId) + val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst() + ?: ReadReceiptsSummaryEntity(eventId) - // Update RR for the sender of a new message - if (direction == PaginationDirection.FORWARDS && !isUnlinked) { - ReadReceiptEntity.where(realm, roomId = roomId, userId = senderId).findFirst()?.also { - val previousEventId = it.eventId - val previousReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId = previousEventId).findFirst() - it.eventId = eventId - previousReceiptsSummary?.readReceipts?.remove(it) - currentReceiptsSummary.readReceipts.add(it) + // Update RR for the sender of a new message with a dummy one + + if (event.originServerTs != null) { + val timestampOfEvent = event.originServerTs.toDouble() + val readReceiptOfSender = ReadReceiptEntity.getOrCreate(realm, roomId = roomId, userId = senderId) + // If the synced RR is older, update + if (timestampOfEvent > readReceiptOfSender.originServerTs) { + val previousReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId = readReceiptOfSender.eventId).findFirst() + readReceiptOfSender.eventId = eventId + readReceiptOfSender.originServerTs = timestampOfEvent + previousReceiptsSummary?.readReceipts?.remove(readReceiptOfSender) + readReceiptsSummaryEntity.readReceipts.add(readReceiptOfSender) } } + val eventEntity = TimelineEventEntity(localId).also { it.root = event.toEntity(roomId).apply { this.stateIndex = currentStateIndex @@ -162,7 +168,7 @@ internal fun ChunkEntity.add(roomId: String, it.eventId = eventId it.roomId = roomId it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() - it.readReceipts = currentReceiptsSummary + it.readReceipts = readReceiptsSummaryEntity } 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/query/ReadReceiptEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadReceiptEntityQueries.kt index e5a1afb6..acac4199 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 @@ -26,4 +26,22 @@ internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, use return realm.where() .equalTo(ReadReceiptEntityFields.ROOM_ID, roomId) .equalTo(ReadReceiptEntityFields.USER_ID, userId) -} \ No newline at end of file +} + +internal fun ReadReceiptEntity.Companion.createUnmanaged(roomId: String, eventId: String, userId: String, originServerTs: Double): ReadReceiptEntity { + return ReadReceiptEntity().apply { + this.primaryKey = "${roomId}_$userId" + this.eventId = eventId + this.roomId = roomId + this.userId = userId + this.originServerTs = originServerTs + } +} + +internal fun ReadReceiptEntity.Companion.getOrCreate(realm: Realm, roomId: String, userId: String): ReadReceiptEntity { + return ReadReceiptEntity.where(realm, roomId, userId).findFirst() + ?: realm.createObject(ReadReceiptEntity::class.java, "${roomId}_$userId").apply { + this.roomId = roomId + this.userId = userId + } +} 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 e6c1e685..d04ced11 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 @@ -25,4 +25,4 @@ import io.realm.kotlin.where internal fun ReadReceiptsSummaryEntity.Companion.where(realm: Realm, eventId: String): RealmQuery { return realm.where() .equalTo(ReadReceiptsSummaryEntityFields.EVENT_ID, eventId) -} \ 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 35acb527..5098e824 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 @@ -18,6 +18,8 @@ package im.vector.matrix.android.internal.session.sync 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.query.createUnmanaged +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 @@ -64,14 +66,7 @@ internal class ReadReceiptHandler @Inject constructor() { for ((userId, paramsDict) in userIdsDict) { val ts = paramsDict[TIMESTAMP_KEY] ?: 0.0 - val primaryKey = "${roomId}_$userId" - val receiptEntity = ReadReceiptEntity().apply { - this.primaryKey = primaryKey - this.eventId = eventId - this.roomId = roomId - this.userId = userId - this.originServerTs = ts - } + val receiptEntity = ReadReceiptEntity.createUnmanaged(roomId, eventId, userId, ts) readReceiptsSummary.readReceipts.add(receiptEntity) } readReceiptSummaries.add(readReceiptsSummary) @@ -87,22 +82,19 @@ internal class ReadReceiptHandler @Inject constructor() { for ((userId, paramsDict) in userIdsDict) { val ts = paramsDict[TIMESTAMP_KEY] ?: 0.0 - val primaryKey = "${roomId}_$userId" - val receiptEntity = ReadReceiptEntity.where(realm, roomId, userId).findFirst() - ?: realm.createObject(ReadReceiptEntity::class.java, primaryKey) - - ReadReceiptsSummaryEntity.where(realm, receiptEntity.eventId).findFirst()?.also { - it.readReceipts.remove(receiptEntity) + val receiptEntity = ReadReceiptEntity.getOrCreate(realm, roomId, userId) + // ensure new ts is superior to the previous one + if (ts > receiptEntity.originServerTs) { + ReadReceiptsSummaryEntity.where(realm, receiptEntity.eventId).findFirst()?.also { + it.readReceipts.remove(receiptEntity) + } + receiptEntity.eventId = eventId + receiptEntity.originServerTs = ts + readReceiptsSummary.readReceipts.add(receiptEntity) } - receiptEntity.apply { - this.eventId = eventId - this.roomId = roomId - this.userId = userId - this.originServerTs = ts - } - readReceiptsSummary.readReceipts.add(receiptEntity) } } } + } \ 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 4a243adf..74b56e77 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 @@ -117,6 +117,14 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch Timber.v("Handle join sync for room $roomId") + if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) { + handleEphemeral(realm, roomId, roomSync.ephemeral, isInitalSync) + } + + if (roomSync.accountData != null && roomSync.accountData.events.isNullOrEmpty().not()) { + handleRoomAccountDataEvents(realm, roomId, roomSync.accountData) + } + val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) @@ -151,14 +159,6 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch roomEntity.addOrUpdate(chunkEntity) } roomSummaryUpdater.update(realm, roomId, Membership.JOIN, roomSync.summary, roomSync.unreadNotifications) - - if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) { - handleEphemeral(realm, roomId, roomSync.ephemeral, isInitalSync) - } - - if (roomSync.accountData != null && roomSync.accountData.events.isNullOrEmpty().not()) { - handleRoomAccountDataEvents(realm, roomId, roomSync.accountData) - } return roomEntity } From c313ce78cb58d8f9a56550396873ead9ef46bb06 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 8 Aug 2019 17:49:50 +0200 Subject: [PATCH 06/40] Read receipts: sort descending by timestamp --- .../mapper/ReadReceiptsSummaryMapper.kt | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/ReadReceiptsSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/ReadReceiptsSummaryMapper.kt index 3887f3ac..b7cdabfc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/ReadReceiptsSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/ReadReceiptsSummaryMapper.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.database.mapper import im.vector.matrix.android.api.session.room.model.ReadReceipt +import im.vector.matrix.android.internal.database.model.ReadReceiptEntityFields import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity import im.vector.matrix.android.internal.database.model.UserEntity import im.vector.matrix.android.internal.database.query.where @@ -25,15 +26,20 @@ import io.realm.Realm import io.realm.RealmConfiguration import javax.inject.Inject -internal class ReadReceiptsSummaryMapper @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration){ +internal class ReadReceiptsSummaryMapper @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration) { fun map(readReceiptsSummaryEntity: ReadReceiptsSummaryEntity): List { return Realm.getInstance(realmConfiguration).use { realm -> - readReceiptsSummaryEntity.readReceipts.mapNotNull { - val user = UserEntity.where(realm, it.userId).findFirst() - ?: return@mapNotNull null - ReadReceipt(user.asDomain(), it.originServerTs.toLong()) - } + val readReceipts = readReceiptsSummaryEntity.readReceipts + readReceipts + .mapNotNull { + val user = UserEntity.where(realm, it.userId).findFirst() + ?: return@mapNotNull null + ReadReceipt(user.asDomain(), it.originServerTs.toLong()) + } + .sortedByDescending { + it.originServerTs + } } } From 825463d9cdae36bda1018da253fab7e01f2ce5cf Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 8 Aug 2019 17:50:16 +0200 Subject: [PATCH 07/40] Change package for NotificationAreaView --- .../core/{platform => ui/views}/NotificationAreaView.kt | 5 +---- .../riotx/features/home/room/detail/RoomDetailFragment.kt | 2 +- vector/src/main/res/layout/fragment_room_detail.xml | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) rename vector/src/main/java/im/vector/riotx/core/{platform => ui/views}/NotificationAreaView.kt (97%) diff --git a/vector/src/main/java/im/vector/riotx/core/platform/NotificationAreaView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/NotificationAreaView.kt similarity index 97% rename from vector/src/main/java/im/vector/riotx/core/platform/NotificationAreaView.kt rename to vector/src/main/java/im/vector/riotx/core/ui/views/NotificationAreaView.kt index a321fd1a..b159b7b0 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/NotificationAreaView.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/NotificationAreaView.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.riotx.core.platform +package im.vector.riotx.core.ui.views import android.content.Context import android.graphics.Color @@ -32,10 +32,7 @@ import androidx.core.content.ContextCompat import butterknife.BindView import butterknife.ButterKnife import im.vector.matrix.android.api.failure.MatrixError -import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent import im.vector.riotx.R import im.vector.riotx.core.error.ResourceLimitErrorFormatter import im.vector.riotx.features.themes.ThemeUtils 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 4d691f50..fad93d38 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 @@ -76,7 +76,7 @@ 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.platform.NotificationAreaView +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.features.autocomplete.command.AutocompleteCommandPresenter diff --git a/vector/src/main/res/layout/fragment_room_detail.xml b/vector/src/main/res/layout/fragment_room_detail.xml index 81a5b33d..d84c4637 100644 --- a/vector/src/main/res/layout/fragment_room_detail.xml +++ b/vector/src/main/res/layout/fragment_room_detail.xml @@ -98,7 +98,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> - Date: Thu, 8 Aug 2019 17:51:06 +0200 Subject: [PATCH 08/40] Read receipts: create custom view to use it wherever we want easily --- .../riotx/core/ui/views/ReadReceiptsView.kt | 77 +++++++++++++++++++ .../timeline/factory/NoticeItemFactory.kt | 13 +--- .../detail/timeline/item/AbsMessageItem.kt | 38 +-------- .../room/detail/timeline/item/NoticeItem.kt | 3 + .../res/layout/item_timeline_event_base.xml | 59 +------------- .../item_timeline_event_base_noinfo.xml | 9 +++ .../main/res/layout/view_read_receipts.xml | 57 ++++++++++++-- vector/src/main/res/values/styles_riot.xml | 1 + 8 files changed, 153 insertions(+), 104 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt new file mode 100644 index 00000000..12d58614 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt @@ -0,0 +1,77 @@ +/* + * 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.widget.ImageView +import android.widget.LinearLayout +import androidx.core.view.isVisible +import butterknife.ButterKnife +import im.vector.riotx.R +import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData +import kotlinx.android.synthetic.main.view_read_receipts.view.* + +private const val MAX_RECEIPT_DISPLAYED = 5 + +class ReadReceiptsView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : LinearLayout(context, attrs, defStyleAttr) { + + private val receiptAvatars: List by lazy { + listOf(receiptAvatar1, receiptAvatar2, receiptAvatar3, receiptAvatar4, receiptAvatar5) + } + + init { + setupView() + } + + private fun setupView() { + inflate(context, R.layout.view_read_receipts, this) + ButterKnife.bind(this) + } + + fun render(readReceipts: List, avatarRenderer: AvatarRenderer) { + if (readReceipts.isNotEmpty()) { + isVisible = true + for (index in 0 until MAX_RECEIPT_DISPLAYED) { + val receiptData = readReceipts.getOrNull(index) + if (receiptData == null) { + receiptAvatars[index].isVisible = false + } else { + receiptAvatars[index].isVisible = true + avatarRenderer.render(receiptData.avatarUrl, receiptData.userId, receiptData.displayName, receiptAvatars[index]) + } + } + if (readReceipts.size > MAX_RECEIPT_DISPLAYED) { + receiptMore.isVisible = true + receiptMore.text = context.getString( + R.string.x_plus, readReceipts.size - MAX_RECEIPT_DISPLAYED + ) + } else { + receiptMore.isVisible = false + } + } else { + isVisible = false + } + + } + +} 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 52771ad6..b1cb5407 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 @@ -25,23 +25,18 @@ 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 im.vector.riotx.features.home.room.detail.timeline.util.MessageInformationDataFactory import javax.inject.Inject class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEventFormatter, - private val avatarRenderer: AvatarRenderer) { + private val avatarRenderer: AvatarRenderer, + private val informationDataFactory: MessageInformationDataFactory) { fun create(event: TimelineEvent, highlight: Boolean, callback: TimelineEventController.Callback?): NoticeItem? { val formattedText = eventFormatter.format(event) ?: 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 - ) + val informationData = informationDataFactory.create(event, null) return NoticeItem_() .avatarRenderer(avatarRenderer) 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 1462e842..51d2ce92 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 @@ -33,6 +33,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.ReadReceiptsView import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.core.utils.DimensionUtils.dpToPx import im.vector.riotx.features.home.AvatarRenderer @@ -40,8 +41,6 @@ import im.vector.riotx.features.home.room.detail.timeline.TimelineEventControlle import im.vector.riotx.features.reactions.widget.ReactionButton import im.vector.riotx.features.ui.getMessageTextColor -private const val MAX_RECEIPT_DISPLAYED = 5 - abstract class AbsMessageItem : BaseEventItem() { @EpoxyAttribute @@ -125,28 +124,7 @@ abstract class AbsMessageItem : BaseEventItem() { holder.memberNameView.setOnLongClickListener(null) } - if (informationData.readReceipts.isNotEmpty()) { - holder.readReceiptsView.isVisible = true - for (index in 0 until MAX_RECEIPT_DISPLAYED) { - val receiptData = informationData.readReceipts.getOrNull(index) - if (receiptData == null) { - holder.receiptAvatars[index].isVisible = false - } else { - holder.receiptAvatars[index].isVisible = true - avatarRenderer.render(receiptData.avatarUrl, receiptData.userId, receiptData.displayName, holder.receiptAvatars[index]) - } - } - if (informationData.readReceipts.size > MAX_RECEIPT_DISPLAYED) { - holder.receiptMoreView.isVisible = true - holder.receiptMoreView.text = holder.view.context.getString( - R.string.x_plus, informationData.readReceipts.size - MAX_RECEIPT_DISPLAYED - ) - } else { - holder.receiptMoreView.isVisible = false - } - } else { - holder.readReceiptsView.isVisible = false - } + holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer) if (!shouldShowReactionAtBottom() || informationData.orderedReactionList.isNullOrEmpty()) { holder.reactionWrapper?.isVisible = false @@ -198,17 +176,7 @@ 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 readReceiptsView by bind(R.id.readReceiptsView) - val receiptAvatar1 by bind(R.id.message_avatar_receipt_1) - val receiptAvatar2 by bind(R.id.message_avatar_receipt_2) - val receiptAvatar3 by bind(R.id.message_avatar_receipt_3) - val receiptAvatar4 by bind(R.id.message_avatar_receipt_4) - val receiptAvatar5 by bind(R.id.message_avatar_receipt_5) - val receiptMoreView by bind(R.id.message_more_than_expected) - val receiptAvatars: List by lazy { - listOf(receiptAvatar1, receiptAvatar2, receiptAvatar3, receiptAvatar4, receiptAvatar5) - } - + val readReceiptsView by bind(R.id.readReceiptsView) var reactionWrapper: ViewGroup? = null var reactionFlowHelper: Flow? = null } 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 2879073f..b18a665d 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 @@ -22,6 +22,7 @@ import android.widget.TextView import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R +import im.vector.riotx.core.ui.views.ReadReceiptsView import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController @@ -55,6 +56,7 @@ abstract class NoticeItem : BaseEventItem() { holder.avatarImageView ) holder.view.setOnLongClickListener(longClickListener) + holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer) } override fun getViewType() = STUB_ID @@ -62,6 +64,7 @@ abstract class NoticeItem : BaseEventItem() { class Holder : BaseHolder(STUB_ID) { val avatarImageView by bind(R.id.itemNoticeAvatarView) val noticeTextView by bind(R.id.itemNoticeTextView) + val readReceiptsView by bind(R.id.readReceiptsView) } companion object { 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 a8b81606..2f0be78f 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -123,65 +123,14 @@ - - - - - - - - - - - - - - - + app:layout_constraintEnd_toEndOf="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 004428ec..77268399 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 @@ -52,5 +52,14 @@ android:layout="@layout/item_timeline_event_merged_header_stub" tools:ignore="MissingConstraints" /> + + \ No newline at end of file diff --git a/vector/src/main/res/layout/view_read_receipts.xml b/vector/src/main/res/layout/view_read_receipts.xml index 4f65a82a..e3cbc6ba 100644 --- a/vector/src/main/res/layout/view_read_receipts.xml +++ b/vector/src/main/res/layout/view_read_receipts.xml @@ -1,11 +1,58 @@ - + android:layout_width="wrap_content" + android:layout_height="wrap_content" + tools:parentTag="android.widget.LinearLayout"> + + - + + + + + + + + + diff --git a/vector/src/main/res/values/styles_riot.xml b/vector/src/main/res/values/styles_riot.xml index eaf0530b..80f5148a 100644 --- a/vector/src/main/res/values/styles_riot.xml +++ b/vector/src/main/res/values/styles_riot.xml @@ -297,6 +297,7 @@ From 70639f180cd0fe5276c33d7a44b1fe21d7e47877 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 8 Aug 2019 19:59:20 +0200 Subject: [PATCH 09/40] Read receipts: add read receipts bottom sheet --- .../main/java/im/vector/matrix/rx/RxRoom.kt | 5 + .../api/session/room/read/ReadService.kt | 4 + .../internal/session/room/RoomFactory.kt | 4 +- .../session/room/read/DefaultReadService.kt | 28 +++++- .../date/VectorDateFormatter.kt} | 14 ++- .../vector/riotx/core/di/ScreenComponent.kt | 9 +- .../vector/riotx/core/di/ViewModelModule.kt | 23 ++++- .../riotx/core/ui/views/ReadReceiptsView.kt | 4 +- .../home/room/detail/RoomDetailFragment.kt | 54 ++++++----- .../readreceipts/DisplayReadReceiptItem.kt | 55 +++++++++++ .../DisplayReadReceiptsBottomSheet.kt | 93 +++++++++++++++++++ .../DisplayReadReceiptsController.kt | 71 ++++++++++++++ .../DisplayReadReceiptsViewModel.kt | 63 +++++++++++++ .../DisplayReadReceiptsViewState.kt | 33 +++++++ .../timeline/TimelineEventController.kt | 18 ++-- .../action/ViewEditHistoryBottomSheet.kt | 2 +- .../action/ViewEditHistoryEpoxyController.kt | 8 +- .../action/ViewEditHistoryViewModel.kt | 4 +- .../timeline/action/ViewReactionViewModel.kt | 20 ++-- .../action/ViewReactionsEpoxyController.kt | 1 + .../timeline/factory/MessageItemFactory.kt | 12 ++- .../timeline/factory/NoticeItemFactory.kt | 1 + .../detail/timeline/item/AbsMessageItem.kt | 9 +- .../timeline/item/MessageInformationData.kt | 1 - .../room/detail/timeline/item/NoticeItem.kt | 10 +- .../util/MessageInformationDataFactory.kt | 8 +- .../home/room/list/RoomSummaryItemFactory.kt | 11 +-- .../res/layout/item_display_read_receipt.xml | 43 +++++++++ 28 files changed, 535 insertions(+), 73 deletions(-) rename vector/src/main/java/im/vector/riotx/{features/home/room/detail/timeline/helper/TimelineDateFormatter.kt => core/date/VectorDateFormatter.kt} (69%) create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptItem.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsController.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewModel.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewState.kt create mode 100644 vector/src/main/res/layout/item_display_read_receipt.xml 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 201622b3..0ff0987d 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 @@ -18,6 +18,7 @@ package im.vector.matrix.rx import im.vector.matrix.android.api.session.room.Room 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 io.reactivex.Observable @@ -49,6 +50,10 @@ class RxRoom(private val room: Room) { room.join(viaServers, MatrixCallbackSingle(it)).toSingle(it) } + fun liveEventReadReceipts(eventId: String): Observable> { + return room.getEventReadReceiptsLive(eventId).asObservable() + } + } fun Room.rx(): RxRoom { 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 ab406b8e..d97fc497 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 @@ -16,7 +16,9 @@ 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 /** * This interface defines methods to handle read receipts and read marker in a room. It's implemented at the room level. @@ -39,4 +41,6 @@ interface ReadService { fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback) fun isEventRead(eventId: String): Boolean + + fun getEventReadReceiptsLive(eventId: String): LiveData> } \ No newline at end of file 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 dd5d2d3b..cf2627b0 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 @@ -21,6 +21,7 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.room.Room +import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService @@ -49,6 +50,7 @@ internal class RoomFactory @Inject constructor(private val context: Context, private val eventFactory: LocalEchoEventFactory, private val roomSummaryMapper: RoomSummaryMapper, private val timelineEventMapper: TimelineEventMapper, + private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, private val taskExecutor: TaskExecutor, private val loadRoomMembersTask: LoadRoomMembersTask, private val inviteTask: InviteTask, @@ -67,7 +69,7 @@ internal class RoomFactory @Inject constructor(private val context: Context, val sendService = DefaultSendService(context, credentials, roomId, eventFactory, cryptoService, monarchy) val stateService = DefaultStateService(roomId, monarchy.realmConfiguration, taskExecutor, sendStateTask) val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask) - val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask, credentials) + val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask, readReceiptsSummaryMapper, credentials) val relationService = DefaultRelationService(context, credentials, roomId, eventFactory, cryptoService, findReactionEventForUndoTask, fetchEditHistoryTask, monarchy, taskExecutor) 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 2e30c12e..3df872bf 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 @@ -16,12 +16,21 @@ package im.vector.matrix.android.internal.session.room.read +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations import com.zhuinden.monarchy.Monarchy 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.EventAnnotationsSummary +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.internal.database.RealmLiveData +import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper +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.EventAnnotationsSummaryEntity 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.query.find import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom import im.vector.matrix.android.internal.database.query.where @@ -33,6 +42,7 @@ internal class DefaultReadService @Inject constructor(private val roomId: String private val monarchy: Monarchy, private val taskExecutor: TaskExecutor, private val setReadMarkersTask: SetReadMarkersTask, + private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, private val credentials: Credentials) : ReadService { override fun markAllAsRead(callback: MatrixCallback) { @@ -67,16 +77,26 @@ internal class DefaultReadService @Inject constructor(private val roomId: String 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 getEventReadReceiptsLive(eventId: String): LiveData> { + val liveEntity = RealmLiveData(monarchy.realmConfiguration) { realm -> + ReadReceiptsSummaryEntity.where(realm, eventId) + } + return Transformations.map(liveEntity) { realmResults -> + realmResults.firstOrNull()?.let { + readReceiptsSummaryMapper.map(it) + } ?: emptyList() + } + } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDateFormatter.kt b/vector/src/main/java/im/vector/riotx/core/date/VectorDateFormatter.kt similarity index 69% rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDateFormatter.kt rename to vector/src/main/java/im/vector/riotx/core/date/VectorDateFormatter.kt index a1104e32..540400d5 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDateFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/core/date/VectorDateFormatter.kt @@ -14,15 +14,18 @@ * limitations under the License. */ -package im.vector.riotx.features.home.room.detail.timeline.helper +package im.vector.riotx.core.date +import android.content.Context +import android.text.format.DateUtils import im.vector.riotx.core.resources.LocaleProvider import org.threeten.bp.LocalDateTime import org.threeten.bp.format.DateTimeFormatter import javax.inject.Inject -class TimelineDateFormatter @Inject constructor (private val localeProvider: LocaleProvider) { +class VectorDateFormatter @Inject constructor(private val context: Context, + private val localeProvider: LocaleProvider) { private val messageHourFormatter by lazy { DateTimeFormatter.ofPattern("H:mm", localeProvider.current()) @@ -39,4 +42,11 @@ class TimelineDateFormatter @Inject constructor (private val localeProvider: Loc return messageDayFormatter.format(localDateTime) } + fun formatRelativeDateTime(time: Long?): String { + if (time == null) { + return "" + } + return DateUtils.getRelativeDateTimeString(context, time, DateUtils.DAY_IN_MILLIS, 2 * DateUtils.DAY_IN_MILLIS, DateUtils.FORMAT_SHOW_WEEKDAY).toString() + } + } \ No newline at end of file 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 35cda2e6..d5445862 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,7 +41,12 @@ import im.vector.riotx.features.home.createdirect.CreateDirectRoomDirectoryUsers import im.vector.riotx.features.home.createdirect.CreateDirectRoomKnownUsersFragment 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.timeline.action.* +import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet +import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet +import im.vector.riotx.features.home.room.detail.timeline.action.MessageMenuFragment +import im.vector.riotx.features.home.room.detail.timeline.action.QuickReactionFragment +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.filtered.FilteredRoomsActivity import im.vector.riotx.features.home.room.list.RoomListFragment import im.vector.riotx.features.invite.VectorInviteView @@ -165,6 +170,8 @@ interface ScreenComponent { fun inject(createDirectRoomActivity: CreateDirectRoomActivity) + fun inject(displayReadReceiptsBottomSheet: DisplayReadReceiptsBottomSheet) + @Component.Factory interface Factory { fun create(vectorComponent: VectorComponent, diff --git a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt index 80410f87..c2c86cad 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt @@ -29,7 +29,11 @@ import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsVie import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsViewModel_AssistedFactory import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupSharedViewModel import im.vector.riotx.features.crypto.verification.SasVerificationViewModel -import im.vector.riotx.features.home.* +import im.vector.riotx.features.home.HomeActivityViewModel +import im.vector.riotx.features.home.HomeActivityViewModel_AssistedFactory +import im.vector.riotx.features.home.HomeDetailViewModel +import im.vector.riotx.features.home.HomeDetailViewModel_AssistedFactory +import im.vector.riotx.features.home.HomeNavigationViewModel import im.vector.riotx.features.home.createdirect.CreateDirectRoomNavigationViewModel import im.vector.riotx.features.home.createdirect.CreateDirectRoomViewModel import im.vector.riotx.features.home.createdirect.CreateDirectRoomViewModel_AssistedFactory @@ -39,7 +43,18 @@ import im.vector.riotx.features.home.room.detail.RoomDetailViewModel import im.vector.riotx.features.home.room.detail.RoomDetailViewModel_AssistedFactory import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel_AssistedFactory -import im.vector.riotx.features.home.room.detail.timeline.action.* +import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsViewModel +import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsViewModel_AssistedFactory +import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsViewModel +import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsViewModel_AssistedFactory +import im.vector.riotx.features.home.room.detail.timeline.action.MessageMenuViewModel +import im.vector.riotx.features.home.room.detail.timeline.action.MessageMenuViewModel_AssistedFactory +import im.vector.riotx.features.home.room.detail.timeline.action.QuickReactionViewModel +import im.vector.riotx.features.home.room.detail.timeline.action.QuickReactionViewModel_AssistedFactory +import im.vector.riotx.features.home.room.detail.timeline.action.ViewEditHistoryViewModel +import im.vector.riotx.features.home.room.detail.timeline.action.ViewEditHistoryViewModel_AssistedFactory +import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionViewModel +import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionViewModel_AssistedFactory import im.vector.riotx.features.home.room.list.RoomListViewModel import im.vector.riotx.features.home.room.list.RoomListViewModel_AssistedFactory import im.vector.riotx.features.reactions.EmojiChooserViewModel @@ -182,4 +197,8 @@ interface ViewModelModule { @Binds fun bindPushGatewaysViewModelFactory(factory: PushGatewaysViewModel_AssistedFactory): PushGatewaysViewModel.Factory + + @Binds + fun bindDisplayReadReceiptsViewModel(factory: DisplayReadReceiptsViewModel_AssistedFactory): DisplayReadReceiptsViewModel.Factory + } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt index 12d58614..6293e22b 100644 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt @@ -23,6 +23,7 @@ import android.widget.LinearLayout import androidx.core.view.isVisible import butterknife.ButterKnife import im.vector.riotx.R +import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData import kotlinx.android.synthetic.main.view_read_receipts.view.* @@ -48,7 +49,8 @@ class ReadReceiptsView @JvmOverloads constructor( ButterKnife.bind(this) } - fun render(readReceipts: List, avatarRenderer: AvatarRenderer) { + fun render(readReceipts: List, avatarRenderer: AvatarRenderer, clickListener: OnClickListener) { + setOnClickListener(clickListener) if (readReceipts.isNotEmpty()) { isVisible = true for (index in 0 until MAX_RECEIPT_DISPLAYED) { 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 fad93d38..544f05cd 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 @@ -91,6 +91,7 @@ import im.vector.riotx.features.home.room.detail.composer.TextComposerActions import im.vector.riotx.features.home.room.detail.composer.TextComposerView 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.helper.EndlessRecyclerViewScrollListener @@ -315,17 +316,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 { @@ -354,9 +355,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)) } @@ -391,26 +392,26 @@ class RoomDetailFragment : if (VectorPreferences.swipeToReplyIsEnabled(requireContext())) { val swipeCallback = RoomMessageTouchHelperCallback(requireContext(), - R.drawable.ic_reply, - object : RoomMessageTouchHelperCallback.QuickReplayHandler { - override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { - (model as? AbsMessageItem)?.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)?.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).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).informationData.sendState == SendState.SYNCED + } + else -> false + } + } + }) val touchHelper = ItemTouchHelper(swipeCallback) touchHelper.attachToRecyclerView(recyclerView) } @@ -816,6 +817,11 @@ class RoomDetailFragment : }) } + override fun onReadReceiptsClicked(informationData: MessageInformationData) { + DisplayReadReceiptsBottomSheet.newInstance(roomDetailArgs.roomId, informationData) + .show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS") + } + // AutocompleteUserPresenter.Callback override fun onQueryUsers(query: CharSequence?) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptItem.kt new file mode 100644 index 00000000..fd9b5f11 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptItem.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.readreceipts + +import android.widget.ImageView +import android.widget.TextView +import androidx.core.view.isVisible +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.features.home.AvatarRenderer + +@EpoxyModelClass(layout = R.layout.item_display_read_receipt) +abstract class DisplayReadReceiptItem : EpoxyModelWithHolder() { + + @EpoxyAttribute var name: String? = null + @EpoxyAttribute var userId: String = "" + @EpoxyAttribute var avatarUrl: String? = null + @EpoxyAttribute var timestamp: CharSequence? = null + @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer + + override fun bind(holder: Holder) { + avatarRenderer.render(avatarUrl, userId, name, holder.avatarView) + holder.displayNameView.text = name ?: userId + timestamp?.let { + holder.timestampView.text = it + holder.timestampView.isVisible = true + } ?: run { + holder.timestampView.isVisible = false + } + } + + class Holder : VectorEpoxyHolder() { + val avatarView by bind(R.id.readReceiptAvatar) + val displayNameView by bind(R.id.readReceiptName) + val timestampView by bind(R.id.readReceiptDate) + } + +} 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 new file mode 100644 index 00000000..572954f1 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt @@ -0,0 +1,93 @@ +/* + * 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.readreceipts + +import android.annotation.SuppressLint +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 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 +import im.vector.riotx.EmojiCompatFontProvider +import im.vector.riotx.R +import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs +import im.vector.riotx.features.home.room.detail.timeline.action.VectorBaseBottomSheetDialogFragment +import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionBottomSheet +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 + +/** + * Bottom sheet displaying list of read receipts for a given event ordered by descending timestamp + */ +class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() { + + private val viewModel: DisplayReadReceiptsViewModel by fragmentViewModel() + + @Inject lateinit var displayReadReceiptsViewModelFactory: DisplayReadReceiptsViewModel.Factory + @Inject lateinit var epoxyController: DisplayReadReceiptsController + + @BindView(R.id.bottom_sheet_display_reactions_list) + lateinit var epoxyRecyclerView: EpoxyRecyclerView + + + 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_epoxylist_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) + bottomSheetTitle.text = getString(R.string.read_receipts_list) + } + + + override fun invalidate() = withState(viewModel) { + epoxyController.setData(it) + } + + companion object { + fun newInstance(roomId: String, informationData: MessageInformationData): DisplayReadReceiptsBottomSheet { + val args = Bundle() + val parcelableArgs = TimelineEventFragmentArgs( + informationData.eventId, + roomId, + informationData + ) + args.putParcelable(MvRx.KEY_ARG, parcelableArgs) + return DisplayReadReceiptsBottomSheet().apply { arguments = args } + + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsController.kt new file mode 100644 index 00000000..2c2f9f49 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsController.kt @@ -0,0 +1,71 @@ +/* + * 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.readreceipts + +import com.airbnb.epoxy.TypedEpoxyController +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Incomplete +import com.airbnb.mvrx.Success +import im.vector.matrix.android.api.session.Session +import im.vector.riotx.R +import im.vector.riotx.core.date.VectorDateFormatter +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.core.ui.list.genericFooterItem +import im.vector.riotx.core.ui.list.genericLoaderItem +import im.vector.riotx.features.home.AvatarRenderer +import javax.inject.Inject + +/** + * Epoxy controller for read receipt event list + */ +class DisplayReadReceiptsController @Inject constructor(private val dateFormatter: VectorDateFormatter, + private val stringProvider: StringProvider, + private val session: Session, + private val avatarRender: AvatarRenderer) + : TypedEpoxyController() { + + + override fun buildModels(state: DisplayReadReceiptsViewState) { + when (state.readReceipts) { + is Incomplete -> { + genericLoaderItem { + id("loading") + } + } + is Fail -> { + genericFooterItem { + id("failure") + text(stringProvider.getString(R.string.unknown_error)) + } + } + is Success -> { + state.readReceipts()?.forEach { + val timestamp = dateFormatter.formatRelativeDateTime(it.originServerTs) + DisplayReadReceiptItem_() + .id(it.user.userId) + .userId(it.user.userId) + .avatarUrl(it.user.avatarUrl) + .name(it.user.displayName) + .avatarRenderer(avatarRender) + .timestamp(timestamp) + .addIf(session.myUserId != it.user.userId, this) + } + } + } + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewModel.kt new file mode 100644 index 00000000..8423ba4a --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewModel.kt @@ -0,0 +1,63 @@ +/* + * 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.readreceipts + +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 + +/** + * Used to display the list of read receipts to a given event + */ +class DisplayReadReceiptsViewModel @AssistedInject constructor(@Assisted initialState: DisplayReadReceiptsViewState, + private val session: Session +) : VectorViewModel(initialState) { + + private val roomId = initialState.roomId + private val eventId = initialState.eventId + private val room = session.getRoom(roomId) + ?: throw IllegalStateException("Shouldn't use this ViewModel without a room") + + @AssistedInject.Factory + interface Factory { + fun create(initialState: DisplayReadReceiptsViewState): DisplayReadReceiptsViewModel + } + + companion object : MvRxViewModelFactory { + + override fun create(viewModelContext: ViewModelContext, state: DisplayReadReceiptsViewState): DisplayReadReceiptsViewModel? { + val fragment: DisplayReadReceiptsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.displayReadReceiptsViewModelFactory.create(state) + } + } + + init { + observeEventAnnotationSummaries() + } + + private fun observeEventAnnotationSummaries() { + RxRoom(room) + .liveEventReadReceipts(eventId) + .execute { + copy(readReceipts = it) + } + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewState.kt new file mode 100644 index 00000000..68952b99 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewState.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.riotx.features.home.room.detail.readreceipts + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.api.session.room.model.ReadReceipt +import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs + +data class DisplayReadReceiptsViewState( + val eventId: String, + val roomId: String, + val readReceipts: Async> = Uninitialized +) : MvRxState { + + constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId) + +} \ No newline at end of file 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 e8956309..28a3d100 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 @@ -27,6 +27,7 @@ import com.airbnb.epoxy.EpoxyModel 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 +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 @@ -42,7 +43,7 @@ import im.vector.riotx.features.media.VideoContentRenderer import org.threeten.bp.LocalDateTime import javax.inject.Inject -class TimelineEventController @Inject constructor(private val dateFormatter: TimelineDateFormatter, +class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter, private val timelineItemFactory: TimelineItemFactory, private val timelineMediaSizeProvider: TimelineMediaSizeProvider, private val avatarRenderer: AvatarRenderer, @@ -51,7 +52,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim userPreferencesProvider: UserPreferencesProvider ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener { - interface Callback : ReactionPillCallback, AvatarCallback, BaseCallback, UrlClickCallback { + interface Callback : BaseCallback, ReactionPillCallback, AvatarCallback, UrlClickCallback, ReadReceiptsCallback { fun onEventVisible(event: TimelineEvent) fun onRoomCreateLinkClicked(url: String) fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View) @@ -77,6 +78,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim fun onMemberNameClicked(informationData: MessageInformationData) } + interface ReadReceiptsCallback { + fun onReadReceiptsClicked(informationData: MessageInformationData) + } + interface UrlClickCallback { fun onUrlClicked(url: String): Boolean fun onUrlLongClicked(url: String): Boolean @@ -158,7 +163,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim 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 } } @@ -219,8 +224,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim // 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) } } @@ -293,7 +298,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim // 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 initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey) + ?: true val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState } if (isCollapsed) { collapsedEventIds.addAll(mergedEventIds) 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/action/ViewEditHistoryBottomSheet.kt index aefbde43..be3dfc80 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/action/ViewEditHistoryBottomSheet.kt @@ -49,7 +49,7 @@ class ViewEditHistoryBottomSheet : VectorBaseBottomSheetDialogFragment() { lateinit var epoxyRecyclerView: EpoxyRecyclerView private val epoxyController by lazy { - ViewEditHistoryEpoxyController(requireContext(), viewModel.timelineDateFormatter, eventHtmlRenderer) + ViewEditHistoryEpoxyController(requireContext(), viewModel.dateFormatter, eventHtmlRenderer) } override fun injectWith(screenComponent: ScreenComponent) { 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/action/ViewEditHistoryEpoxyController.kt index fc11f255..dab43108 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/action/ViewEditHistoryEpoxyController.kt @@ -33,7 +33,7 @@ import im.vector.riotx.core.ui.list.genericFooterItem import im.vector.riotx.core.ui.list.genericItem import im.vector.riotx.core.ui.list.genericItemHeader import im.vector.riotx.core.ui.list.genericLoaderItem -import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFormatter +import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.features.html.EventHtmlRenderer import me.gujun.android.span.span import name.fraser.neil.plaintext.diff_match_patch @@ -44,7 +44,7 @@ import java.util.* * Epoxy controller for reaction event list */ class ViewEditHistoryEpoxyController(private val context: Context, - val timelineDateFormatter: TimelineDateFormatter, + val dateFormatter: VectorDateFormatter, val eventHtmlRenderer: EventHtmlRenderer) : TypedEpoxyController() { override fun buildModels(state: ViewEditHistoryViewState) { @@ -84,7 +84,7 @@ class ViewEditHistoryEpoxyController(private val context: Context, if (lastDate?.get(Calendar.DAY_OF_YEAR) != evDate.get(Calendar.DAY_OF_YEAR)) { //need to display header with day val dateString = if (DateUtils.isToday(evDate.timeInMillis)) context.getString(R.string.today) - else timelineDateFormatter.formatMessageDay(timelineEvent.localDateTime()) + else dateFormatter.formatMessageDay(timelineEvent.localDateTime()) genericItemHeader { id(evDate.hashCode()) text(dateString) @@ -136,7 +136,7 @@ class ViewEditHistoryEpoxyController(private val context: Context, } genericItem { id(timelineEvent.eventId) - title(timelineDateFormatter.formatMessageHour(timelineEvent.localDateTime())) + title(dateFormatter.formatMessageHour(timelineEvent.localDateTime())) description(spannedDiff ?: body) } } 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/action/ViewEditHistoryViewModel.kt index 6ad17210..e8b27e18 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/action/ViewEditHistoryViewModel.kt @@ -27,7 +27,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageContent 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.features.home.room.detail.timeline.helper.TimelineDateFormatter +import im.vector.riotx.core.date.VectorDateFormatter import timber.log.Timber import java.util.* @@ -46,7 +46,7 @@ data class ViewEditHistoryViewState( class ViewEditHistoryViewModel @AssistedInject constructor(@Assisted initialState: ViewEditHistoryViewState, val session: Session, - val timelineDateFormatter: TimelineDateFormatter + val dateFormatter: VectorDateFormatter ) : VectorViewModel(initialState) { private val roomId = initialState.roomId 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/action/ViewReactionViewModel.kt index 6f5a7847..9479c307 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/action/ViewReactionViewModel.kt @@ -16,16 +16,20 @@ package im.vector.riotx.features.home.room.detail.timeline.action -import com.airbnb.mvrx.* +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Uninitialized +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.room.model.ReactionAggregatedSummary import im.vector.matrix.rx.RxRoom -import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.utils.isSingleEmoji -import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFormatter +import im.vector.riotx.core.date.VectorDateFormatter import io.reactivex.Observable import io.reactivex.Single @@ -54,13 +58,13 @@ data class ReactionInfo( class ViewReactionViewModel @AssistedInject constructor(@Assisted initialState: DisplayReactionsViewState, private val session: Session, - private val timelineDateFormatter: TimelineDateFormatter + private val dateFormatter: VectorDateFormatter ) : VectorViewModel(initialState) { private val roomId = initialState.roomId private val eventId = initialState.eventId private val room = session.getRoom(roomId) - ?: throw IllegalStateException("Shouldn't use this ViewModel without a room") + ?: throw IllegalStateException("Shouldn't use this ViewModel without a room") @AssistedInject.Factory interface Factory { @@ -100,14 +104,14 @@ class ViewReactionViewModel @AssistedInject constructor(@Assisted .fromIterable(summary.sourceEvents) .map { val event = room.getTimeLineEvent(it) - ?: throw RuntimeException("Your eventId is not valid") - val localDate = event.root.localDateTime() + ?: throw RuntimeException("Your eventId is not valid") ReactionInfo( event.root.eventId!!, summary.key, event.root.senderId ?: "", event.getDisambiguatedDisplayName(), - timelineDateFormatter.formatMessageHour(localDate) + dateFormatter.formatRelativeDateTime(event.root.originServerTs) + ) } }.toList() 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/action/ViewReactionsEpoxyController.kt index 74b3f492..e3df0b73 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/action/ViewReactionsEpoxyController.kt @@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.detail.timeline.action import android.content.Context import android.graphics.Typeface +import android.text.format.DateUtils import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Incomplete 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 5ed7bcb3..71e7627d 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 @@ -161,6 +161,7 @@ class MessageItemFactory @Inject constructor( .informationData(informationData) .highlighted(highlight) .avatarCallback(callback) + .readReceiptsCallback(callback) .filename(messageContent.body) .iconRes(R.drawable.filetype_audio) .reactionPillCallback(callback) @@ -191,6 +192,7 @@ class MessageItemFactory @Inject constructor( .avatarCallback(callback) .filename(messageContent.body) .reactionPillCallback(callback) + .readReceiptsCallback(callback) .emojiTypeFace(emojiCompatFontProvider.typeface) .iconRes(R.drawable.filetype_attachment) .cellClickListener( @@ -205,10 +207,6 @@ class MessageItemFactory @Inject constructor( DebouncedClickListener(View.OnClickListener { _ -> callback?.onFileMessageClicked(informationData.eventId, messageContent) })) - .longClickListener { view -> - return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false - } } private fun buildNotHandledMessageItem(messageContent: MessageContent, highlight: Boolean): DefaultItem? { @@ -246,6 +244,7 @@ class MessageItemFactory @Inject constructor( .avatarCallback(callback) .mediaData(data) .reactionPillCallback(callback) + .readReceiptsCallback(callback) .emojiTypeFace(emojiCompatFontProvider.typeface) .clickListener( DebouncedClickListener(View.OnClickListener { view -> @@ -297,6 +296,7 @@ class MessageItemFactory @Inject constructor( .avatarCallback(callback) .mediaData(thumbnailData) .reactionPillCallback(callback) + .readReceiptsCallback(callback) .emojiTypeFace(emojiCompatFontProvider.typeface) .cellClickListener( DebouncedClickListener(View.OnClickListener { view -> @@ -336,6 +336,7 @@ class MessageItemFactory @Inject constructor( .avatarCallback(callback) .urlClickCallback(callback) .reactionPillCallback(callback) + .readReceiptsCallback(callback) .emojiTypeFace(emojiCompatFontProvider.typeface) //click on the text .cellClickListener( @@ -402,6 +403,7 @@ class MessageItemFactory @Inject constructor( .avatarCallback(callback) .reactionPillCallback(callback) .urlClickCallback(callback) + .readReceiptsCallback(callback) .emojiTypeFace(emojiCompatFontProvider.typeface) .memberClickListener( DebouncedClickListener(View.OnClickListener { view -> @@ -441,6 +443,7 @@ class MessageItemFactory @Inject constructor( .highlighted(highlight) .avatarCallback(callback) .reactionPillCallback(callback) + .readReceiptsCallback(callback) .urlClickCallback(callback) .emojiTypeFace(emojiCompatFontProvider.typeface) .cellClickListener( @@ -462,6 +465,7 @@ class MessageItemFactory @Inject constructor( .informationData(informationData) .highlighted(highlight) .avatarCallback(callback) + .readReceiptsCallback(callback) .cellClickListener( DebouncedClickListener(View.OnClickListener { view -> callback?.onEventCellClicked(informationData, null, view) 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 b1cb5407..f73a2001 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 @@ -44,6 +44,7 @@ class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEv .highlighted(highlight) .informationData(informationData) .baseCallback(callback) + .readReceiptsCallback(callback) } 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 51d2ce92..6f3a0a1e 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 @@ -16,7 +16,6 @@ package im.vector.riotx.features.home.room.detail.timeline.item -import android.annotation.SuppressLint import android.graphics.Typeface import android.os.Build import android.view.View @@ -70,6 +69,9 @@ abstract class AbsMessageItem : BaseEventItem() { @EpoxyAttribute var avatarCallback: TimelineEventController.AvatarCallback? = null + @EpoxyAttribute + var readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null + private val _avatarClickListener = DebouncedClickListener(View.OnClickListener { avatarCallback?.onAvatarClicked(informationData) }) @@ -77,6 +79,9 @@ abstract class AbsMessageItem : BaseEventItem() { avatarCallback?.onMemberNameClicked(informationData) }) + private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener { + readReceiptsCallback?.onReadReceiptsClicked(informationData) + }) var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener { override fun onReacted(reactionButton: ReactionButton) { @@ -124,7 +129,7 @@ abstract class AbsMessageItem : BaseEventItem() { holder.memberNameView.setOnLongClickListener(null) } - holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer) + holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener) if (!shouldShowReactionAtBottom() || informationData.orderedReactionList.isNullOrEmpty()) { holder.reactionWrapper?.isVisible = false 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 679bfbba..d46b2a8d 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 @@ -17,7 +17,6 @@ package im.vector.riotx.features.home.room.detail.timeline.item import android.os.Parcelable -import im.vector.matrix.android.api.session.room.model.ReadReceipt import im.vector.matrix.android.api.session.room.send.SendState import kotlinx.android.parcel.Parcelize 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 b18a665d..c4b5f042 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 @@ -23,6 +23,7 @@ import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R import im.vector.riotx.core.ui.views.ReadReceiptsView +import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController @@ -45,6 +46,13 @@ abstract class NoticeItem : BaseEventItem() { return@OnLongClickListener baseCallback?.onEventLongClicked(informationData, null, it) == true } + @EpoxyAttribute + var readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null + + private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener { + readReceiptsCallback?.onReadReceiptsClicked(informationData) + }) + override fun bind(holder: Holder) { super.bind(holder) holder.noticeTextView.text = noticeText @@ -56,7 +64,7 @@ abstract class NoticeItem : BaseEventItem() { holder.avatarImageView ) holder.view.setOnLongClickListener(longClickListener) - holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer) + holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener) } override fun getViewType() = STUB_ID 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 2f6a4320..a34c9874 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 @@ -24,7 +24,7 @@ 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.features.home.room.detail.timeline.helper.TimelineDateFormatter +import im.vector.riotx.core.date.VectorDateFormatter 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 @@ -35,7 +35,7 @@ import javax.inject.Inject * This class compute if data of an event (such has avatar, display name, ...) should be displayed, depending on the previous event in the timeline */ class MessageInformationDataFactory @Inject constructor(private val session: Session, - private val timelineDateFormatter: TimelineDateFormatter, + private val dateFormatter: VectorDateFormatter, private val colorProvider: ColorProvider) { fun create(event: TimelineEvent, nextEvent: TimelineEvent?): MessageInformationData { @@ -55,7 +55,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses || (nextEvent?.root?.getClearType() != EventType.MESSAGE && nextEvent?.root?.getClearType() != EventType.ENCRYPTED) || isNextMessageReceivedMoreThanOneHourAgo - val time = timelineDateFormatter.formatMessageHour(date) + val time = dateFormatter.formatMessageHour(date) val avatarUrl = event.senderAvatar val memberName = event.getDisambiguatedDisplayName() val formattedMemberName = span(memberName) { @@ -79,12 +79,14 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses hasBeenEdited = event.hasBeenEdited(), hasPendingEdits = event.annotations?.editSummary?.localEchos?.any() ?: false, readReceipts = event.readReceipts + .asSequence() .filter { it.user.userId != session.myUserId } .map { ReadReceiptData(it.user.userId, it.user.avatarUrl, it.user.displayName, it.originServerTs) } + .toList() ) } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt index 6af62341..38f15974 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt @@ -29,13 +29,13 @@ import im.vector.riotx.core.resources.DateProvider import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter -import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFormatter +import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.features.home.room.detail.timeline.helper.senderName import me.gujun.android.span.span import javax.inject.Inject class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatter: NoticeEventFormatter, - private val timelineDateFormatter: TimelineDateFormatter, + private val dateFormatter: VectorDateFormatter, private val colorProvider: ColorProvider, private val stringProvider: StringProvider, private val avatarRenderer: AvatarRenderer) { @@ -94,7 +94,7 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte val currentDate = DateProvider.currentLocalDateTime() val isSameDay = date.toLocalDate() == currentDate.toLocalDate() latestFormattedEvent = if (latestEvent.root.isEncrypted() - && latestEvent.root.mxDecryptionResult == null) { + && latestEvent.root.mxDecryptionResult == null) { stringProvider.getString(R.string.encrypted_message) } else if (latestEvent.root.getClearType() == EventType.MESSAGE) { val senderName = latestEvent.senderName() ?: latestEvent.root.senderId @@ -117,10 +117,9 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte } } latestEventTime = if (isSameDay) { - timelineDateFormatter.formatMessageHour(date) + dateFormatter.formatMessageHour(date) } else { - //TODO: change this - timelineDateFormatter.formatMessageDay(date) + dateFormatter.formatMessageDay(date) } } return RoomSummaryItem_() diff --git a/vector/src/main/res/layout/item_display_read_receipt.xml b/vector/src/main/res/layout/item_display_read_receipt.xml new file mode 100644 index 00000000..cf3c9a26 --- /dev/null +++ b/vector/src/main/res/layout/item_display_read_receipt.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + \ No newline at end of file From 21deb2551d952b64aee1f86a2160a8b9370eb420 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 12 Aug 2019 17:59:07 +0200 Subject: [PATCH 10/40] Read receipts: handle read receipts set on filtered events + let BottomSheet takes a snapshot instead of being live. --- .../session/room/timeline/TimelineService.kt | 6 +- .../session/room/timeline/TimelineSettings.kt | 39 ++++ .../database/helper/ChunkEntityHelper.kt | 2 +- .../mapper/ReadReceiptsSummaryMapper.kt | 5 +- .../database/mapper/TimelineEventMapper.kt | 13 +- .../model/ReadReceiptsSummaryEntity.kt | 6 + .../query/ReadReceiptsSummaryEntityQueries.kt | 8 + .../internal/session/room/RoomFactory.kt | 2 +- .../session/room/timeline/DefaultTimeline.kt | 178 +++++++++++++----- .../room/timeline/DefaultTimelineService.kt | 10 +- .../session/sync/ReadReceiptHandler.kt | 2 +- .../vector/riotx/core/di/ViewModelModule.kt | 6 - .../home/room/detail/RoomDetailFragment.kt | 4 +- .../home/room/detail/RoomDetailViewModel.kt | 24 +-- .../DisplayReadReceiptsBottomSheet.kt | 34 ++-- .../DisplayReadReceiptsController.kt | 49 ++--- .../DisplayReadReceiptsViewModel.kt | 63 ------- .../DisplayReadReceiptsViewState.kt | 33 ---- .../timeline/TimelineEventController.kt | 3 +- .../timeline/factory/MessageItemFactory.kt | 25 +-- .../timeline/factory/TimelineItemFactory.kt | 27 +-- .../timeline/format/NoticeEventFormatter.kt | 14 +- .../detail/timeline/item/AbsMessageItem.kt | 2 +- .../room/detail/timeline/item/NoticeItem.kt | 2 +- .../util/MessageInformationDataFactory.kt | 5 +- 25 files changed, 277 insertions(+), 285 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.kt delete mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewModel.kt delete mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewState.kt 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 5a4838ad..fdf99bd2 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 @@ -25,12 +25,12 @@ interface TimelineService { /** * Instantiate a [Timeline] with an optional initial eventId, to be used with permalink. - * You can filter the type you want to grab with the allowedTypes param. + * You can also configure some settings with the [settings] param. * @param eventId the optional initial eventId. - * @param allowedTypes the optional filter types + * @param settings settings to configure the timeline. * @return the instantiated timeline */ - fun createTimeline(eventId: String?, allowedTypes: List? = null): Timeline + fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline fun getTimeLineEvent(eventId: String): TimelineEvent? diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.kt new file mode 100644 index 00000000..219c23eb --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.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.api.session.room.timeline + +/** + * Data class holding setting values for a [Timeline] instance. + */ +data class TimelineSettings( + /** + * The initial number of events to retrieve from cache. You might get less events if you don't have loaded enough yet. + */ + val initialSize: Int, + /** + * A flag to filter edit events + */ + val filterEdits: Boolean = false, + /** + * A flag to filter by types. It should be used with [allowedTypes] field + */ + val filterTypes: Boolean = false, + /** + * If [filterTypes] is true, the list of types allowed by the list. + */ + val allowedTypes: List = emptyList() +) 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 a541e8cf..69065f51 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 @@ -140,7 +140,7 @@ internal fun ChunkEntity.add(roomId: String, val senderId = event.senderId ?: "" val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst() - ?: ReadReceiptsSummaryEntity(eventId) + ?: ReadReceiptsSummaryEntity(eventId, roomId) // Update RR for the sender of a new message with a dummy one diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/ReadReceiptsSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/ReadReceiptsSummaryMapper.kt index b7cdabfc..1f53d1b4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/ReadReceiptsSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/ReadReceiptsSummaryMapper.kt @@ -28,7 +28,10 @@ import javax.inject.Inject internal class ReadReceiptsSummaryMapper @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration) { - fun map(readReceiptsSummaryEntity: ReadReceiptsSummaryEntity): List { + fun map(readReceiptsSummaryEntity: ReadReceiptsSummaryEntity?): List { + if (readReceiptsSummaryEntity == null) { + return emptyList() + } return Realm.getInstance(realmConfiguration).use { realm -> val readReceipts = readReceiptsSummaryEntity.readReceipts readReceipts 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 5290692c..42365d7e 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 @@ -17,15 +17,18 @@ package im.vector.matrix.android.internal.database.mapper import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.room.model.ReadReceipt import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.internal.database.model.TimelineEventEntity import javax.inject.Inject -internal class TimelineEventMapper @Inject constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper){ - - fun map(timelineEventEntity: TimelineEventEntity): TimelineEvent { +internal class TimelineEventMapper @Inject constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper) { + fun map(timelineEventEntity: TimelineEventEntity, correctedReadReceipts: List? = null): TimelineEvent { + val readReceipts = correctedReadReceipts ?: timelineEventEntity.readReceipts?.let { + readReceiptsSummaryMapper.map(it) + } return TimelineEvent( root = timelineEventEntity.root?.asDomain() ?: Event("", timelineEventEntity.eventId), @@ -35,9 +38,7 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS senderName = timelineEventEntity.senderName, isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName, senderAvatar = timelineEventEntity.senderAvatar, - readReceipts = timelineEventEntity.readReceipts?.let { - readReceiptsSummaryMapper.map(it) - } ?: emptyList() + readReceipts = readReceipts ?: emptyList() ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadReceiptsSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadReceiptsSummaryEntity.kt index e0fe970f..56e8938c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadReceiptsSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadReceiptsSummaryEntity.kt @@ -18,14 +18,20 @@ package im.vector.matrix.android.internal.database.model import io.realm.RealmList import io.realm.RealmObject +import io.realm.RealmResults +import io.realm.annotations.LinkingObjects import io.realm.annotations.PrimaryKey internal open class ReadReceiptsSummaryEntity( @PrimaryKey var eventId: String = "", + var roomId: String = "", var readReceipts: RealmList = RealmList() ) : RealmObject() { + @LinkingObjects("readReceipts") + 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/query/ReadReceiptsSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadReceiptsSummaryEntityQueries.kt index d04ced11..0c3d7d8e 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 @@ -26,3 +26,11 @@ internal fun ReadReceiptsSummaryEntity.Companion.where(realm: Realm, eventId: St return realm.where() .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 +} 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 cf2627b0..e68e4282 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 @@ -65,7 +65,7 @@ internal class RoomFactory @Inject constructor(private val context: Context, private val leaveRoomTask: LeaveRoomTask) { fun create(roomId: String): Room { - val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, cryptoService, paginationTask, timelineEventMapper) + val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, cryptoService, paginationTask, timelineEventMapper, readReceiptsSummaryMapper) val sendService = DefaultSendService(context, credentials, roomId, eventFactory, cryptoService, monarchy) val stateService = DefaultStateService(roomId, monarchy.realmConfiguration, taskExecutor, sendStateTask) val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask) 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 921c65ea..059b2415 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 @@ -16,25 +16,47 @@ package im.vector.matrix.android.internal.session.room.timeline +import android.util.SparseArray 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 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.mapper.ReadReceiptsSummaryMapper 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.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.query.* +import im.vector.matrix.android.internal.database.model.EventEntityFields +import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity +import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntityFields +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.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 @@ -43,10 +65,11 @@ import kotlin.collections.ArrayList import kotlin.collections.HashMap -private const val INITIAL_LOAD_SIZE = 30 private const val MIN_FETCHING_COUNT = 30 private const val DISPLAY_INDEX_UNKNOWN = Int.MIN_VALUE +private const val EDIT_FILTER_LIKE = "{*\"m.relates_to\"*\"rel_type\":*\"m.replace\"*}" + internal class DefaultTimeline( private val roomId: String, private val initialEventId: String? = null, @@ -56,7 +79,8 @@ internal class DefaultTimeline( private val paginationTask: PaginationTask, private val cryptoService: CryptoService, private val timelineEventMapper: TimelineEventMapper, - private val allowedTypes: List? + private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, + private val settings: TimelineSettings ) : Timeline { private companion object { @@ -79,6 +103,11 @@ internal class DefaultTimeline( private val debouncer = Debouncer(mainHandler) private lateinit var liveEvents: RealmResults + private lateinit var eventRelations: RealmResults + private var hiddenReadReceipts: RealmResults? = null + private val correctedReadReceiptsEventByIndex = SparseArray() + private val correctedReadReceiptsByEvent = HashMap>() + private var roomEntity: RoomEntity? = null private var prevDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN @@ -92,7 +121,6 @@ internal class DefaultTimeline( private val timelineID = UUID.randomUUID().toString() - private lateinit var eventRelations: RealmResults private val eventDecryptor = TimelineEventDecryptor(realmConfiguration, timelineID, cryptoService) @@ -132,9 +160,9 @@ internal class DefaultTimeline( val eventEntity = results[index] eventEntity?.eventId?.let { eventId -> builtEventsIdMap[eventId]?.let { builtIndex -> - //Update the relation of existing event + //Update an existing event builtEvents[builtIndex]?.let { te -> - builtEvents[builtIndex] = timelineEventMapper.map(eventEntity) + builtEvents[builtIndex] = timelineEventMapper.map(eventEntity, correctedReadReceiptsByEvent[te.root.eventId]) hasChanged = true } } @@ -164,32 +192,54 @@ internal class DefaultTimeline( postSnapshot() } -// private val newSessionListener = object : NewSessionListener { -// override fun onNewSession(roomId: String?, senderKey: String, sessionId: String) { -// if (roomId == this@DefaultTimeline.roomId) { -// Timber.v("New session id detected for this room") -// BACKGROUND_HANDLER.post { -// val realm = backgroundRealm.get() -// var hasChange = false -// builtEvents.forEachIndexed { index, timelineEvent -> -// if (timelineEvent.isEncrypted()) { -// val eventContent = timelineEvent.root.content.toModel() -// if (eventContent?.sessionId == sessionId -// && (timelineEvent.root.mClearEvent == null || timelineEvent.root.mCryptoError != null)) { -// //we need to rebuild this event -// EventEntity.where(realm, eventId = timelineEvent.root.eventId!!).findFirst()?.let { -// //builtEvents[index] = timelineEventFactory.create(it, realm) -// hasChange = true -// } -// } -// } -// } -// if (hasChange) postSnapshot() -// } -// } -// } -// -// } + private val hiddenReadReceiptsListener = OrderedRealmCollectionChangeListener> { collection, changeSet -> + var hasChange = false + changeSet.deletions.forEach { + val eventId = correctedReadReceiptsEventByIndex[it] + val timelineEvent = liveEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, eventId).findFirst() + builtEventsIdMap[eventId]?.let { builtIndex -> + builtEvents[builtIndex]?.let { te -> + builtEvents[builtIndex] = te.copy(readReceipts = readReceiptsSummaryMapper.map(timelineEvent?.readReceipts)) + hasChange = true + } + } + } + correctedReadReceiptsEventByIndex.clear() + correctedReadReceiptsByEvent.clear() + val loadedReadReceipts = collection.where().greaterThan("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.DISPLAY_INDEX}", prevDisplayIndex).findAll() + loadedReadReceipts.forEachIndexed { index, summary -> + val timelineEvent = summary?.timelineEvent?.firstOrNull() + val displayIndex = timelineEvent?.root?.displayIndex + if (displayIndex != null) { + val firstDisplayedEvent = liveEvents.where() + .sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING) + .lessThan(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex) + .findFirst() + + if (firstDisplayedEvent != null) { + correctedReadReceiptsEventByIndex.put(index, firstDisplayedEvent.eventId) + correctedReadReceiptsByEvent.getOrPut(firstDisplayedEvent.eventId, { + readReceiptsSummaryMapper.map(firstDisplayedEvent.readReceipts).toMutableList() + }).addAll( + readReceiptsSummaryMapper.map(summary) + ) + } + } + } + if (correctedReadReceiptsByEvent.isNotEmpty()) { + correctedReadReceiptsByEvent.forEach { (eventId, correctedReadReceipts) -> + builtEventsIdMap[eventId]?.let { builtIndex -> + builtEvents[builtIndex]?.let { te -> + builtEvents[builtIndex] = te.copy(readReceipts = correctedReadReceipts) + hasChange = true + } + } + } + } + if (hasChange) { + postSnapshot() + } + } // Public methods ****************************************************************************** @@ -236,15 +286,23 @@ internal class DefaultTimeline( } liveEvents = buildEventQuery(realm) + .filterEventsWithSettings() .sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING) .findAllAsync() .also { it.addChangeListener(eventsChangeListener) } - isReady.set(true) - eventRelations = EventAnnotationsSummaryEntity.whereInRoom(realm, roomId) .findAllAsync() .also { it.addChangeListener(relationsListener) } + + hiddenReadReceipts = ReadReceiptsSummaryEntity.whereInRoom(realm, roomId) + .isNotEmpty(ReadReceiptsSummaryEntityFields.TIMELINE_EVENT) + .isNotEmpty(ReadReceiptsSummaryEntityFields.READ_RECEIPTS.`$`) + .filterReceiptsWithSettings() + .findAllAsync() + .also { it.addChangeListener(hiddenReadReceiptsListener) } + + isReady.set(true) } } } @@ -257,6 +315,7 @@ internal class DefaultTimeline( cancelableBag.cancel() roomEntity?.sendingTimelineEvents?.removeAllChangeListeners() eventRelations.removeAllChangeListeners() + hiddenReadReceipts?.removeAllChangeListeners() liveEvents.removeAllChangeListeners() backgroundRealm.getAndSet(null).also { it.close() @@ -274,7 +333,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) { if (findCurrentChunk(localRealm)?.isLastForward == true) { return false @@ -331,7 +390,9 @@ internal class DefaultTimeline( val sendingEvents = ArrayList() if (hasReachedEnd(Timeline.Direction.FORWARDS)) { roomEntity?.sendingTimelineEvents - ?.filter { allowedTypes?.contains(it.root?.type) ?: false } + ?.where() + ?.filterEventsWithSettings() + ?.findAll() ?.forEach { sendingEvents.add(timelineEventMapper.map(it)) } @@ -380,7 +441,7 @@ internal class DefaultTimeline( if (initialEventId != null && shouldFetchInitialEvent) { fetchEvent(initialEventId) } else { - val count = Math.min(INITIAL_LOAD_SIZE, liveEvents.size) + val count = Math.min(settings.initialSize, liveEvents.size) if (isLive) { paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count) } else { @@ -397,9 +458,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 @@ -465,10 +526,11 @@ internal class DefaultTimeline( nextDisplayIndex = offsetIndex + 1 } offsetResults.forEach { eventEntity -> + val timelineEvent = timelineEventMapper.map(eventEntity) if (timelineEvent.isEncrypted() - && timelineEvent.root.mxDecryptionResult == null) { + && timelineEvent.root.mxDecryptionResult == null) { timelineEvent.root.eventId?.let { eventDecryptor.requestDecryption(it) } } @@ -500,7 +562,6 @@ internal class DefaultTimeline( .greaterThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex) } return offsetQuery - .filterAllowedTypes() .limit(count) .findAll() } @@ -559,14 +620,35 @@ internal class DefaultTimeline( } else { sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.ASCENDING) } - .filterAllowedTypes() + .filterEventsWithSettings() .findFirst() } - private fun RealmQuery.filterAllowedTypes(): RealmQuery { - if (allowedTypes != null) { - `in`(TimelineEventEntityFields.ROOT.TYPE, allowedTypes.toTypedArray()) + private fun RealmQuery.filterEventsWithSettings(): RealmQuery { + if (settings.filterTypes) { + `in`(TimelineEventEntityFields.ROOT.TYPE, settings.allowedTypes.toTypedArray()) } + if (settings.filterEdits) { + not().like(TimelineEventEntityFields.ROOT.CONTENT, EDIT_FILTER_LIKE) + } + return this + } + + /** + * We are looking for receipts related to filtered events. So, it's the opposite of [filterEventsWithSettings] method. + */ + private fun RealmQuery.filterReceiptsWithSettings(): RealmQuery { + beginGroup() + if (settings.filterTypes) { + not().`in`("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.TYPE}", settings.allowedTypes.toTypedArray()) + } + if (settings.filterTypes && settings.filterEdits) { + or() + } + if (settings.filterEdits) { + like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", EDIT_FILTER_LIKE) + } + endGroup() return this } } 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 94fc433a..a2a80274 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 @@ -23,7 +23,9 @@ import im.vector.matrix.android.api.session.crypto.CryptoService 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.TimelineService +import im.vector.matrix.android.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.internal.database.RealmLiveData +import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper 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.TimelineEventEntity @@ -38,10 +40,11 @@ internal class DefaultTimelineService @Inject constructor(private val roomId: St private val contextOfEventTask: GetContextOfEventTask, private val cryptoService: CryptoService, private val paginationTask: PaginationTask, - private val timelineEventMapper: TimelineEventMapper + private val timelineEventMapper: TimelineEventMapper, + private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper ) : TimelineService { - override fun createTimeline(eventId: String?, allowedTypes: List?): Timeline { + override fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline { return DefaultTimeline(roomId, eventId, monarchy.realmConfiguration, @@ -50,7 +53,8 @@ internal class DefaultTimelineService @Inject constructor(private val roomId: St paginationTask, cryptoService, timelineEventMapper, - allowedTypes + readReceiptsSummaryMapper, + settings ) } 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 5098e824..7c752e49 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 @@ -62,7 +62,7 @@ internal class ReadReceiptHandler @Inject constructor() { val readReceiptSummaries = ArrayList() for ((eventId, receiptDict) in content) { val userIdsDict = receiptDict[READ_KEY] ?: continue - val readReceiptsSummary = ReadReceiptsSummaryEntity(eventId = eventId) + val readReceiptsSummary = ReadReceiptsSummaryEntity(eventId = eventId, roomId = roomId) for ((userId, paramsDict) in userIdsDict) { val ts = paramsDict[TIMESTAMP_KEY] ?: 0.0 diff --git a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt index c2c86cad..b7d63e67 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt @@ -43,8 +43,6 @@ import im.vector.riotx.features.home.room.detail.RoomDetailViewModel import im.vector.riotx.features.home.room.detail.RoomDetailViewModel_AssistedFactory import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel_AssistedFactory -import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsViewModel -import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsViewModel_AssistedFactory import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsViewModel import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsViewModel_AssistedFactory import im.vector.riotx.features.home.room.detail.timeline.action.MessageMenuViewModel @@ -197,8 +195,4 @@ interface ViewModelModule { @Binds fun bindPushGatewaysViewModelFactory(factory: PushGatewaysViewModel_AssistedFactory): PushGatewaysViewModel.Factory - - @Binds - fun bindDisplayReadReceiptsViewModel(factory: DisplayReadReceiptsViewModel_AssistedFactory): DisplayReadReceiptsViewModel.Factory - } \ 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 544f05cd..04dd1150 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 @@ -817,8 +817,8 @@ class RoomDetailFragment : }) } - override fun onReadReceiptsClicked(informationData: MessageInformationData) { - DisplayReadReceiptsBottomSheet.newInstance(roomDetailArgs.roomId, informationData) + override fun onReadReceiptsClicked(readReceipts: List) { + DisplayReadReceiptsBottomSheet.newInstance(readReceipts) .show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS") } 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 2bb7327d..2bc08bd0 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.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.internal.crypto.attachments.toElementToDecrypt import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.matrix.rx.rx @@ -73,12 +74,13 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private val roomId = initialState.roomId private val eventId = initialState.eventId private val displayedEventsObservable = BehaviorRelay.create() - private val allowedTypes = if (userPreferencesProvider.shouldShowHiddenEvents()) { - TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES + private val timelineSettings = if (userPreferencesProvider.shouldShowHiddenEvents()) { + TimelineSettings(30, false, true, TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES) } else { - TimelineDisplayableEvents.DISPLAYABLE_TYPES + TimelineSettings(30, true, true, TimelineDisplayableEvents.DISPLAYABLE_TYPES) } - private var timeline = room.createTimeline(eventId, allowedTypes) + + private var timeline = room.createTimeline(eventId, timelineSettings) // Slot to keep a pending action during permission request var pendingAction: RoomDetailActions? = null @@ -137,7 +139,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 @@ -283,7 +285,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 { @@ -292,12 +294,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") } @@ -312,7 +314,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) @@ -550,7 +552,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } else { // change timeline timeline.dispose() - timeline = room.createTimeline(targetEventId, allowedTypes) + timeline = room.createTimeline(targetEventId, timelineSettings) timeline.start() withState { 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 572954f1..b8c1519f 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 @@ -16,8 +16,8 @@ package im.vector.riotx.features.home.room.detail.readreceipts -import android.annotation.SuppressLint import android.os.Bundle +import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -27,31 +27,31 @@ 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 -import im.vector.riotx.EmojiCompatFontProvider +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.TimelineEventFragmentArgs import im.vector.riotx.features.home.room.detail.timeline.action.VectorBaseBottomSheetDialogFragment -import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionBottomSheet -import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData +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 javax.inject.Inject +@Parcelize +data class DisplayReadReceiptArgs( + val readReceipts: List +) : Parcelable + /** * Bottom sheet displaying list of read receipts for a given event ordered by descending timestamp */ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() { - private val viewModel: DisplayReadReceiptsViewModel by fragmentViewModel() - - @Inject lateinit var displayReadReceiptsViewModelFactory: DisplayReadReceiptsViewModel.Factory @Inject lateinit var epoxyController: DisplayReadReceiptsController @BindView(R.id.bottom_sheet_display_reactions_list) lateinit var epoxyRecyclerView: EpoxyRecyclerView + private val displayReadReceiptArgs: DisplayReadReceiptArgs by args() override fun injectWith(screenComponent: ScreenComponent) { screenComponent.inject(this) @@ -70,20 +70,18 @@ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() { LinearLayout.VERTICAL) epoxyRecyclerView.addItemDecoration(dividerItemDecoration) bottomSheetTitle.text = getString(R.string.read_receipts_list) + epoxyController.setData(displayReadReceiptArgs.readReceipts) } - - override fun invalidate() = withState(viewModel) { - epoxyController.setData(it) + override fun invalidate() { + // we are not using state for this one as it's static } companion object { - fun newInstance(roomId: String, informationData: MessageInformationData): DisplayReadReceiptsBottomSheet { + fun newInstance(readReceipts: List): DisplayReadReceiptsBottomSheet { val args = Bundle() - val parcelableArgs = TimelineEventFragmentArgs( - informationData.eventId, - roomId, - informationData + val parcelableArgs = DisplayReadReceiptArgs( + readReceipts ) args.putParcelable(MvRx.KEY_ARG, parcelableArgs) return DisplayReadReceiptsBottomSheet().apply { arguments = args } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsController.kt index 2c2f9f49..51c150be 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsController.kt @@ -17,55 +17,32 @@ package im.vector.riotx.features.home.room.detail.readreceipts import com.airbnb.epoxy.TypedEpoxyController -import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.Incomplete -import com.airbnb.mvrx.Success import im.vector.matrix.android.api.session.Session -import im.vector.riotx.R import im.vector.riotx.core.date.VectorDateFormatter -import im.vector.riotx.core.resources.StringProvider -import im.vector.riotx.core.ui.list.genericFooterItem -import im.vector.riotx.core.ui.list.genericLoaderItem import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData import javax.inject.Inject /** * Epoxy controller for read receipt event list */ class DisplayReadReceiptsController @Inject constructor(private val dateFormatter: VectorDateFormatter, - private val stringProvider: StringProvider, private val session: Session, private val avatarRender: AvatarRenderer) - : TypedEpoxyController() { + : TypedEpoxyController>() { - override fun buildModels(state: DisplayReadReceiptsViewState) { - when (state.readReceipts) { - is Incomplete -> { - genericLoaderItem { - id("loading") - } - } - is Fail -> { - genericFooterItem { - id("failure") - text(stringProvider.getString(R.string.unknown_error)) - } - } - is Success -> { - state.readReceipts()?.forEach { - val timestamp = dateFormatter.formatRelativeDateTime(it.originServerTs) - DisplayReadReceiptItem_() - .id(it.user.userId) - .userId(it.user.userId) - .avatarUrl(it.user.avatarUrl) - .name(it.user.displayName) - .avatarRenderer(avatarRender) - .timestamp(timestamp) - .addIf(session.myUserId != it.user.userId, this) - } - } + override fun buildModels(readReceipts: List) { + readReceipts.forEach { + val timestamp = dateFormatter.formatRelativeDateTime(it.timestamp) + DisplayReadReceiptItem_() + .id(it.userId) + .userId(it.userId) + .avatarUrl(it.avatarUrl) + .name(it.displayName) + .avatarRenderer(avatarRender) + .timestamp(timestamp) + .addIf(session.myUserId != it.userId, this) } } - } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewModel.kt deleted file mode 100644 index 8423ba4a..00000000 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewModel.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.readreceipts - -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 - -/** - * Used to display the list of read receipts to a given event - */ -class DisplayReadReceiptsViewModel @AssistedInject constructor(@Assisted initialState: DisplayReadReceiptsViewState, - private val session: Session -) : VectorViewModel(initialState) { - - private val roomId = initialState.roomId - private val eventId = initialState.eventId - private val room = session.getRoom(roomId) - ?: throw IllegalStateException("Shouldn't use this ViewModel without a room") - - @AssistedInject.Factory - interface Factory { - fun create(initialState: DisplayReadReceiptsViewState): DisplayReadReceiptsViewModel - } - - companion object : MvRxViewModelFactory { - - override fun create(viewModelContext: ViewModelContext, state: DisplayReadReceiptsViewState): DisplayReadReceiptsViewModel? { - val fragment: DisplayReadReceiptsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.displayReadReceiptsViewModelFactory.create(state) - } - } - - init { - observeEventAnnotationSummaries() - } - - private fun observeEventAnnotationSummaries() { - RxRoom(room) - .liveEventReadReceipts(eventId) - .execute { - copy(readReceipts = it) - } - } - -} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewState.kt deleted file mode 100644 index 68952b99..00000000 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewState.kt +++ /dev/null @@ -1,33 +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.readreceipts - -import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState -import com.airbnb.mvrx.Uninitialized -import im.vector.matrix.android.api.session.room.model.ReadReceipt -import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs - -data class DisplayReadReceiptsViewState( - val eventId: String, - val roomId: String, - val readReceipts: Async> = Uninitialized -) : MvRxState { - - constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId) - -} \ No newline at end of file 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 28a3d100..3c212d61 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 @@ -38,6 +38,7 @@ 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.MessageInformationData +import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.riotx.features.media.ImageContentRenderer import im.vector.riotx.features.media.VideoContentRenderer import org.threeten.bp.LocalDateTime @@ -79,7 +80,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } interface ReadReceiptsCallback { - fun onReadReceiptsClicked(informationData: MessageInformationData) + fun onReadReceiptsClicked(readReceipts: List) } interface UrlClickCallback { 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 71e7627d..f3a93a8d 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 @@ -84,7 +84,7 @@ class MessageItemFactory @Inject constructor( private val imageContentRenderer: ImageContentRenderer, private val messageInformationDataFactory: MessageInformationDataFactory, private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder, - private val userPreferencesProvider: UserPreferencesProvider) { + private val noticeItemFactory: NoticeItemFactory) { fun create(event: TimelineEvent, @@ -109,27 +109,8 @@ class MessageItemFactory @Inject constructor( if (messageContent.relatesTo?.type == RelationType.REPLACE || event.isEncrypted() && event.root.content.toModel()?.relatesTo?.type == RelationType.REPLACE ) { - // ignore replace event, the targeted id is already edited - if (userPreferencesProvider.shouldShowHiddenEvents()) { - //These are just for debug to display hidden event, they should be filtered out in normal mode - val informationData = MessageInformationData( - eventId = event.root.eventId ?: "?", - senderId = event.root.senderId ?: "", - sendState = event.root.sendState, - time = "", - avatarUrl = event.senderAvatar(), - memberName = "", - showInformation = false - ) - return NoticeItem_() - .avatarRenderer(avatarRenderer) - .informationData(informationData) - .noticeText("{ \"type\": ${event.root.getClearType()} }") - .highlighted(highlight) - .baseCallback(callback) - } else { - return BlankItem_() - } + // This is an edit event, we should it when debugging as a notice event + return noticeItemFactory.create(event, highlight, callback) } // val all = event.root.toContent() // val ev = all.toModel() 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 43197d8b..b1ae595e 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 @@ -25,6 +25,7 @@ import im.vector.riotx.features.home.room.detail.timeline.TimelineEventControlle 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 @@ -33,8 +34,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me private val encryptedItemFactory: EncryptedItemFactory, private val noticeItemFactory: NoticeItemFactory, private val defaultItemFactory: DefaultItemFactory, - private val roomCreateItemFactory: RoomCreateItemFactory, - private val avatarRenderer: AvatarRenderer) { + private val roomCreateItemFactory: RoomCreateItemFactory) { fun create(event: TimelineEvent, nextEvent: TimelineEvent?, @@ -53,7 +53,9 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.STATE_HISTORY_VISIBILITY, EventType.CALL_INVITE, EventType.CALL_HANGUP, - EventType.CALL_ANSWER -> noticeItemFactory.create(event, highlight, callback) + EventType.CALL_ANSWER, + EventType.REACTION, + EventType.REDACTION -> noticeItemFactory.create(event, highlight, callback) // State room create EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback) // Crypto @@ -70,24 +72,9 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me // Unhandled event types (yet) EventType.STATE_ROOM_THIRD_PARTY_INVITE, EventType.STICKER -> defaultItemFactory.create(event, highlight) - else -> { - //These are just for debug to display hidden event, they should be filtered out in normal mode - val informationData = MessageInformationData( - eventId = event.root.eventId ?: "?", - senderId = event.root.senderId ?: "", - sendState = event.root.sendState, - time = "", - avatarUrl = event.senderAvatar(), - memberName = "", - showInformation = false - ) - NoticeItem_() - .avatarRenderer(avatarRenderer) - .informationData(informationData) - .noticeText("{ \"type\": ${event.root.getClearType()} }") - .highlighted(highlight) - .baseCallback(callback) + Timber.v("Type ${event.root.getClearType()} not handled") + null } } } catch (e: Exception) { 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 2b1a2633..05ce7a9c 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 @@ -22,7 +22,6 @@ 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.* import im.vector.matrix.android.api.session.room.model.call.CallInviteContent -import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotx.R import im.vector.riotx.core.resources.StringProvider @@ -42,6 +41,9 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin EventType.CALL_INVITE, EventType.CALL_HANGUP, EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) + EventType.MESSAGE, + EventType.REACTION, + EventType.REDACTION -> formatDebug(timelineEvent.root) else -> { Timber.v("Type $type not handled by this formatter") null @@ -66,6 +68,10 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin } } + private fun formatDebug(event: Event): CharSequence? { + return "{ \"type\": ${event.getClearType()} }" + } + private fun formatRoomNameEvent(event: Event, senderName: String?): CharSequence? { val content = event.getClearContent().toModel() ?: return null return if (!TextUtils.isEmpty(content.name)) { @@ -90,7 +96,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) @@ -140,7 +146,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) } @@ -167,7 +173,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() -> 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 6f3a0a1e..a394f471 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 @@ -80,7 +80,7 @@ abstract class AbsMessageItem : BaseEventItem() { }) private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener { - readReceiptsCallback?.onReadReceiptsClicked(informationData) + readReceiptsCallback?.onReadReceiptsClicked(informationData.readReceipts) }) var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener { 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 c4b5f042..51a7b0ce 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 @@ -50,7 +50,7 @@ abstract class NoticeItem : BaseEventItem() { var readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener { - readReceiptsCallback?.onReadReceiptsClicked(informationData) + readReceiptsCallback?.onReadReceiptsClicked(informationData.readReceipts) }) override fun bind(holder: Holder) { 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 a34c9874..a00dd3fa 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 @@ -52,15 +52,14 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses addDaySeparator || event.senderAvatar != nextEvent?.senderAvatar || event.getDisambiguatedDisplayName() != nextEvent?.getDisambiguatedDisplayName() - || (nextEvent?.root?.getClearType() != EventType.MESSAGE && nextEvent?.root?.getClearType() != EventType.ENCRYPTED) + || (nextEvent.root.getClearType() != EventType.MESSAGE && nextEvent.root.getClearType() != EventType.ENCRYPTED) || isNextMessageReceivedMoreThanOneHourAgo val time = dateFormatter.formatMessageHour(date) val avatarUrl = event.senderAvatar val memberName = event.getDisambiguatedDisplayName() val formattedMemberName = span(memberName) { - textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId - ?: "")) + textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId ?: "")) } return MessageInformationData( From 06dcf75a3211e6d057684715ccb5d8d18d2a68b3 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 13 Aug 2019 12:06:49 +0200 Subject: [PATCH 11/40] Read receipts: fix not appearing RR --- .../mapper/ReadReceiptsSummaryMapper.kt | 4 - .../database/mapper/TimelineEventMapper.kt | 12 +- .../session/room/read/DefaultReadService.kt | 2 + .../session/room/timeline/DefaultTimeline.kt | 112 ++++----------- .../room/timeline/DefaultTimelineService.kt | 4 +- .../timeline/TimelineHiddenReadReceipts.kt | 130 ++++++++++++++++++ .../session/sync/ReadReceiptHandler.kt | 4 +- 7 files changed, 170 insertions(+), 98 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/ReadReceiptsSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/ReadReceiptsSummaryMapper.kt index 1f53d1b4..9fa9fc01 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/ReadReceiptsSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/ReadReceiptsSummaryMapper.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.internal.database.mapper import im.vector.matrix.android.api.session.room.model.ReadReceipt -import im.vector.matrix.android.internal.database.model.ReadReceiptEntityFields import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity import im.vector.matrix.android.internal.database.model.UserEntity import im.vector.matrix.android.internal.database.query.where @@ -40,9 +39,6 @@ internal class ReadReceiptsSummaryMapper @Inject constructor(@SessionDatabase pr ?: return@mapNotNull null ReadReceipt(user.asDomain(), it.originServerTs.toLong()) } - .sortedByDescending { - it.originServerTs - } } } 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 42365d7e..84c860a8 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 @@ -26,9 +26,11 @@ import javax.inject.Inject internal class TimelineEventMapper @Inject constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper) { fun map(timelineEventEntity: TimelineEventEntity, correctedReadReceipts: List? = null): TimelineEvent { - val readReceipts = correctedReadReceipts ?: timelineEventEntity.readReceipts?.let { - readReceiptsSummaryMapper.map(it) - } + val readReceipts = correctedReadReceipts ?: timelineEventEntity.readReceipts + ?.let { + readReceiptsSummaryMapper.map(it) + } + return TimelineEvent( root = timelineEventEntity.root?.asDomain() ?: Event("", timelineEventEntity.eventId), @@ -38,7 +40,9 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS senderName = timelineEventEntity.senderName, isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName, senderAvatar = timelineEventEntity.senderAvatar, - readReceipts = readReceipts ?: emptyList() + readReceipts = readReceipts?.sortedByDescending { + it.originServerTs + } ?: emptyList() ) } 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 3df872bf..470668ef 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 @@ -96,6 +96,8 @@ internal class DefaultReadService @Inject constructor(private val roomId: String return Transformations.map(liveEntity) { realmResults -> realmResults.firstOrNull()?.let { readReceiptsSummaryMapper.map(it) + }?.sortedByDescending { + it.originServerTs } ?: emptyList() } } 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 059b2415..f961f9d5 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 @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.session.room.timeline -import android.util.SparseArray 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 @@ -34,8 +33,6 @@ 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.ReadReceiptsSummaryEntity -import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntityFields 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 @@ -68,7 +65,7 @@ import kotlin.collections.HashMap private const val MIN_FETCHING_COUNT = 30 private const val DISPLAY_INDEX_UNKNOWN = Int.MIN_VALUE -private const val EDIT_FILTER_LIKE = "{*\"m.relates_to\"*\"rel_type\":*\"m.replace\"*}" +internal const val EDIT_FILTER_LIKE = "{*\"m.relates_to\"*\"rel_type\":*\"m.replace\"*}" internal class DefaultTimeline( private val roomId: String, @@ -79,9 +76,9 @@ internal class DefaultTimeline( private val paginationTask: PaginationTask, private val cryptoService: CryptoService, private val timelineEventMapper: TimelineEventMapper, - private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, - private val settings: TimelineSettings -) : Timeline { + private val settings: TimelineSettings, + private val hiddenReadReceipts: TimelineHiddenReadReceipts +) : Timeline, TimelineHiddenReadReceipts.Delegate { private companion object { val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD") @@ -104,9 +101,6 @@ internal class DefaultTimeline( private lateinit var liveEvents: RealmResults private lateinit var eventRelations: RealmResults - private var hiddenReadReceipts: RealmResults? = null - private val correctedReadReceiptsEventByIndex = SparseArray() - private val correctedReadReceiptsByEvent = HashMap>() private var roomEntity: RoomEntity? = null @@ -118,10 +112,8 @@ internal class DefaultTimeline( private val backwardsPaginationState = AtomicReference(PaginationState()) private val forwardsPaginationState = AtomicReference(PaginationState()) - private val timelineID = UUID.randomUUID().toString() - private val eventDecryptor = TimelineEventDecryptor(realmConfiguration, timelineID, cryptoService) private val eventsChangeListener = OrderedRealmCollectionChangeListener> { results, changeSet -> @@ -162,7 +154,7 @@ internal class DefaultTimeline( builtEventsIdMap[eventId]?.let { builtIndex -> //Update an existing event builtEvents[builtIndex]?.let { te -> - builtEvents[builtIndex] = timelineEventMapper.map(eventEntity, correctedReadReceiptsByEvent[te.root.eventId]) + builtEvents[builtIndex] = timelineEventMapper.map(eventEntity, hiddenReadReceipts.correctedReadReceipts(te.root.eventId)) hasChanged = true } } @@ -192,56 +184,8 @@ internal class DefaultTimeline( postSnapshot() } - private val hiddenReadReceiptsListener = OrderedRealmCollectionChangeListener> { collection, changeSet -> - var hasChange = false - changeSet.deletions.forEach { - val eventId = correctedReadReceiptsEventByIndex[it] - val timelineEvent = liveEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, eventId).findFirst() - builtEventsIdMap[eventId]?.let { builtIndex -> - builtEvents[builtIndex]?.let { te -> - builtEvents[builtIndex] = te.copy(readReceipts = readReceiptsSummaryMapper.map(timelineEvent?.readReceipts)) - hasChange = true - } - } - } - correctedReadReceiptsEventByIndex.clear() - correctedReadReceiptsByEvent.clear() - val loadedReadReceipts = collection.where().greaterThan("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.DISPLAY_INDEX}", prevDisplayIndex).findAll() - loadedReadReceipts.forEachIndexed { index, summary -> - val timelineEvent = summary?.timelineEvent?.firstOrNull() - val displayIndex = timelineEvent?.root?.displayIndex - if (displayIndex != null) { - val firstDisplayedEvent = liveEvents.where() - .sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING) - .lessThan(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex) - .findFirst() - if (firstDisplayedEvent != null) { - correctedReadReceiptsEventByIndex.put(index, firstDisplayedEvent.eventId) - correctedReadReceiptsByEvent.getOrPut(firstDisplayedEvent.eventId, { - readReceiptsSummaryMapper.map(firstDisplayedEvent.readReceipts).toMutableList() - }).addAll( - readReceiptsSummaryMapper.map(summary) - ) - } - } - } - if (correctedReadReceiptsByEvent.isNotEmpty()) { - correctedReadReceiptsByEvent.forEach { (eventId, correctedReadReceipts) -> - builtEventsIdMap[eventId]?.let { builtIndex -> - builtEvents[builtIndex]?.let { te -> - builtEvents[builtIndex] = te.copy(readReceipts = correctedReadReceipts) - hasChange = true - } - } - } - } - if (hasChange) { - postSnapshot() - } - } - -// Public methods ****************************************************************************** + // Public methods ****************************************************************************** override fun paginate(direction: Timeline.Direction, count: Int) { BACKGROUND_HANDLER.post { @@ -295,12 +239,7 @@ internal class DefaultTimeline( .findAllAsync() .also { it.addChangeListener(relationsListener) } - hiddenReadReceipts = ReadReceiptsSummaryEntity.whereInRoom(realm, roomId) - .isNotEmpty(ReadReceiptsSummaryEntityFields.TIMELINE_EVENT) - .isNotEmpty(ReadReceiptsSummaryEntityFields.READ_RECEIPTS.`$`) - .filterReceiptsWithSettings() - .findAllAsync() - .also { it.addChangeListener(hiddenReadReceiptsListener) } + hiddenReadReceipts.start(realm, liveEvents, this) isReady.set(true) } @@ -315,8 +254,8 @@ internal class DefaultTimeline( cancelableBag.cancel() roomEntity?.sendingTimelineEvents?.removeAllChangeListeners() eventRelations.removeAllChangeListeners() - hiddenReadReceipts?.removeAllChangeListeners() liveEvents.removeAllChangeListeners() + hiddenReadReceipts.dispose() backgroundRealm.getAndSet(null).also { it.close() } @@ -328,6 +267,22 @@ internal class DefaultTimeline( return hasMoreInCache(direction) || !hasReachedEnd(direction) } + // 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 + } + + override fun onReadReceiptsUpdated() { + postSnapshot() + } + // Private methods ***************************************************************************** private fun hasMoreInCache(direction: Timeline.Direction): Boolean { @@ -608,7 +563,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 @@ -634,23 +589,6 @@ internal class DefaultTimeline( return this } - /** - * We are looking for receipts related to filtered events. So, it's the opposite of [filterEventsWithSettings] method. - */ - private fun RealmQuery.filterReceiptsWithSettings(): RealmQuery { - beginGroup() - if (settings.filterTypes) { - not().`in`("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.TYPE}", settings.allowedTypes.toTypedArray()) - } - if (settings.filterTypes && settings.filterEdits) { - or() - } - if (settings.filterEdits) { - like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", EDIT_FILTER_LIKE) - } - endGroup() - return this - } } private data class PaginationState( 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 a2a80274..82058a91 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 @@ -53,8 +53,8 @@ internal class DefaultTimelineService @Inject constructor(private val roomId: St paginationTask, cryptoService, timelineEventMapper, - readReceiptsSummaryMapper, - settings + settings, + TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings) ) } 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 new file mode 100644 index 00000000..db34d240 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt @@ -0,0 +1,130 @@ +/* + * 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 android.util.SparseArray +import im.vector.matrix.android.api.session.room.model.ReadReceipt +import im.vector.matrix.android.api.session.room.timeline.TimelineSettings +import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper +import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity +import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntityFields +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.whereInRoom +import io.realm.OrderedRealmCollectionChangeListener +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.RealmResults + +internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, + private val roomId: String, + private val settings: TimelineSettings) { + + interface Delegate { + fun rebuildEvent(eventId: String, readReceipts: List): Boolean + fun onReadReceiptsUpdated() + } + + private val correctedReadReceiptsEventByIndex = SparseArray() + private val correctedReadReceiptsByEvent = HashMap>() + + private lateinit var hiddenReadReceipts: RealmResults + private lateinit var liveEvents: RealmResults + private lateinit var delegate: Delegate + + private val hiddenReadReceiptsListener = OrderedRealmCollectionChangeListener> { collection, changeSet -> + var hasChange = false + changeSet.deletions.forEach { + val eventId = correctedReadReceiptsEventByIndex[it] + val timelineEvent = liveEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, eventId).findFirst() + val readReceipts = readReceiptsSummaryMapper.map(timelineEvent?.readReceipts) + hasChange = hasChange || delegate.rebuildEvent(eventId, readReceipts) + } + correctedReadReceiptsEventByIndex.clear() + correctedReadReceiptsByEvent.clear() + hiddenReadReceipts.forEachIndexed { index, summary -> + val timelineEvent = summary?.timelineEvent?.firstOrNull() + val displayIndex = timelineEvent?.root?.displayIndex + if (displayIndex != null) { + val firstDisplayedEvent = liveEvents.where() + .lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex) + .findFirst() + + if (firstDisplayedEvent != null) { + correctedReadReceiptsEventByIndex.put(index, firstDisplayedEvent.eventId) + correctedReadReceiptsByEvent.getOrPut(firstDisplayedEvent.eventId, { + readReceiptsSummaryMapper.map(firstDisplayedEvent.readReceipts).toMutableList() + }).addAll( + readReceiptsSummaryMapper.map(summary) + ) + } + } + } + if (correctedReadReceiptsByEvent.isNotEmpty()) { + correctedReadReceiptsByEvent.forEach { (eventId, correctedReadReceipts) -> + val sortedReadReceipts = correctedReadReceipts.sortedByDescending { + it.originServerTs + } + hasChange = hasChange || delegate.rebuildEvent(eventId, sortedReadReceipts) + } + } + if (hasChange) { + delegate.onReadReceiptsUpdated() + } + } + + + fun start(realm: Realm, liveEvents: RealmResults, delegate: Delegate) { + this.liveEvents = liveEvents + this.delegate = delegate + this.hiddenReadReceipts = ReadReceiptsSummaryEntity.whereInRoom(realm, roomId) + .isNotEmpty(ReadReceiptsSummaryEntityFields.TIMELINE_EVENT) + .isNotEmpty(ReadReceiptsSummaryEntityFields.READ_RECEIPTS.`$`) + .filterReceiptsWithSettings() + .findAllAsync() + .also { it.addChangeListener(hiddenReadReceiptsListener) } + } + + fun dispose() { + this.hiddenReadReceipts?.removeAllChangeListeners() + } + + fun correctedReadReceipts(eventId: String?): List? { + return correctedReadReceiptsByEvent[eventId] + } + + + /** + * We are looking for receipts related to filtered events. So, it's the opposite of [filterEventsWithSettings] method. + */ + private fun RealmQuery.filterReceiptsWithSettings(): RealmQuery { + beginGroup() + if (settings.filterTypes) { + not().`in`("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.TYPE}", settings.allowedTypes.toTypedArray()) + } + if (settings.filterTypes && settings.filterEdits) { + or() + } + if (settings.filterEdits) { + like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", EDIT_FILTER_LIKE) + } + 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/ReadReceiptHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/ReadReceiptHandler.kt index 7c752e49..e61e81dd 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 @@ -78,7 +78,9 @@ internal class ReadReceiptHandler @Inject constructor() { for ((eventId, receiptDict) in content) { val userIdsDict = receiptDict[READ_KEY] ?: continue val readReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst() - ?: realm.createObject(ReadReceiptsSummaryEntity::class.java, eventId) + ?: realm.createObject(ReadReceiptsSummaryEntity::class.java, eventId).apply { + this.roomId = roomId + } for ((userId, paramsDict) in userIdsDict) { val ts = paramsDict[TIMESTAMP_KEY] ?: 0.0 From b9cfda23b6497957a9bf6551139815a684040354 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 13 Aug 2019 15:06:00 +0200 Subject: [PATCH 12/40] Read receipts: just juste invisible on hidden avatars, to have a bigger touch zone --- .../im/vector/riotx/core/ui/views/ReadReceiptsView.kt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt index 6293e22b..44d1ee6f 100644 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt @@ -18,8 +18,10 @@ package im.vector.riotx.core.ui.views import android.content.Context import android.util.AttributeSet +import android.view.View import android.widget.ImageView import android.widget.LinearLayout +import androidx.core.view.isInvisible import androidx.core.view.isVisible import butterknife.ButterKnife import im.vector.riotx.R @@ -56,24 +58,23 @@ class ReadReceiptsView @JvmOverloads constructor( for (index in 0 until MAX_RECEIPT_DISPLAYED) { val receiptData = readReceipts.getOrNull(index) if (receiptData == null) { - receiptAvatars[index].isVisible = false + receiptAvatars[index].visibility = View.INVISIBLE } else { - receiptAvatars[index].isVisible = true + receiptAvatars[index].visibility = View.VISIBLE avatarRenderer.render(receiptData.avatarUrl, receiptData.userId, receiptData.displayName, receiptAvatars[index]) } } if (readReceipts.size > MAX_RECEIPT_DISPLAYED) { - receiptMore.isVisible = true + receiptMore.visibility = View.VISIBLE receiptMore.text = context.getString( R.string.x_plus, readReceipts.size - MAX_RECEIPT_DISPLAYED ) } else { - receiptMore.isVisible = false + receiptMore.visibility = View.GONE } } else { isVisible = false } - } } From 4e8dc724397de731294eeec4ca149f36a9977554 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 13 Aug 2019 15:17:04 +0200 Subject: [PATCH 13/40] Update CHANGES --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index b8fa02b9..3f83d977 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,7 +2,7 @@ Changes in RiotX 0.4.0 (2019-XX-XX) =================================================== Features: - - + - Display read receipts in timeline Improvements: - From 4ca2531e47b66575343d910bee31431e588b0667 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 14 Aug 2019 10:43:47 +0200 Subject: [PATCH 14/40] `develop` branch will have version code from timestamp, to ensure each build from CI has a incremented versionCode Other branches (master, features, etc.) will have version code based on application version. --- vector/build.gradle | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/vector/build.gradle b/vector/build.gradle index 8393462c..2e5c1adc 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -29,7 +29,15 @@ static def generateVersionCodeFromTimestamp() { } def generateVersionCodeFromVersionName() { - return versionMajor * 10000 + versionMinor * 100 + versionPatch + return versionMajor * 1_00_00 + versionMinor * 1_00 + versionPatch +} + +def getVersionCode() { + if (gitBranchName() == "develop") { + return generateVersionCodeFromTimestamp() + } else { + return generateVersionCodeFromVersionName() + } } static def gitRevision() { @@ -79,7 +87,9 @@ android { targetSdkVersion 28 multiDexEnabled true - // Note: versionCode is depending on the build variant + // `develop` branch will have version code from timestamp, to ensure each build from CI has a incremented versionCode. + // Other branches (master, features, etc.) will have version code based on application version. + versionCode project.getVersionCode() versionName "${versionMajor}.${versionMinor}.${versionPatch}${getVersionSuffix()}" @@ -169,8 +179,6 @@ android { gplay { dimension "store" - versionCode = generateVersionCodeFromVersionName() - buildConfigField "boolean", "ALLOW_FCM_USE", "true" buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"G\"" buildConfigField "String", "FLAVOR_DESCRIPTION", "\"GooglePlay\"" @@ -179,8 +187,6 @@ android { fdroid { dimension "store" - versionCode = generateVersionCodeFromTimestamp() - buildConfigField "boolean", "ALLOW_FCM_USE", "false" buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"F\"" buildConfigField "String", "FLAVOR_DESCRIPTION", "\"FDroid\"" From d3827b86735639e4cef78b1bf8e9ed463b62f0c0 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 14 Aug 2019 10:51:09 +0200 Subject: [PATCH 15/40] Read receipts: branch settings to show/hide them --- .../session/room/timeline/TimelineSettings.kt | 7 +++++- .../database/mapper/TimelineEventMapper.kt | 15 +++++++----- .../internal/database/query/FilterContent.kt | 23 ++++++++++++++++++ .../session/room/timeline/DefaultTimeline.kt | 24 ++++++++++++------- .../room/timeline/DefaultTimelineService.kt | 1 - .../timeline/TimelineHiddenReadReceipts.kt | 15 ++++++++---- .../core/resources/UserPreferencesProvider.kt | 5 ++++ .../home/room/detail/RoomDetailViewModel.kt | 6 ++--- 8 files changed, 73 insertions(+), 23 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.kt index 219c23eb..992cad41 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.kt @@ -35,5 +35,10 @@ data class TimelineSettings( /** * If [filterTypes] is true, the list of types allowed by the list. */ - val allowedTypes: List = emptyList() + val allowedTypes: List = emptyList(), + /** + * If true, will build read receipts for each event. + */ + val buildReadReceipts: Boolean = true + ) 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 84c860a8..fe98ebfb 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 @@ -25,12 +25,15 @@ import javax.inject.Inject internal class TimelineEventMapper @Inject constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper) { - fun map(timelineEventEntity: TimelineEventEntity, correctedReadReceipts: List? = null): TimelineEvent { - val readReceipts = correctedReadReceipts ?: timelineEventEntity.readReceipts - ?.let { - readReceiptsSummaryMapper.map(it) - } - + fun map(timelineEventEntity: TimelineEventEntity, buildReadReceipts: Boolean = true, correctedReadReceipts: List? = null): TimelineEvent { + val readReceipts = if (buildReadReceipts) { + correctedReadReceipts ?: timelineEventEntity.readReceipts + ?.let { + readReceiptsSummaryMapper.map(it) + } + } else { + null + } return TimelineEvent( root = timelineEventEntity.root?.asDomain() ?: Event("", timelineEventEntity.eventId), diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt new file mode 100644 index 00000000..9e6261c6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.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.matrix.android.internal.database.query + +internal object FilterContent { + + internal const val EDIT_TYPE = "{*\"m.relates_to\"*\"rel_type\":*\"m.replace\"*}" + +} \ 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 f961f9d5..03f5da6e 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 @@ -25,7 +25,6 @@ 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.mapper.ReadReceiptsSummaryMapper 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 @@ -36,6 +35,7 @@ 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 @@ -65,8 +65,6 @@ import kotlin.collections.HashMap private const val MIN_FETCHING_COUNT = 30 private const val DISPLAY_INDEX_UNKNOWN = Int.MIN_VALUE -internal const val EDIT_FILTER_LIKE = "{*\"m.relates_to\"*\"rel_type\":*\"m.replace\"*}" - internal class DefaultTimeline( private val roomId: String, private val initialEventId: String? = null, @@ -154,7 +152,7 @@ internal class DefaultTimeline( builtEventsIdMap[eventId]?.let { builtIndex -> //Update an existing event builtEvents[builtIndex]?.let { te -> - builtEvents[builtIndex] = timelineEventMapper.map(eventEntity, hiddenReadReceipts.correctedReadReceipts(te.root.eventId)) + builtEvents[builtIndex] = buildTimelineEvent(eventEntity) hasChanged = true } } @@ -239,7 +237,9 @@ internal class DefaultTimeline( .findAllAsync() .also { it.addChangeListener(relationsListener) } - hiddenReadReceipts.start(realm, liveEvents, this) + if (settings.buildReadReceipts) { + hiddenReadReceipts.start(realm, liveEvents, this) + } isReady.set(true) } @@ -255,7 +255,9 @@ internal class DefaultTimeline( roomEntity?.sendingTimelineEvents?.removeAllChangeListeners() eventRelations.removeAllChangeListeners() liveEvents.removeAllChangeListeners() - hiddenReadReceipts.dispose() + if (settings.buildReadReceipts) { + hiddenReadReceipts.dispose() + } backgroundRealm.getAndSet(null).also { it.close() } @@ -482,7 +484,7 @@ internal class DefaultTimeline( } offsetResults.forEach { eventEntity -> - val timelineEvent = timelineEventMapper.map(eventEntity) + val timelineEvent = buildTimelineEvent(eventEntity) if (timelineEvent.isEncrypted() && timelineEvent.root.mxDecryptionResult == null) { @@ -500,6 +502,12 @@ internal class DefaultTimeline( return offsetResults.size } + private fun buildTimelineEvent(eventEntity: TimelineEventEntity) = timelineEventMapper.map( + timelineEventEntity = eventEntity, + buildReadReceipts = settings.buildReadReceipts, + correctedReadReceipts = hiddenReadReceipts.correctedReadReceipts(eventEntity.eventId) + ) + /** * This has to be called on TimelineThread as it access realm live results */ @@ -584,7 +592,7 @@ internal class DefaultTimeline( `in`(TimelineEventEntityFields.ROOT.TYPE, settings.allowedTypes.toTypedArray()) } if (settings.filterEdits) { - not().like(TimelineEventEntityFields.ROOT.CONTENT, EDIT_FILTER_LIKE) + not().like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.EDIT_TYPE) } return this } 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 82058a91..441b0620 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 @@ -27,7 +27,6 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.internal.database.RealmLiveData import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper 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.TimelineEventEntity import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.task.TaskExecutor 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 db34d240..5678677d 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 @@ -24,6 +24,7 @@ import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntit import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntityFields 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.whereInRoom import io.realm.OrderedRealmCollectionChangeListener import io.realm.Realm @@ -48,11 +49,13 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu private val hiddenReadReceiptsListener = OrderedRealmCollectionChangeListener> { collection, changeSet -> 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 timelineEvent = liveEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, eventId).findFirst() + // We are rebuilding the corresponding event with only his own RR val readReceipts = readReceiptsSummaryMapper.map(timelineEvent?.readReceipts) - hasChange = hasChange || delegate.rebuildEvent(eventId, readReceipts) + hasChange = delegate.rebuildEvent(eventId, readReceipts) || hasChange } correctedReadReceiptsEventByIndex.clear() correctedReadReceiptsByEvent.clear() @@ -60,10 +63,12 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu val timelineEvent = summary?.timelineEvent?.firstOrNull() val displayIndex = timelineEvent?.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 if (firstDisplayedEvent != null) { correctedReadReceiptsEventByIndex.put(index, firstDisplayedEvent.eventId) correctedReadReceiptsByEvent.getOrPut(firstDisplayedEvent.eventId, { @@ -79,7 +84,7 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu val sortedReadReceipts = correctedReadReceipts.sortedByDescending { it.originServerTs } - hasChange = hasChange || delegate.rebuildEvent(eventId, sortedReadReceipts) + hasChange = delegate.rebuildEvent(eventId, sortedReadReceipts) || hasChange } } if (hasChange) { @@ -91,6 +96,8 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu 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). this.hiddenReadReceipts = ReadReceiptsSummaryEntity.whereInRoom(realm, roomId) .isNotEmpty(ReadReceiptsSummaryEntityFields.TIMELINE_EVENT) .isNotEmpty(ReadReceiptsSummaryEntityFields.READ_RECEIPTS.`$`) @@ -109,7 +116,7 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu /** - * We are looking for receipts related to filtered events. So, it's the opposite of [filterEventsWithSettings] method. + * We are looking for receipts related to filtered events. So, it's the opposite of [DefaultTimeline.filterEventsWithSettings] method. */ private fun RealmQuery.filterReceiptsWithSettings(): RealmQuery { beginGroup() @@ -120,7 +127,7 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu or() } if (settings.filterEdits) { - like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", EDIT_FILTER_LIKE) + like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", FilterContent.EDIT_TYPE) } endGroup() return this diff --git a/vector/src/main/java/im/vector/riotx/core/resources/UserPreferencesProvider.kt b/vector/src/main/java/im/vector/riotx/core/resources/UserPreferencesProvider.kt index 0d2c30a5..2d411f30 100644 --- a/vector/src/main/java/im/vector/riotx/core/resources/UserPreferencesProvider.kt +++ b/vector/src/main/java/im/vector/riotx/core/resources/UserPreferencesProvider.kt @@ -24,4 +24,9 @@ class UserPreferencesProvider @Inject constructor(private val vectorPreferences: fun shouldShowHiddenEvents(): Boolean { return vectorPreferences.shouldShowHiddenEvents() } + + fun shouldShowReadReceipts(): Boolean { + return vectorPreferences.showReadReceipts() + } + } \ No newline at end of file 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 12dace1b..1cd8cc4a 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 @@ -67,7 +67,7 @@ import java.util.concurrent.TimeUnit class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: RoomDetailViewState, - userPreferencesProvider: UserPreferencesProvider, + private val userPreferencesProvider: UserPreferencesProvider, private val vectorPreferences: VectorPreferences, private val session: Session ) : VectorViewModel(initialState) { @@ -77,9 +77,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private val eventId = initialState.eventId private val displayedEventsObservable = BehaviorRelay.create() private val timelineSettings = if (userPreferencesProvider.shouldShowHiddenEvents()) { - TimelineSettings(30, false, true, TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES) + TimelineSettings(30, false, true, TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES, userPreferencesProvider.shouldShowReadReceipts()) } else { - TimelineSettings(30, true, true, TimelineDisplayableEvents.DISPLAYABLE_TYPES) + TimelineSettings(30, true, true, TimelineDisplayableEvents.DISPLAYABLE_TYPES, userPreferencesProvider.shouldShowReadReceipts()) } private var timeline = room.createTimeline(eventId, timelineSettings) From 63af03bedd543be78d0c44367372cf651cf95bef Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 14 Aug 2019 14:18:42 +0200 Subject: [PATCH 16/40] List: add overScroll --- vector/src/main/res/layout/fragment_create_direct_room.xml | 1 + .../res/layout/fragment_create_direct_room_directory_users.xml | 1 + vector/src/main/res/layout/fragment_group_list.xml | 1 + vector/src/main/res/layout/fragment_public_rooms.xml | 1 + vector/src/main/res/layout/fragment_room_detail.xml | 1 + vector/src/main/res/layout/fragment_room_list.xml | 3 ++- 6 files changed, 7 insertions(+), 1 deletion(-) diff --git a/vector/src/main/res/layout/fragment_create_direct_room.xml b/vector/src/main/res/layout/fragment_create_direct_room.xml index 41109b14..66a040b9 100644 --- a/vector/src/main/res/layout/fragment_create_direct_room.xml +++ b/vector/src/main/res/layout/fragment_create_direct_room.xml @@ -127,6 +127,7 @@ android:layout_width="0dp" android:layout_height="0dp" android:fastScrollEnabled="true" + android:overScrollMode="always" android:scrollbars="vertical" app:layout_behavior="@string/appbar_scrolling_view_behavior" app:layout_constraintBottom_toBottomOf="parent" diff --git a/vector/src/main/res/layout/fragment_create_direct_room_directory_users.xml b/vector/src/main/res/layout/fragment_create_direct_room_directory_users.xml index 8416f35d..40821096 100644 --- a/vector/src/main/res/layout/fragment_create_direct_room_directory_users.xml +++ b/vector/src/main/res/layout/fragment_create_direct_room_directory_users.xml @@ -95,6 +95,7 @@ android:layout_height="0dp" android:layout_marginTop="16dp" android:fastScrollEnabled="true" + android:overScrollMode="always" android:scrollbars="vertical" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/vector/src/main/res/layout/fragment_group_list.xml b/vector/src/main/res/layout/fragment_group_list.xml index c371b465..5ccd63a2 100644 --- a/vector/src/main/res/layout/fragment_group_list.xml +++ b/vector/src/main/res/layout/fragment_group_list.xml @@ -9,6 +9,7 @@ android:id="@+id/groupListEpoxyRecyclerView" android:layout_width="match_parent" android:layout_height="match_parent" + android:overScrollMode="always" tools:listitem="@layout/item_group" /> diff --git a/vector/src/main/res/layout/fragment_public_rooms.xml b/vector/src/main/res/layout/fragment_public_rooms.xml index 99fbd896..ceb45b27 100644 --- a/vector/src/main/res/layout/fragment_public_rooms.xml +++ b/vector/src/main/res/layout/fragment_public_rooms.xml @@ -12,6 +12,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:background="?riotx_header_panel_background" + android:overScrollMode="always" app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:listitem="@layout/item_public_room" /> diff --git a/vector/src/main/res/layout/fragment_room_detail.xml b/vector/src/main/res/layout/fragment_room_detail.xml index dee37e9d..7b179323 100644 --- a/vector/src/main/res/layout/fragment_room_detail.xml +++ b/vector/src/main/res/layout/fragment_room_detail.xml @@ -98,6 +98,7 @@ android:id="@+id/recyclerView" android:layout_width="0dp" android:layout_height="0dp" + android:overScrollMode="always" app:layout_constraintBottom_toTopOf="@+id/recyclerViewBarrier" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/vector/src/main/res/layout/fragment_room_list.xml b/vector/src/main/res/layout/fragment_room_list.xml index c3fa4530..0259ca1e 100644 --- a/vector/src/main/res/layout/fragment_room_list.xml +++ b/vector/src/main/res/layout/fragment_room_list.xml @@ -10,7 +10,8 @@ + android:layout_height="match_parent" + android:overScrollMode="always" /> Date: Wed, 14 Aug 2019 14:18:56 +0200 Subject: [PATCH 17/40] Room list: let the fab animation be quicker --- .../im/vector/riotx/features/home/room/list/RoomListFragment.kt | 2 +- 1 file changed, 1 insertion(+), 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 4e8fe284..c0df7a8f 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 @@ -121,7 +121,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O when (newState) { RecyclerView.SCROLL_STATE_IDLE -> { - createChatFabMenu.postDelayed(showFabRunnable, 1000) + createChatFabMenu.postDelayed(showFabRunnable, 250) } RecyclerView.SCROLL_STATE_DRAGGING, RecyclerView.SCROLL_STATE_SETTLING -> { From e11c66035ce62242208022224a2981d3dbcf285e Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 14 Aug 2019 14:19:21 +0200 Subject: [PATCH 18/40] Theme: the action menu text items should use colorAccent --- 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 +- vector/src/main/res/values/theme_status.xml | 3 +-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/vector/src/main/res/values/theme_black.xml b/vector/src/main/res/values/theme_black.xml index d2b12e78..7b582e20 100644 --- a/vector/src/main/res/values/theme_black.xml +++ b/vector/src/main/res/values/theme_black.xml @@ -59,7 +59,7 @@ @color/riotx_links - + ?colorAccent diff --git a/vector/src/main/res/values/theme_dark.xml b/vector/src/main/res/values/theme_dark.xml index 11184539..b5796eb9 100644 --- a/vector/src/main/res/values/theme_dark.xml +++ b/vector/src/main/res/values/theme_dark.xml @@ -61,7 +61,7 @@ @color/riotx_links - + ?colorAccent @color/primary_color_dark diff --git a/vector/src/main/res/values/theme_light.xml b/vector/src/main/res/values/theme_light.xml index d55148e7..56f2b90b 100644 --- a/vector/src/main/res/values/theme_light.xml +++ b/vector/src/main/res/values/theme_light.xml @@ -60,7 +60,7 @@ @color/riotx_links - + ?colorAccent #FFF3F8FD diff --git a/vector/src/main/res/values/theme_status.xml b/vector/src/main/res/values/theme_status.xml index 6ad65f6a..322522c7 100644 --- a/vector/src/main/res/values/theme_status.xml +++ b/vector/src/main/res/values/theme_status.xml @@ -16,9 +16,8 @@ @color/link_color_status - - #FFFFFFFF + ?colorAccent @color/riot_primary_background_color_status From 501474b7207040a6cf11f61fbe4206096c182445 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 14 Aug 2019 14:53:40 +0200 Subject: [PATCH 19/40] Fix code quality issues --- .../internal/session/room/RoomFactory.kt | 20 +++++++++++++++++-- .../timeline/TimelineHiddenReadReceipts.kt | 15 ++++++++------ .../riotx/core/date/VectorDateFormatter.kt | 7 ++++++- 3 files changed, 33 insertions(+), 9 deletions(-) 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 e68e4282..d982763a 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 @@ -65,13 +65,29 @@ internal class RoomFactory @Inject constructor(private val context: Context, private val leaveRoomTask: LeaveRoomTask) { fun create(roomId: String): Room { - val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, cryptoService, paginationTask, timelineEventMapper, readReceiptsSummaryMapper) + val timelineService = DefaultTimelineService(roomId, + monarchy, + taskExecutor, + contextOfEventTask, + cryptoService, + paginationTask, + timelineEventMapper, + readReceiptsSummaryMapper + ) val sendService = DefaultSendService(context, credentials, roomId, eventFactory, cryptoService, monarchy) val stateService = DefaultStateService(roomId, monarchy.realmConfiguration, taskExecutor, sendStateTask) val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask) val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask, readReceiptsSummaryMapper, credentials) val relationService = DefaultRelationService(context, - credentials, roomId, eventFactory, cryptoService, findReactionEventForUndoTask, fetchEditHistoryTask, monarchy, taskExecutor) + credentials, + roomId, + eventFactory, + cryptoService, + findReactionEventForUndoTask, + fetchEditHistoryTask, + monarchy, + taskExecutor + ) return DefaultRoom( roomId, 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 5678677d..e42bf230 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 @@ -52,7 +52,10 @@ 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[it] - val timelineEvent = liveEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, eventId).findFirst() + val timelineEvent = liveEvents.where() + .equalTo(TimelineEventEntityFields.EVENT_ID, eventId) + .findFirst() + // We are rebuilding the corresponding event with only his own RR val readReceipts = readReceiptsSummaryMapper.map(timelineEvent?.readReceipts) hasChange = delegate.rebuildEvent(eventId, readReceipts) || hasChange @@ -71,11 +74,11 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu // If we find one, we should if (firstDisplayedEvent != null) { correctedReadReceiptsEventByIndex.put(index, firstDisplayedEvent.eventId) - correctedReadReceiptsByEvent.getOrPut(firstDisplayedEvent.eventId, { - readReceiptsSummaryMapper.map(firstDisplayedEvent.readReceipts).toMutableList() - }).addAll( - readReceiptsSummaryMapper.map(summary) - ) + correctedReadReceiptsByEvent + .getOrPut(firstDisplayedEvent.eventId, { + ArrayList(readReceiptsSummaryMapper.map(firstDisplayedEvent.readReceipts)) + }) + .addAll(readReceiptsSummaryMapper.map(summary)) } } } diff --git a/vector/src/main/java/im/vector/riotx/core/date/VectorDateFormatter.kt b/vector/src/main/java/im/vector/riotx/core/date/VectorDateFormatter.kt index 540400d5..f8e8d612 100644 --- a/vector/src/main/java/im/vector/riotx/core/date/VectorDateFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/core/date/VectorDateFormatter.kt @@ -46,7 +46,12 @@ class VectorDateFormatter @Inject constructor(private val context: Context, if (time == null) { return "" } - return DateUtils.getRelativeDateTimeString(context, time, DateUtils.DAY_IN_MILLIS, 2 * DateUtils.DAY_IN_MILLIS, DateUtils.FORMAT_SHOW_WEEKDAY).toString() + return DateUtils.getRelativeDateTimeString(context, + time, + DateUtils.DAY_IN_MILLIS, + 2 * DateUtils.DAY_IN_MILLIS, + DateUtils.FORMAT_SHOW_WEEKDAY + ).toString() } } \ No newline at end of file From e0628da1cb243fbc2d4810d115cc493de51729a4 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 14 Aug 2019 19:09:56 +0200 Subject: [PATCH 20/40] Dagger: use AssistedInjectModule for viewModel + use AssistedFactory for room dependencies --- .../di/SessionAssistedInjectModule.kt | 24 ++++++ .../internal/session/SessionComponent.kt | 4 +- .../android/internal/session/SessionModule.kt | 3 +- .../internal/session/room/RoomFactory.kt | 66 +++++--------- .../internal/session/room/RoomModule.kt | 43 +++------- .../membership/DefaultMembershipService.kt | 22 +++-- .../session/room/read/DefaultReadService.kt | 27 +++--- .../room/relation/DefaultRelationService.kt | 36 ++++---- .../session/room/send/DefaultSendService.kt | 38 ++++++--- .../session/room/state/DefaultStateService.kt | 27 +++--- .../room/timeline/DefaultTimelineService.kt | 20 +++-- .../riotx/core/di/AssistedInjectModule.kt | 24 ++++++ .../vector/riotx/core/di/ScreenComponent.kt | 16 +++- .../vector/riotx/core/di/ViewModelModule.kt | 85 +------------------ 14 files changed, 210 insertions(+), 225 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/SessionAssistedInjectModule.kt create mode 100644 vector/src/main/java/im/vector/riotx/core/di/AssistedInjectModule.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/SessionAssistedInjectModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/SessionAssistedInjectModule.kt new file mode 100644 index 00000000..1e8686da --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/SessionAssistedInjectModule.kt @@ -0,0 +1,24 @@ +/* + * 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.di + +import com.squareup.inject.assisted.dagger2.AssistedModule +import dagger.Module + +@AssistedModule +@Module(includes = [AssistedInject_SessionAssistedInjectModule::class]) +interface SessionAssistedInjectModule \ No newline at end of file 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 26f43be0..c8745fc3 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 @@ -22,6 +22,7 @@ import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.internal.crypto.CryptoModule import im.vector.matrix.android.internal.di.MatrixComponent +import im.vector.matrix.android.internal.di.SessionAssistedInjectModule import im.vector.matrix.android.internal.network.NetworkConnectivityChecker import im.vector.matrix.android.internal.session.cache.CacheModule import im.vector.matrix.android.internal.session.content.ContentModule @@ -59,7 +60,8 @@ import im.vector.matrix.android.internal.task.TaskExecutor CacheModule::class, CryptoModule::class, PushersModule::class, - AccountDataModule::class + AccountDataModule::class, + SessionAssistedInjectModule::class ] ) @SessionScope 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 38637fb7..ab44a4aa 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,7 +36,9 @@ 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 @@ -114,7 +116,6 @@ internal abstract class SessionModule { } } - @Binds abstract fun bindSession(session: DefaultSession): Session 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 1c64c91b..53da2d77 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 @@ -16,70 +16,46 @@ package im.vector.matrix.android.internal.session.room -import android.content.Context import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService -import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask -import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask -import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask -import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask import im.vector.matrix.android.internal.session.room.read.DefaultReadService -import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask import im.vector.matrix.android.internal.session.room.relation.DefaultRelationService -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.send.DefaultSendService -import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory import im.vector.matrix.android.internal.session.room.state.DefaultStateService -import im.vector.matrix.android.internal.session.room.state.SendStateTask import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService -import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask -import im.vector.matrix.android.internal.session.room.timeline.PaginationTask -import im.vector.matrix.android.internal.task.TaskExecutor import javax.inject.Inject -internal class RoomFactory @Inject constructor(private val context: Context, - private val credentials: Credentials, - private val monarchy: Monarchy, - private val eventFactory: LocalEchoEventFactory, - private val roomSummaryMapper: RoomSummaryMapper, - private val taskExecutor: TaskExecutor, - private val loadRoomMembersTask: LoadRoomMembersTask, - private val inviteTask: InviteTask, - private val sendStateTask: SendStateTask, - private val paginationTask: PaginationTask, - private val contextOfEventTask: GetContextOfEventTask, - private val setReadMarkersTask: SetReadMarkersTask, - private val cryptoService: CryptoService, - private val findReactionEventForUndoTask: FindReactionEventForUndoTask, - private val fetchEditHistoryTask: FetchEditHistoryTask, - private val joinRoomTask: JoinRoomTask, - private val leaveRoomTask: LeaveRoomTask) { - fun create(roomId: String): Room { - val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, cryptoService, paginationTask) - val sendService = DefaultSendService(context, credentials, roomId, eventFactory, cryptoService, monarchy) - val stateService = DefaultStateService(roomId, monarchy.realmConfiguration, taskExecutor, sendStateTask) - val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask) - val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask, credentials) - val relationService = DefaultRelationService(context, - credentials, roomId, eventFactory, cryptoService, findReactionEventForUndoTask, fetchEditHistoryTask, monarchy, taskExecutor) +internal interface RoomFactory { + fun create(roomId: String): Room +} +internal class DefaultRoomFactory @Inject constructor(private val monarchy: Monarchy, + private val roomSummaryMapper: RoomSummaryMapper, + private val cryptoService: CryptoService, + private val timelineServiceFactory: DefaultTimelineService.Factory, + private val sendServiceFactory: DefaultSendService.Factory, + private val stateServiceFactory: DefaultStateService.Factory, + private val readServiceFactory: DefaultReadService.Factory, + private val relationServiceFactory: DefaultRelationService.Factory, + private val membershipServiceFactory: DefaultMembershipService.Factory) : + RoomFactory { + + override fun create(roomId: String): Room { return DefaultRoom( roomId, monarchy, roomSummaryMapper, - timelineService, - sendService, - stateService, - readService, + timelineServiceFactory.create(roomId), + sendServiceFactory.create(roomId), + stateServiceFactory.create(roomId), + readServiceFactory.create(roomId), cryptoService, - relationService, - roomMembersService + relationServiceFactory.create(roomId), + membershipServiceFactory.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 942239ea..572e03d0 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 @@ -22,12 +22,6 @@ import dagger.Provides import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.room.RoomDirectoryService import im.vector.matrix.android.api.session.room.RoomService -import im.vector.matrix.android.api.session.room.members.MembershipService -import im.vector.matrix.android.api.session.room.model.relation.RelationService -import im.vector.matrix.android.api.session.room.read.ReadService -import im.vector.matrix.android.api.session.room.send.SendService -import im.vector.matrix.android.api.session.room.state.StateService -import im.vector.matrix.android.api.session.room.timeline.TimelineService import im.vector.matrix.android.internal.session.DefaultFileService import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.room.create.CreateRoomTask @@ -37,7 +31,6 @@ import im.vector.matrix.android.internal.session.room.directory.DefaultGetThirdP import im.vector.matrix.android.internal.session.room.directory.GetPublicRoomTask import im.vector.matrix.android.internal.session.room.directory.GetThirdPartyProtocolsTask import im.vector.matrix.android.internal.session.room.membership.DefaultLoadRoomMembersTask -import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask import im.vector.matrix.android.internal.session.room.membership.joining.DefaultInviteTask import im.vector.matrix.android.internal.session.room.membership.joining.DefaultJoinRoomTask @@ -47,15 +40,20 @@ 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.DefaultReadService import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask 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.send.DefaultSendService +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.state.DefaultSendStateTask -import im.vector.matrix.android.internal.session.room.state.DefaultStateService 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.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 @@ -71,6 +69,9 @@ internal abstract class RoomModule { } } + @Binds + abstract fun bindRoomFactory(roomFactory: DefaultRoomFactory): RoomFactory + @Binds abstract fun bindRoomService(roomService: DefaultRoomService): RoomService @@ -98,24 +99,15 @@ internal abstract class RoomModule { @Binds abstract fun bindLeaveRoomTask(leaveRoomTask: DefaultLeaveRoomTask): LeaveRoomTask - @Binds - abstract fun bindMembershipService(membershipService: DefaultMembershipService): MembershipService - @Binds abstract fun bindLoadRoomMembersTask(loadRoomMembersTask: DefaultLoadRoomMembersTask): LoadRoomMembersTask @Binds abstract fun bindPruneEventTask(pruneEventTask: DefaultPruneEventTask): PruneEventTask - @Binds - abstract fun bindReadService(readService: DefaultReadService): ReadService - @Binds abstract fun bindSetReadMarkersTask(setReadMarkersTask: DefaultSetReadMarkersTask): SetReadMarkersTask - @Binds - abstract fun bindRelationService(relationService: DefaultRelationService): RelationService - @Binds abstract fun bindFindReactionEventForUndoTask(findReactionEventForUndoTask: DefaultFindReactionEventForUndoTask): FindReactionEventForUndoTask @@ -125,21 +117,12 @@ internal abstract class RoomModule { @Binds abstract fun bindSendStateTask(sendStateTask: DefaultSendStateTask): SendStateTask - @Binds - abstract fun bindSendService(sendService: DefaultSendService): SendService - - @Binds - abstract fun bindStateService(stateService: DefaultStateService): StateService - @Binds abstract fun bindGetContextOfEventTask(getContextOfEventTask: DefaultGetContextOfEventTask): GetContextOfEventTask @Binds abstract fun bindPaginationTask(paginationTask: DefaultPaginationTask): PaginationTask - @Binds - abstract fun bindTimelineService(timelineService: DefaultTimelineService): TimelineService - @Binds abstract fun bindFileService(fileService: DefaultFileService): FileService diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt index 7fca1e42..a1aac0ae 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt @@ -17,6 +17,8 @@ package im.vector.matrix.android.internal.session.room.membership import androidx.lifecycle.LiveData +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.events.model.toModel @@ -31,17 +33,21 @@ import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRo import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.fetchCopied -import javax.inject.Inject -internal class DefaultMembershipService @Inject constructor(private val roomId: String, - private val monarchy: Monarchy, - private val taskExecutor: TaskExecutor, - private val loadRoomMembersTask: LoadRoomMembersTask, - private val inviteTask: InviteTask, - private val joinTask: JoinRoomTask, - private val leaveRoomTask: LeaveRoomTask +internal class DefaultMembershipService @AssistedInject constructor(@Assisted private val roomId: String, + private val monarchy: Monarchy, + private val taskExecutor: TaskExecutor, + private val loadRoomMembersTask: LoadRoomMembersTask, + private val inviteTask: InviteTask, + private val joinTask: JoinRoomTask, + private val leaveRoomTask: LeaveRoomTask ) : MembershipService { + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): MembershipService + } + override fun loadRoomMembersIfNeeded(matrixCallback: MatrixCallback): Cancelable { val params = LoadRoomMembersTask.Params(roomId, Membership.LEAVE) return loadRoomMembersTask 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 2e30c12e..eb31f5a9 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 @@ -16,6 +16,8 @@ package im.vector.matrix.android.internal.session.room.read +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.data.Credentials @@ -27,13 +29,18 @@ import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoo import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith -import javax.inject.Inject -internal class DefaultReadService @Inject constructor(private val roomId: String, - private val monarchy: Monarchy, - private val taskExecutor: TaskExecutor, - private val setReadMarkersTask: SetReadMarkersTask, - private val credentials: Credentials) : ReadService { +internal class DefaultReadService @AssistedInject constructor(@Assisted private val roomId: String, + private val monarchy: Monarchy, + private val taskExecutor: TaskExecutor, + private val setReadMarkersTask: SetReadMarkersTask, + private val credentials: Credentials +) : ReadService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): ReadService + } override fun markAllAsRead(callback: MatrixCallback) { val params = SetReadMarkersTask.Params(roomId, markAllAsRead = true) @@ -67,13 +74,13 @@ internal class DefaultReadService @Inject constructor(private val roomId: String 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 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 cd31c978..addad44b 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 @@ -19,6 +19,8 @@ import android.content.Context import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations import androidx.work.OneTimeWorkRequest +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.data.Credentials @@ -45,19 +47,23 @@ import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.CancelableWork import im.vector.matrix.android.internal.worker.WorkerParamsFactory import timber.log.Timber -import javax.inject.Inject -internal class DefaultRelationService @Inject constructor(private val context: Context, - private val credentials: Credentials, - private val roomId: String, - private val eventFactory: LocalEchoEventFactory, - private val cryptoService: CryptoService, - private val findReactionEventForUndoTask: FindReactionEventForUndoTask, - private val fetchEditHistoryTask: FetchEditHistoryTask, - private val monarchy: Monarchy, - private val taskExecutor: TaskExecutor) +internal class DefaultRelationService @AssistedInject constructor(@Assisted private val roomId: String, + private val context: Context, + private val credentials: Credentials, + private val eventFactory: LocalEchoEventFactory, + private val cryptoService: CryptoService, + private val findReactionEventForUndoTask: FindReactionEventForUndoTask, + private val fetchEditHistoryTask: FetchEditHistoryTask, + private val monarchy: Monarchy, + private val taskExecutor: TaskExecutor) : RelationService { + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): RelationService + } + override fun sendReaction(reaction: String, targetEventId: String): Cancelable { val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction) .also { @@ -148,9 +154,9 @@ internal class DefaultRelationService @Inject constructor(private val context: C compatibilityBodyText: String): Cancelable { val event = eventFactory .createReplaceTextOfReply(roomId, - replyToEdit, - originalEvent, - newBodyText, true, MessageType.MSGTYPE_TEXT, compatibilityBodyText) + replyToEdit, + originalEvent, + newBodyText, true, MessageType.MSGTYPE_TEXT, compatibilityBodyText) .also { saveLocalEcho(it) } @@ -214,7 +220,7 @@ internal class DefaultRelationService @Inject constructor(private val context: C } return Transformations.map(liveEntity) { realmResults -> realmResults.firstOrNull()?.asDomain() - ?: EventAnnotationsSummary(eventId, emptyList(), null) + ?: EventAnnotationsSummary(eventId, emptyList(), null) } } @@ -227,7 +233,7 @@ internal class DefaultRelationService @Inject constructor(private val context: C private fun saveLocalEcho(event: Event) { monarchy.writeAsync { realm -> val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() - ?: return@writeAsync + ?: return@writeAsync roomEntity.addSendingEvent(event) } } 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 d822e949..2c20839b 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 @@ -17,12 +17,22 @@ package im.vector.matrix.android.internal.session.room.send import android.content.Context -import androidx.work.* +import androidx.work.BackoffPolicy +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequest +import androidx.work.Operation +import androidx.work.WorkManager +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.crypto.CryptoService -import im.vector.matrix.android.api.session.events.model.* +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.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.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.send.SendService @@ -47,18 +57,22 @@ import im.vector.matrix.android.internal.worker.startChain import timber.log.Timber import java.util.concurrent.Executors import java.util.concurrent.TimeUnit -import javax.inject.Inject private const val UPLOAD_WORK = "UPLOAD_WORK" private const val BACKOFF_DELAY = 10_000L -internal class DefaultSendService @Inject constructor(private val context: Context, - private val credentials: Credentials, - private val roomId: String, - private val localEchoEventFactory: LocalEchoEventFactory, - private val cryptoService: CryptoService, - private val monarchy: Monarchy) - : SendService { +internal class DefaultSendService @AssistedInject constructor(@Assisted private val roomId: String, + private val context: Context, + private val credentials: Credentials, + private val localEchoEventFactory: LocalEchoEventFactory, + private val cryptoService: CryptoService, + private val monarchy: Monarchy +) : SendService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): SendService + } private val workerFutureListenerExecutor = Executors.newSingleThreadExecutor() override fun sendTextMessage(text: String, msgType: String, autoMarkdown: Boolean): Cancelable { @@ -152,11 +166,11 @@ internal class DefaultSendService @Inject constructor(private val context: Conte override fun deleteFailedEcho(localEcho: TimelineEvent) { monarchy.writeAsync { realm -> TimelineEventEntity.where(realm, eventId = localEcho.root.eventId - ?: "").findFirst()?.let { + ?: "").findFirst()?.let { it.deleteFromRealm() } EventEntity.where(realm, eventId = localEcho.root.eventId - ?: "").findFirst()?.let { + ?: "").findFirst()?.let { it.deleteFromRealm() } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt index 60999b61..f51b0e04 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt @@ -16,6 +16,8 @@ package im.vector.matrix.android.internal.session.room.state +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.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType @@ -29,13 +31,18 @@ import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import io.realm.Realm import io.realm.RealmConfiguration -import javax.inject.Inject -internal class DefaultStateService @Inject constructor(private val roomId: String, - @SessionDatabase - private val realmConfiguration: RealmConfiguration, - private val taskExecutor: TaskExecutor, - private val sendStateTask: SendStateTask) : StateService { +internal class DefaultStateService @AssistedInject constructor(@Assisted private val roomId: String, + @SessionDatabase + private val realmConfiguration: RealmConfiguration, + private val taskExecutor: TaskExecutor, + private val sendStateTask: SendStateTask +) : StateService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): StateService + } override fun getStateEvent(eventType: String): Event? { return Realm.getInstance(realmConfiguration).use { realm -> @@ -45,10 +52,10 @@ internal class DefaultStateService @Inject constructor(private val roomId: Strin override fun updateTopic(topic: String, callback: MatrixCallback) { val params = SendStateTask.Params(roomId, - EventType.STATE_ROOM_TOPIC, - mapOf( - "topic" to topic - )) + EventType.STATE_ROOM_TOPIC, + mapOf( + "topic" to topic + )) sendStateTask 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 d70f1b92..d41b74ca 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 @@ -18,6 +18,8 @@ package im.vector.matrix.android.internal.session.room.timeline import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.room.timeline.Timeline @@ -29,16 +31,20 @@ import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.fetchCopyMap -import javax.inject.Inject -internal class DefaultTimelineService @Inject constructor(private val roomId: String, - private val monarchy: Monarchy, - private val taskExecutor: TaskExecutor, - private val contextOfEventTask: GetContextOfEventTask, - private val cryptoService: CryptoService, - private val paginationTask: PaginationTask +internal class DefaultTimelineService @AssistedInject constructor(@Assisted private val roomId: String, + private val monarchy: Monarchy, + private val taskExecutor: TaskExecutor, + private val contextOfEventTask: GetContextOfEventTask, + private val cryptoService: CryptoService, + private val paginationTask: PaginationTask ) : TimelineService { + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): TimelineService + } + override fun createTimeline(eventId: String?, allowedTypes: List?): Timeline { return DefaultTimeline(roomId, eventId, diff --git a/vector/src/main/java/im/vector/riotx/core/di/AssistedInjectModule.kt b/vector/src/main/java/im/vector/riotx/core/di/AssistedInjectModule.kt new file mode 100644 index 00000000..915780db --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/di/AssistedInjectModule.kt @@ -0,0 +1,24 @@ +/* + * 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.di + +import com.squareup.inject.assisted.dagger2.AssistedModule +import dagger.Module + +@AssistedModule +@Module(includes = [AssistedInject_AssistedInjectModule::class]) +interface AssistedInjectModule \ No newline at end of file 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 fb1c18f9..c6f71189 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,7 +41,11 @@ import im.vector.riotx.features.home.createdirect.CreateDirectRoomDirectoryUsers import im.vector.riotx.features.home.createdirect.CreateDirectRoomKnownUsersFragment 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.timeline.action.* +import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet +import im.vector.riotx.features.home.room.detail.timeline.action.MessageMenuFragment +import im.vector.riotx.features.home.room.detail.timeline.action.QuickReactionFragment +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.filtered.FilteredRoomsActivity import im.vector.riotx.features.home.room.list.RoomListFragment import im.vector.riotx.features.invite.VectorInviteView @@ -59,10 +63,16 @@ import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment 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.VectorSettingsActivity +import im.vector.riotx.features.settings.VectorSettingsAdvancedNotificationPreferenceFragment +import im.vector.riotx.features.settings.VectorSettingsHelpAboutFragment +import im.vector.riotx.features.settings.VectorSettingsNotificationPreferenceFragment +import im.vector.riotx.features.settings.VectorSettingsNotificationsTroubleshootFragment +import im.vector.riotx.features.settings.VectorSettingsPreferencesFragment +import im.vector.riotx.features.settings.VectorSettingsSecurityPrivacyFragment import im.vector.riotx.features.settings.push.PushGatewaysFragment -@Component(dependencies = [VectorComponent::class], modules = [ViewModelModule::class, HomeModule::class]) +@Component(dependencies = [VectorComponent::class], modules = [AssistedInjectModule::class, ViewModelModule::class, HomeModule::class]) @ScreenScope interface ScreenComponent { diff --git a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt index 80410f87..954b067d 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt @@ -25,41 +25,17 @@ import im.vector.riotx.core.platform.ConfigurationViewModel import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromKeyViewModel import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromPassphraseViewModel import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreSharedViewModel -import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsViewModel -import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsViewModel_AssistedFactory import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupSharedViewModel import im.vector.riotx.features.crypto.verification.SasVerificationViewModel -import im.vector.riotx.features.home.* +import im.vector.riotx.features.home.HomeNavigationViewModel import im.vector.riotx.features.home.createdirect.CreateDirectRoomNavigationViewModel -import im.vector.riotx.features.home.createdirect.CreateDirectRoomViewModel -import im.vector.riotx.features.home.createdirect.CreateDirectRoomViewModel_AssistedFactory -import im.vector.riotx.features.home.group.GroupListViewModel -import im.vector.riotx.features.home.group.GroupListViewModel_AssistedFactory -import im.vector.riotx.features.home.room.detail.RoomDetailViewModel -import im.vector.riotx.features.home.room.detail.RoomDetailViewModel_AssistedFactory -import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel -import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel_AssistedFactory -import im.vector.riotx.features.home.room.detail.timeline.action.* -import im.vector.riotx.features.home.room.list.RoomListViewModel -import im.vector.riotx.features.home.room.list.RoomListViewModel_AssistedFactory import im.vector.riotx.features.reactions.EmojiChooserViewModel import im.vector.riotx.features.roomdirectory.RoomDirectoryNavigationViewModel -import im.vector.riotx.features.roomdirectory.RoomDirectoryViewModel -import im.vector.riotx.features.roomdirectory.RoomDirectoryViewModel_AssistedFactory -import im.vector.riotx.features.roomdirectory.createroom.CreateRoomViewModel -import im.vector.riotx.features.roomdirectory.createroom.CreateRoomViewModel_AssistedFactory -import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerViewModel -import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerViewModel_AssistedFactory -import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewViewModel -import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewViewModel_AssistedFactory -import im.vector.riotx.features.settings.push.PushGatewaysViewModel -import im.vector.riotx.features.settings.push.PushGatewaysViewModel_AssistedFactory import im.vector.riotx.features.workers.signout.SignOutViewModel @Module interface ViewModelModule { - /** * ViewModels with @IntoMap will be injected by this factory */ @@ -69,6 +45,7 @@ interface ViewModelModule { /** * Below are bindings for the androidx view models (which extend ViewModel). Will be converted to MvRx ViewModel in the future. */ + @Binds @IntoMap @ViewModelKey(SignOutViewModel::class) @@ -124,62 +101,4 @@ interface ViewModelModule { @ViewModelKey(CreateDirectRoomNavigationViewModel::class) fun bindCreateDirectRoomNavigationViewModel(viewModel: CreateDirectRoomNavigationViewModel): ViewModel - /** - * Below are bindings for the MvRx view models (which extend VectorViewModel). Will be the only usage in the future. - */ - - @Binds - fun bindHomeActivityViewModelFactory(factory: HomeActivityViewModel_AssistedFactory): HomeActivityViewModel.Factory - - @Binds - fun bindTextComposerViewModelFactory(factory: TextComposerViewModel_AssistedFactory): TextComposerViewModel.Factory - - @Binds - fun bindRoomDetailViewModelFactory(factory: RoomDetailViewModel_AssistedFactory): RoomDetailViewModel.Factory - - @Binds - fun bindQuickReactionViewModelFactory(factory: QuickReactionViewModel_AssistedFactory): QuickReactionViewModel.Factory - - @Binds - fun bindMessageActionsViewModelFactory(factory: MessageActionsViewModel_AssistedFactory): MessageActionsViewModel.Factory - - @Binds - fun bindMessageMenuViewModelFactory(factory: MessageMenuViewModel_AssistedFactory): MessageMenuViewModel.Factory - - @Binds - fun bindRoomListViewModelFactory(factory: RoomListViewModel_AssistedFactory): RoomListViewModel.Factory - - @Binds - fun bindGroupListViewModelFactory(factory: GroupListViewModel_AssistedFactory): GroupListViewModel.Factory - - @Binds - fun bindHomeDetailViewModelFactory(factory: HomeDetailViewModel_AssistedFactory): HomeDetailViewModel.Factory - - @Binds - fun bindKeysBackupSettingsViewModelFactory(factory: KeysBackupSettingsViewModel_AssistedFactory): KeysBackupSettingsViewModel.Factory - - @Binds - fun bindRoomDirectoryPickerViewModelFactory(factory: RoomDirectoryPickerViewModel_AssistedFactory): RoomDirectoryPickerViewModel.Factory - - @Binds - fun bindRoomDirectoryViewModelFactory(factory: RoomDirectoryViewModel_AssistedFactory): RoomDirectoryViewModel.Factory - - @Binds - fun bindRoomPreviewViewModelFactory(factory: RoomPreviewViewModel_AssistedFactory): RoomPreviewViewModel.Factory - - @Binds - fun bindViewReactionViewModelFactory(factory: ViewReactionViewModel_AssistedFactory): ViewReactionViewModel.Factory - - @Binds - fun bindViewEditHistoryViewModelFactory(factory: ViewEditHistoryViewModel_AssistedFactory): ViewEditHistoryViewModel.Factory - - @Binds - fun bindCreateRoomViewModelFactory(factory: CreateRoomViewModel_AssistedFactory): CreateRoomViewModel.Factory - - @Binds - fun bindCreateDirectRoomViewModelFactory(factory: CreateDirectRoomViewModel_AssistedFactory): CreateDirectRoomViewModel.Factory - - @Binds - fun bindPushGatewaysViewModelFactory(factory: PushGatewaysViewModel_AssistedFactory): PushGatewaysViewModel.Factory - } \ No newline at end of file From fd74e3dfb1f5471cb458891c394b206ea77cb4b9 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 19 Aug 2019 14:08:15 +0200 Subject: [PATCH 21/40] Read receipts: clean code after review --- CHANGES.md | 2 +- .../internal/database/query/FilterContent.kt | 2 +- .../room/timeline/TimelineHiddenReadReceipts.kt | 17 +++++++++++++++-- .../res/layout/item_display_read_receipt.xml | 11 +++++------ .../res/layout/item_simple_reaction_info.xml | 6 +++--- 5 files changed, 25 insertions(+), 13 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 3f83d977..682c176b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,7 +2,7 @@ Changes in RiotX 0.4.0 (2019-XX-XX) =================================================== Features: - - Display read receipts in timeline + - Display read receipts in timeline (#81) Improvements: - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt index 9e6261c6..92608a1f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt @@ -18,6 +18,6 @@ package im.vector.matrix.android.internal.database.query internal object FilterContent { - internal const val EDIT_TYPE = "{*\"m.relates_to\"*\"rel_type\":*\"m.replace\"*}" + internal const val EDIT_TYPE = """{*"m.relates_to"*"rel_type":*"m.replace"*}""" } \ No newline at end of file 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 e42bf230..56582103 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 @@ -31,6 +31,11 @@ import io.realm.Realm import io.realm.RealmQuery import io.realm.RealmResults +/** + * This class is responsible for handling the read receipts for hidden events (check [TimelineSettings] to see filtering). + * When an hidden event has read receipts, we want to transfer these read receipts 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 TimelineHiddenReadReceipts constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, private val roomId: String, private val settings: TimelineSettings) { @@ -95,7 +100,9 @@ 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 this.delegate = delegate @@ -109,10 +116,16 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu .also { it.addChangeListener(hiddenReadReceiptsListener) } } + /** + * Dispose the realm query subscription. Has to be called on an HandlerThread + */ fun dispose() { - this.hiddenReadReceipts?.removeAllChangeListeners() + this.hiddenReadReceipts.removeAllChangeListeners() } + /** + * Return the current corrected [ReadReceipt] list for an event, or null + */ fun correctedReadReceipts(eventId: String?): List? { return correctedReadReceiptsByEvent[eventId] } diff --git a/vector/src/main/res/layout/item_display_read_receipt.xml b/vector/src/main/res/layout/item_display_read_receipt.xml index cf3c9a26..9b4072ab 100644 --- a/vector/src/main/res/layout/item_display_read_receipt.xml +++ b/vector/src/main/res/layout/item_display_read_receipt.xml @@ -3,11 +3,10 @@ @@ -35,9 +35,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:lines="1" - android:textColor="?android:textColorSecondary" + android:textColor="?riotx_text_secondary" android:textSize="12sp" tools:text="10:44" /> - \ No newline at end of file diff --git a/vector/src/main/res/layout/item_simple_reaction_info.xml b/vector/src/main/res/layout/item_simple_reaction_info.xml index 0458b171..06f94fc8 100644 --- a/vector/src/main/res/layout/item_simple_reaction_info.xml +++ b/vector/src/main/res/layout/item_simple_reaction_info.xml @@ -2,11 +2,10 @@ Date: Mon, 19 Aug 2019 17:22:04 +0200 Subject: [PATCH 22/40] An error was displayed by mistake --- .../roompreview/RoomPreviewNoPreviewFragment.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt index c0da697a..b8b81006 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt @@ -18,6 +18,7 @@ package im.vector.riotx.features.roomdirectory.roompreview import android.os.Bundle import android.view.View +import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.transition.TransitionManager import com.airbnb.mvrx.args @@ -104,7 +105,12 @@ class RoomPreviewNoPreviewFragment : VectorBaseFragment() { } ) - roomPreviewNoPreviewError.setTextOrHide(errorFormatter.toHumanReadable(state.lastError)) + if (state.lastError == null) { + roomPreviewNoPreviewError.isVisible = false + } else { + roomPreviewNoPreviewError.isVisible = true + roomPreviewNoPreviewError.text = errorFormatter.toHumanReadable(state.lastError) + } if (state.roomJoinState == JoinState.JOINED) { // Quit this screen From ed5faca5d2eb10e4fb30aaab40d8ed0d42a66bfe Mon Sep 17 00:00:00 2001 From: Valere Date: Sun, 18 Aug 2019 17:46:17 -0400 Subject: [PATCH 23/40] Slide-in reply icon is distorted --- CHANGES.md | 2 +- .../room/detail/RoomMessageTouchHelperCallback.kt | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 682c176b..eae3bd53 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,7 +11,7 @@ Other changes: - Bugfix: - - + - Slide-in reply icon is distorted (#423) Translations: - diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomMessageTouchHelperCallback.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomMessageTouchHelperCallback.kt index d30bad2f..a7e617c0 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomMessageTouchHelperCallback.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomMessageTouchHelperCallback.kt @@ -191,12 +191,13 @@ class RoomMessageTouchHelperCallback(private val context: Context, } val y = (itemView.top + itemView.measuredHeight / 2).toFloat() - //magic numbers? + val hw = imageDrawable.intrinsicWidth / 2f + val hh = imageDrawable.intrinsicHeight / 2f imageDrawable.setBounds( - (x - convertToPx(12) * scale).toInt(), - (y - convertToPx(11) * scale).toInt(), - (x + convertToPx(12) * scale).toInt(), - (y + convertToPx(10) * scale).toInt() + (x - hw * scale).toInt(), + (y - hh * scale).toInt(), + (x + hw * scale).toInt(), + (y + hh * scale).toInt() ) imageDrawable.draw(canvas) imageDrawable.alpha = 255 From 7966ebef0359fb3cc26ebdf3c822cfa41e8517a9 Mon Sep 17 00:00:00 2001 From: Valere Date: Sun, 18 Aug 2019 15:12:59 -0400 Subject: [PATCH 24/40] Date change message repeats for each redaction until a normal message --- CHANGES.md | 2 +- .../detail/timeline/helper/TimelineDisplayableEvents.kt | 3 ++- .../detail/timeline/util/MessageInformationDataFactory.kt | 8 ++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 682c176b..d7533ee3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,7 +11,7 @@ Other changes: - Bugfix: - - + - Date change message repeats for each redaction until a normal message (#358) Translations: - 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 fa0a71bd..7307e067 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 @@ -56,7 +56,8 @@ fun TimelineEvent.isDisplayable(showHiddenEvent: Boolean): Boolean { return false } if (root.content.isNullOrEmpty()) { - return false + //redacted events have empty content but are displayable + return root.unsignedData?.redactedEvent != null } //Edits should be filtered out! if (EventType.MESSAGE == root.type 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 a00dd3fa..46887cad 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 @@ -50,10 +50,10 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses val showInformation = addDaySeparator - || event.senderAvatar != nextEvent?.senderAvatar - || event.getDisambiguatedDisplayName() != nextEvent?.getDisambiguatedDisplayName() - || (nextEvent.root.getClearType() != EventType.MESSAGE && nextEvent.root.getClearType() != EventType.ENCRYPTED) - || isNextMessageReceivedMoreThanOneHourAgo + || event.senderAvatar != nextEvent?.senderAvatar + || event.getDisambiguatedDisplayName() != nextEvent?.getDisambiguatedDisplayName() + || (nextEvent.root.getClearType() != EventType.MESSAGE && nextEvent.root.getClearType() != EventType.ENCRYPTED) + || isNextMessageReceivedMoreThanOneHourAgo val time = dateFormatter.formatMessageHour(date) val avatarUrl = event.senderAvatar From d4161e9a1aa1b7fa85384039f70da8d3ed56fe4b Mon Sep 17 00:00:00 2001 From: Valere Date: Sun, 18 Aug 2019 17:30:41 -0400 Subject: [PATCH 25/40] Fix text diff removed linebreak --- CHANGES.md | 2 +- .../detail/timeline/action/ViewEditHistoryEpoxyController.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 682c176b..436d4590 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,7 +11,7 @@ Other changes: - Bugfix: - - + - Fix text diff linebreak display (#441) Translations: - 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/action/ViewEditHistoryEpoxyController.kt index dab43108..0b311e7c 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/action/ViewEditHistoryEpoxyController.kt @@ -113,7 +113,7 @@ class ViewEditHistoryEpoxyController(private val context: Context, when (it.operation) { diff_match_patch.Operation.DELETE -> { span { - text = it.text + text = it.text.replace("\n"," ") textColor = ContextCompat.getColor(context, R.color.vector_error_color) textDecorationLine = "line-through" } From 2be6058971d2bd2bcd5021cd0033b7121b5dd311 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 9 Jul 2019 10:13:56 +0200 Subject: [PATCH 26/40] accept non unicode reactions --- .../home/room/detail/timeline/action/MessageMenuViewModel.kt | 3 +-- .../room/detail/timeline/action/ViewReactionViewModel.kt | 2 +- .../detail/timeline/util/MessageInformationDataFactory.kt | 2 +- vector/src/main/res/layout/reaction_button.xml | 5 +++-- 4 files changed, 6 insertions(+), 6 deletions(-) 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 index ae8803f5..d89035b8 100644 --- 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 @@ -35,7 +35,6 @@ 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.core.utils.isSingleEmoji import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData @@ -244,7 +243,7 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M //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?.any { isSingleEmoji(it.key) } ?: false + return event.annotations?.reactionsSummary?.isNotEmpty() ?: false } 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/action/ViewReactionViewModel.kt index 9479c307..221cc889 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/action/ViewReactionViewModel.kt @@ -90,7 +90,7 @@ class ViewReactionViewModel @AssistedInject constructor(@Assisted .flatMapSingle { summaries -> Observable .fromIterable(summaries.reactionsSummary) - .filter { reactionAggregatedSummary -> isSingleEmoji(reactionAggregatedSummary.key) } + //.filter { reactionAggregatedSummary -> isSingleEmoji(reactionAggregatedSummary.key) } .toReactionInfoList() } .execute { 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 46887cad..bfb0848d 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 @@ -71,7 +71,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses memberName = formattedMemberName, showInformation = showInformation, orderedReactionList = event.annotations?.reactionsSummary - ?.filter { isSingleEmoji(it.key) } + //?.filter { isSingleEmoji(it.key) } ?.map { ReactionInfoData(it.key, it.count, it.addedByMe, it.localEchoEvents.isEmpty()) }, diff --git a/vector/src/main/res/layout/reaction_button.xml b/vector/src/main/res/layout/reaction_button.xml index 3d2cd3c8..ae1ec0a6 100644 --- a/vector/src/main/res/layout/reaction_button.xml +++ b/vector/src/main/res/layout/reaction_button.xml @@ -37,8 +37,9 @@ + tools:text="* Party Parrot Again *" /> Date: Sun, 18 Aug 2019 14:26:53 -0400 Subject: [PATCH 27/40] Use EmojiCompat to build EmojiSpans from text --- CHANGES.md | 4 ++-- vector/build.gradle | 2 ++ .../java/im/vector/riotx/VectorApplication.kt | 20 +++++++++++++++++++ .../timeline/action/ReactionInfoSimpleItem.kt | 4 ---- .../action/ViewReactionBottomSheet.kt | 4 +--- .../action/ViewReactionsEpoxyController.kt | 6 +++--- .../detail/timeline/item/AbsMessageItem.kt | 2 +- .../reactions/widget/ReactionButton.kt | 13 +++++------- .../src/main/res/layout/reaction_button.xml | 10 ++++++---- 9 files changed, 40 insertions(+), 25 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 3a82647d..a3b37761 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,10 +5,10 @@ Features: - Display read receipts in timeline (#81) Improvements: - - + - Reactions: Reinstate the ability to react with non-unicode keys (#307) Other changes: - - + - Bugfix: - Fix text diff linebreak display (#441) diff --git a/vector/build.gradle b/vector/build.gradle index 2e5c1adc..f8c800d1 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -318,6 +318,8 @@ dependencies { implementation 'diff_match_patch:diff_match_patch:current' + implementation "androidx.emoji:emoji-appcompat:1.0.0" + // TESTS testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.2.0' diff --git a/vector/src/main/java/im/vector/riotx/VectorApplication.kt b/vector/src/main/java/im/vector/riotx/VectorApplication.kt index 66356ac0..4e09d790 100644 --- a/vector/src/main/java/im/vector/riotx/VectorApplication.kt +++ b/vector/src/main/java/im/vector/riotx/VectorApplication.kt @@ -19,10 +19,13 @@ package im.vector.riotx import android.app.Application import android.content.Context import android.content.res.Configuration +import android.graphics.Color import android.os.Handler import android.os.HandlerThread import androidx.core.provider.FontRequest import androidx.core.provider.FontsContractCompat +import androidx.emoji.text.EmojiCompat +import androidx.emoji.text.FontRequestEmojiCompatConfig import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.OnLifecycleEvent @@ -105,6 +108,23 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration. ) FontsContractCompat.requestFont(this, fontRequest, emojiCompatFontProvider, getFontThreadHandler()) vectorConfiguration.initConfiguration() + + //Use emoji compat for the benefit of emoji spans + val config = FontRequestEmojiCompatConfig(this, fontRequest) + .setReplaceAll(true) // we want to replace all emojis with selected font +// .setEmojiSpanIndicatorEnabled(true) +// .setEmojiSpanIndicatorColor(Color.GREEN) + EmojiCompat.init(config) + .registerInitCallback(object : EmojiCompat.InitCallback() { + override fun onInitialized() { + Timber.v("Emoji compat onInitialized success ") + } + + override fun onFailed(throwable: Throwable?) { + Timber.e(throwable,"Failed to init EmojiCompat") + } + }) + NotificationUtils.createNotificationChannels(applicationContext) if (authenticator.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) { val lastAuthenticatedSession = authenticator.getLastAuthenticatedSession()!! 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/action/ReactionInfoSimpleItem.kt index 10c2357d..e63ac29c 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/action/ReactionInfoSimpleItem.kt @@ -38,12 +38,8 @@ abstract class ReactionInfoSimpleItem : EpoxyModelWithHolder() { override fun buildModels(state: DisplayReactionsViewState) { @@ -50,9 +51,8 @@ class ViewReactionsEpoxyController(private val context: Context, private val emo state.mapReactionKeyToMemberList()?.forEach { reactionInfoSimpleItem { id(it.eventId) - emojiTypeFace(emojiCompatTypeface) timeStamp(it.timestamp) - reactionKey(it.reactionKey) + reactionKey(EmojiCompat.get().process(it.reactionKey)) authorDisplayName(it.authorName ?: it.authorId) } } 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 a394f471..cf5f2517 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 @@ -151,7 +151,7 @@ abstract class AbsMessageItem : BaseEventItem() { idToRefInFlow.add(reactionButton.id) reactionButton.reactionString = reaction.key reactionButton.reactionCount = reaction.count - reactionButton.emojiTypeFace = emojiTypeFace + //reactionButton.emojiTypeFace = emojiTypeFace reactionButton.setChecked(reaction.addedByMe) reactionButton.isEnabled = reaction.synced } diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/widget/ReactionButton.kt b/vector/src/main/java/im/vector/riotx/features/reactions/widget/ReactionButton.kt index 0af910a0..0644e962 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/widget/ReactionButton.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/widget/ReactionButton.kt @@ -35,6 +35,7 @@ import android.widget.TextView import androidx.annotation.ColorInt import androidx.annotation.ColorRes import androidx.core.content.ContextCompat +import androidx.emoji.text.EmojiCompat import im.vector.riotx.R import im.vector.riotx.core.utils.TextUtils @@ -58,12 +59,6 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut private var reactionSelector: View? = null - var emojiTypeFace: Typeface? = null - set(value) { - field = value - emojiView?.typeface = value ?: Typeface.DEFAULT - } - private var dotsView: DotsView private var circleView: CircleView var reactedListener: ReactedListener? = null @@ -82,7 +77,9 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut var reactionString = "😀" set(value) { field = value - emojiView?.text = field + //maybe cache this for performances? + val emojiSpanned = EmojiCompat.get().process(value) + emojiView?.text = emojiSpanned } private var animationScaleFactor: Float = 0.toFloat() @@ -104,7 +101,7 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut countTextView?.text = TextUtils.formatCountToShortDecimal(reactionCount) - emojiView?.typeface = this.emojiTypeFace ?: Typeface.DEFAULT +// emojiView?.typeface = this.emojiTypeFace ?: Typeface.DEFAULT val array = context.obtainStyledAttributes(attrs, R.styleable.ReactionButton, defStyleAttr, 0) diff --git a/vector/src/main/res/layout/reaction_button.xml b/vector/src/main/res/layout/reaction_button.xml index ae1ec0a6..7e715a71 100644 --- a/vector/src/main/res/layout/reaction_button.xml +++ b/vector/src/main/res/layout/reaction_button.xml @@ -40,18 +40,20 @@ android:layout_width="wrap_content" android:layout_height="20dp" android:minWidth="20dp" - android:layout_gravity="center" android:layout_marginStart="6dp" android:layout_marginLeft="6dp" android:gravity="center" android:textColor="@color/black" android:textSize="13sp" + android:maxEms="10" + android:ellipsize="middle" + android:singleLine="true" app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toStartOf="@id/reactionCount" - tools:text="* Party Parrot Again *" /> + tools:text="* Party Parrot Again * 👀" /> Date: Tue, 27 Aug 2019 16:50:02 +0200 Subject: [PATCH 28/40] Some video won't play VideoView fails to play some remote uri video on some device. For now video is downloaded locally in internal cache then played. This offers basic support before full media preview implementation --- CHANGES.md | 1 + .../features/media/VideoContentRenderer.kt | 36 ++++++++++++++++--- .../layout/activity_video_media_viewer.xml | 3 ++ 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a3b37761..2c498851 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,7 @@ Bugfix: - Fix text diff linebreak display (#441) - Date change message repeats for each redaction until a normal message (#358) - Slide-in reply icon is distorted (#423) + - Some video won't play Translations: - diff --git a/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt b/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt index 22650b0e..0509a0d4 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt @@ -89,18 +89,44 @@ class VideoContentRenderer @Inject constructor(private val activeSessionHolder: }) } } else { - thumbnailView.isVisible = false - loadingView.isVisible = false val resolvedUrl = contentUrlResolver.resolveFullSize(data.url) if (resolvedUrl == null) { + thumbnailView.isVisible = false + loadingView.isVisible = false errorView.isVisible = true errorView.setText(R.string.unknown_error) } else { - videoView.isVisible = true - videoView.setVideoPath(resolvedUrl) - videoView.start() + + //Temporary code, some remote videos are not played by videoview setVideoUri + //So for now we download them then play + thumbnailView.isVisible = true + loadingView.isVisible = true + + activeSessionHolder.getActiveSession() + .downloadFile( + FileService.DownloadMode.FOR_INTERNAL_USE, + data.eventId, + data.filename, + data.url, + null, + object : MatrixCallback { + override fun onSuccess(data: File) { + thumbnailView.isVisible = false + loadingView.isVisible = false + videoView.isVisible = true + + videoView.setVideoPath(data.path) + videoView.start() + } + + override fun onFailure(failure: Throwable) { + loadingView.isVisible = false + errorView.isVisible = true + errorView.text = errorFormatter.toHumanReadable(failure) + } + }) } } } diff --git a/vector/src/main/res/layout/activity_video_media_viewer.xml b/vector/src/main/res/layout/activity_video_media_viewer.xml index f21e6daf..c68577bc 100644 --- a/vector/src/main/res/layout/activity_video_media_viewer.xml +++ b/vector/src/main/res/layout/activity_video_media_viewer.xml @@ -34,7 +34,9 @@ android:id="@+id/videoMediaViewerThumbnailView" android:layout_width="match_parent" android:layout_height="match_parent" + android:layout_gravity="center" android:visibility="gone" + android:scaleType="centerInside" tools:visibility="visible" /> Date: Tue, 27 Aug 2019 17:01:52 +0200 Subject: [PATCH 29/40] Fix / regression on e2e reply and edit of reply --- CHANGES.md | 1 + .../session/room/relation/DefaultRelationService.kt | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a3b37761..d657337b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,7 @@ Bugfix: - Fix text diff linebreak display (#441) - Date change message repeats for each redaction until a normal message (#358) - Slide-in reply icon is distorted (#423) + - Regression / e2e replies not encrypted Translations: - 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 addad44b..9640cd4e 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 @@ -136,7 +136,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv } if (cryptoService.isRoomEncrypted(roomId)) { val encryptWork = createEncryptEventWork(event, listOf("m.relates_to")) - val workRequest = createSendEventWork(event) + val workRequest = createSendEventWork(event, false) TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, workRequest) return CancelableWork(context, encryptWork.id) @@ -162,7 +162,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv } if (cryptoService.isRoomEncrypted(roomId)) { val encryptWork = createEncryptEventWork(event, listOf("m.relates_to")) - val workRequest = createSendEventWork(event) + val workRequest = createSendEventWork(event, false) TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, workRequest) return CancelableWork(context, encryptWork.id) @@ -189,7 +189,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv if (cryptoService.isRoomEncrypted(roomId)) { val encryptWork = createEncryptEventWork(event, listOf("m.relates_to")) - val workRequest = createSendEventWork(event) + val workRequest = createSendEventWork(event, false) TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, workRequest) return CancelableWork(context, encryptWork.id) @@ -208,10 +208,10 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv return TimelineSendEventWorkCommon.createWork(sendWorkData, true) } - private fun createSendEventWork(event: Event): OneTimeWorkRequest { + private fun createSendEventWork(event: Event, startChain: Boolean = true): OneTimeWorkRequest { val sendContentWorkerParams = SendEventWorker.Params(credentials.userId, roomId, event) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) - return TimelineSendEventWorkCommon.createWork(sendWorkData, true) + return TimelineSendEventWorkCommon.createWork(sendWorkData, startChain) } override fun getEventSummaryLive(eventId: String): LiveData { From 166be4e289600be9898a47f02964391fb163165c Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 27 Aug 2019 15:36:44 +0200 Subject: [PATCH 30/40] Improve read receipt design --- .../DisplayReadReceiptsBottomSheet.kt | 2 +- .../main/res/drawable/pill_receipt_black.xml | 6 ++++ .../main/res/drawable/pill_receipt_dark.xml | 6 ++++ .../main/res/drawable/pill_receipt_light.xml | 6 ++++ .../res/layout/item_display_read_receipt.xml | 22 +++---------- .../res/layout/item_simple_reaction_info.xml | 18 ++--------- .../main/res/layout/view_read_receipts.xml | 31 ++++++++++++------- vector/src/main/res/values/attrs.xml | 1 + vector/src/main/res/values/strings_riotX.xml | 1 + vector/src/main/res/values/styles_riot.xml | 19 ++++++++++++ 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 ++ 13 files changed, 73 insertions(+), 45 deletions(-) create mode 100644 vector/src/main/res/drawable/pill_receipt_black.xml create mode 100644 vector/src/main/res/drawable/pill_receipt_dark.xml create mode 100644 vector/src/main/res/drawable/pill_receipt_light.xml 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 b8c1519f..42ad1664 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 @@ -69,7 +69,7 @@ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() { val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context, LinearLayout.VERTICAL) epoxyRecyclerView.addItemDecoration(dividerItemDecoration) - bottomSheetTitle.text = getString(R.string.read_receipts_list) + bottomSheetTitle.text = getString(R.string.read_at) epoxyController.setData(displayReadReceiptArgs.readReceipts) } diff --git a/vector/src/main/res/drawable/pill_receipt_black.xml b/vector/src/main/res/drawable/pill_receipt_black.xml new file mode 100644 index 00000000..e6390355 --- /dev/null +++ b/vector/src/main/res/drawable/pill_receipt_black.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/vector/src/main/res/drawable/pill_receipt_dark.xml b/vector/src/main/res/drawable/pill_receipt_dark.xml new file mode 100644 index 00000000..5442f139 --- /dev/null +++ b/vector/src/main/res/drawable/pill_receipt_dark.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/vector/src/main/res/drawable/pill_receipt_light.xml b/vector/src/main/res/drawable/pill_receipt_light.xml new file mode 100644 index 00000000..66875dc1 --- /dev/null +++ b/vector/src/main/res/drawable/pill_receipt_light.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_display_read_receipt.xml b/vector/src/main/res/layout/item_display_read_receipt.xml index 9b4072ab..2aa4485c 100644 --- a/vector/src/main/res/layout/item_display_read_receipt.xml +++ b/vector/src/main/res/layout/item_display_read_receipt.xml @@ -5,38 +5,26 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center_vertical" + android:minHeight="40dp" android:orientation="horizontal" android:paddingStart="8dp" android:paddingEnd="8dp"> \ No newline at end of file diff --git a/vector/src/main/res/layout/item_simple_reaction_info.xml b/vector/src/main/res/layout/item_simple_reaction_info.xml index 06f94fc8..02a36fc6 100644 --- a/vector/src/main/res/layout/item_simple_reaction_info.xml +++ b/vector/src/main/res/layout/item_simple_reaction_info.xml @@ -6,6 +6,7 @@ android:gravity="center_vertical" android:orientation="horizontal" android:paddingStart="8dp" + android:minHeight="40dp" android:paddingEnd="8dp"> diff --git a/vector/src/main/res/layout/view_read_receipts.xml b/vector/src/main/res/layout/view_read_receipts.xml index e3cbc6ba..f477a979 100644 --- a/vector/src/main/res/layout/view_read_receipts.xml +++ b/vector/src/main/res/layout/view_read_receipts.xml @@ -9,48 +9,55 @@ diff --git a/vector/src/main/res/values/attrs.xml b/vector/src/main/res/values/attrs.xml index 368c03c0..e9a4296a 100644 --- a/vector/src/main/res/values/attrs.xml +++ b/vector/src/main/res/values/attrs.xml @@ -88,6 +88,7 @@ + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 118fde84..23ea1e2f 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -2,5 +2,6 @@ + Read at \ 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 80f5148a..d1894254 100644 --- a/vector/src/main/res/values/styles_riot.xml +++ b/vector/src/main/res/values/styles_riot.xml @@ -319,4 +319,23 @@ @drawable/vector_label_background_light + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/theme_black.xml b/vector/src/main/res/values/theme_black.xml index 7b582e20..129a2e9b 100644 --- a/vector/src/main/res/values/theme_black.xml +++ b/vector/src/main/res/values/theme_black.xml @@ -75,6 +75,8 @@ #FF4D4D4D + @drawable/pill_receipt_black + @color/riot_primary_background_color_black @color/primary_color_black diff --git a/vector/src/main/res/values/theme_dark.xml b/vector/src/main/res/values/theme_dark.xml index b5796eb9..41432292 100644 --- a/vector/src/main/res/values/theme_dark.xml +++ b/vector/src/main/res/values/theme_dark.xml @@ -163,6 +163,8 @@ @android:color/white @color/riot_primary_text_color_dark + @drawable/pill_receipt_dark + @drawable/direct_chat_circle_dark #FF454545 diff --git a/vector/src/main/res/values/theme_light.xml b/vector/src/main/res/values/theme_light.xml index 56f2b90b..8d51486e 100644 --- a/vector/src/main/res/values/theme_light.xml +++ b/vector/src/main/res/values/theme_light.xml @@ -163,6 +163,8 @@ @color/riot_primary_text_color_light @android:color/white + @drawable/pill_receipt_light + @drawable/direct_chat_circle_light #FFD3EFE1 From 1b394527b6a0a7cec012e3293db5b1b3253bc937 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 28 Aug 2019 10:22:51 +0200 Subject: [PATCH 31/40] cleaning + code review --- .../room/relation/DefaultRelationService.kt | 44 +++++++++---------- 1 file changed, 20 insertions(+), 24 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 9640cd4e..0c8695cb 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 @@ -69,15 +69,11 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv .also { saveLocalEcho(it) } - val sendRelationWork = createSendRelationWork(event) + val sendRelationWork = createSendEventWork(event, true) TimelineSendEventWorkCommon.postWork(context, roomId, sendRelationWork) return CancelableWork(context, sendRelationWork.id) } - private fun createSendRelationWork(event: Event): OneTimeWorkRequest { - return createSendEventWork(event) - } - override fun undoReaction(reaction: String, targetEventId: String, myUserId: String)/*: Cancelable*/ { val params = FindReactionEventForUndoTask.Params( @@ -134,42 +130,42 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv .also { saveLocalEcho(it) } - if (cryptoService.isRoomEncrypted(roomId)) { + return if (cryptoService.isRoomEncrypted(roomId)) { val encryptWork = createEncryptEventWork(event, listOf("m.relates_to")) val workRequest = createSendEventWork(event, false) TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, workRequest) - return CancelableWork(context, encryptWork.id) + CancelableWork(context, encryptWork.id) } else { - val workRequest = createSendEventWork(event) + val workRequest = createSendEventWork(event, true) TimelineSendEventWorkCommon.postWork(context, roomId, workRequest) - return CancelableWork(context, workRequest.id) + CancelableWork(context, workRequest.id) } } override fun editReply(replyToEdit: TimelineEvent, - originalEvent: TimelineEvent, + originalTimelineEvent: TimelineEvent, newBodyText: String, compatibilityBodyText: String): Cancelable { val event = eventFactory .createReplaceTextOfReply(roomId, - replyToEdit, - originalEvent, - newBodyText, true, MessageType.MSGTYPE_TEXT, compatibilityBodyText) + replyToEdit, + originalTimelineEvent, + newBodyText, true, MessageType.MSGTYPE_TEXT, compatibilityBodyText) .also { saveLocalEcho(it) } - if (cryptoService.isRoomEncrypted(roomId)) { + return if (cryptoService.isRoomEncrypted(roomId)) { val encryptWork = createEncryptEventWork(event, listOf("m.relates_to")) val workRequest = createSendEventWork(event, false) TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, workRequest) - return CancelableWork(context, encryptWork.id) + CancelableWork(context, encryptWork.id) } else { - val workRequest = createSendEventWork(event) + val workRequest = createSendEventWork(event, true) TimelineSendEventWorkCommon.postWork(context, roomId, workRequest) - return CancelableWork(context, workRequest.id) + CancelableWork(context, workRequest.id) } } @@ -187,16 +183,16 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv saveLocalEcho(it) } ?: return null - if (cryptoService.isRoomEncrypted(roomId)) { + return if (cryptoService.isRoomEncrypted(roomId)) { val encryptWork = createEncryptEventWork(event, listOf("m.relates_to")) val workRequest = createSendEventWork(event, false) TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, workRequest) - return CancelableWork(context, encryptWork.id) + CancelableWork(context, encryptWork.id) } else { - val workRequest = createSendEventWork(event) + val workRequest = createSendEventWork(event, true) TimelineSendEventWorkCommon.postWork(context, roomId, workRequest) - return CancelableWork(context, workRequest.id) + CancelableWork(context, workRequest.id) } } @@ -208,7 +204,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv return TimelineSendEventWorkCommon.createWork(sendWorkData, true) } - private fun createSendEventWork(event: Event, startChain: Boolean = true): OneTimeWorkRequest { + private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { val sendContentWorkerParams = SendEventWorker.Params(credentials.userId, roomId, event) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) return TimelineSendEventWorkCommon.createWork(sendWorkData, startChain) @@ -220,7 +216,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv } return Transformations.map(liveEntity) { realmResults -> realmResults.firstOrNull()?.asDomain() - ?: EventAnnotationsSummary(eventId, emptyList(), null) + ?: EventAnnotationsSummary(eventId, emptyList(), null) } } @@ -233,7 +229,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv private fun saveLocalEcho(event: Event) { monarchy.writeAsync { realm -> val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() - ?: return@writeAsync + ?: return@writeAsync roomEntity.addSendingEvent(event) } } From ea242f6737a819a665661391605ef47d3d2c14ab Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 28 Aug 2019 17:17:37 +0200 Subject: [PATCH 32/40] Hide ReadReceipt View when it is not relevant --- .../java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt | 4 ---- .../features/home/room/detail/timeline/item/AbsMessageItem.kt | 2 -- .../features/home/room/detail/timeline/item/BaseEventItem.kt | 2 ++ .../features/home/room/detail/timeline/item/DefaultItem.kt | 4 ++++ .../home/room/detail/timeline/item/MergedHeaderItem.kt | 4 ++++ .../features/home/room/detail/timeline/item/NoticeItem.kt | 2 -- vector/src/main/res/layout/item_timeline_event_base.xml | 4 +++- .../src/main/res/layout/item_timeline_event_base_noinfo.xml | 4 +++- 8 files changed, 16 insertions(+), 10 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt index 44d1ee6f..15a54248 100644 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt @@ -21,11 +21,8 @@ import android.util.AttributeSet import android.view.View import android.widget.ImageView import android.widget.LinearLayout -import androidx.core.view.isInvisible import androidx.core.view.isVisible -import butterknife.ButterKnife import im.vector.riotx.R -import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData import kotlinx.android.synthetic.main.view_read_receipts.view.* @@ -48,7 +45,6 @@ class ReadReceiptsView @JvmOverloads constructor( private fun setupView() { inflate(context, R.layout.view_read_receipts, this) - ButterKnife.bind(this) } fun render(readReceipts: List, avatarRenderer: AvatarRenderer, clickListener: OnClickListener) { 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 cf5f2517..2a4142e4 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,7 +32,6 @@ 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.ReadReceiptsView import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.core.utils.DimensionUtils.dpToPx import im.vector.riotx.features.home.AvatarRenderer @@ -181,7 +180,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 readReceiptsView by bind(R.id.readReceiptsView) 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 843f52b3..96625d16 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.ReadReceiptsView import im.vector.riotx.core.utils.DimensionUtils.dpToPx /** @@ -49,6 +50,7 @@ abstract class BaseEventItem : VectorEpoxyModel abstract class BaseHolder(@IdRes val stubId: Int) : VectorEpoxyHolder() { val leftGuideline by bind(R.id.messageStartGuideline) val checkableBackground by bind(R.id.messageSelectedBackground) + val readReceiptsView by bind(R.id.readReceiptsView) override fun bindView(itemView: View) { super.bindView(itemView) 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 0b30facf..e43c0a95 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 @@ -17,6 +17,7 @@ package im.vector.riotx.features.home.room.detail.timeline.item import android.widget.TextView +import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R @@ -29,6 +30,9 @@ abstract class DefaultItem : BaseEventItem() { override fun bind(holder: Holder) { holder.messageView.text = text + + // TODO We should handle read receipt here as well + holder.readReceiptsView.isVisible = false } override fun getViewType() = 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 4f26f9bb..03dd5a8a 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,6 +21,7 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.core.view.children +import androidx.core.view.isVisible import im.vector.riotx.R import im.vector.riotx.features.home.AvatarRenderer @@ -75,6 +76,9 @@ data class MergedHeaderItem(private val isCollapsed: Boolean, holder.separatorView.visibility = View.VISIBLE holder.expandView.setText(R.string.merged_events_collapse) } + + // No read receipt for this item + holder.readReceiptsView.isVisible = false } 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 51a7b0ce..29850497 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 @@ -22,7 +22,6 @@ import android.widget.TextView import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R -import im.vector.riotx.core.ui.views.ReadReceiptsView import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController @@ -72,7 +71,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 readReceiptsView by bind(R.id.readReceiptsView) } companion object { 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 2f0be78f..8e42804c 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -129,8 +129,10 @@ android:layout_height="wrap_content" android:layout_marginEnd="8dp" android:layout_marginBottom="4dp" + android:visibility="gone" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" /> + app:layout_constraintEnd_toEndOf="parent" + tools:visibility="visible" /> \ 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 77268399..3f82c8db 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,8 +58,10 @@ android:layout_height="wrap_content" android:layout_marginEnd="8dp" android:layout_marginBottom="4dp" + android:visibility="gone" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" /> + app:layout_constraintEnd_toEndOf="parent" + tools:visibility="visible" /> \ No newline at end of file From ef0362ba9cbf8eb41be177b6b7676343c370f80a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 28 Aug 2019 17:31:31 +0200 Subject: [PATCH 33/40] Display Read Receipt on unsupported events --- .../timeline/factory/DefaultItemFactory.kt | 17 +++++++++++++-- .../timeline/factory/TimelineItemFactory.kt | 9 ++------ .../room/detail/timeline/item/DefaultItem.kt | 21 ++++++++++++++++--- 3 files changed, 35 insertions(+), 12 deletions(-) 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 05e4007e..9adbfb8a 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,21 +17,34 @@ 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.item.DefaultItem import im.vector.riotx.features.home.room.detail.timeline.item.DefaultItem_ +import im.vector.riotx.features.home.room.detail.timeline.util.MessageInformationDataFactory import javax.inject.Inject -class DefaultItemFactory @Inject constructor(){ +class DefaultItemFactory @Inject constructor(private val avatarRenderer: AvatarRenderer, + private val informationDataFactory: MessageInformationDataFactory) { - fun create(event: TimelineEvent, highlight: Boolean, exception: Exception? = null): DefaultItem? { + fun create(event: TimelineEvent, + highlight: Boolean, + callback: TimelineEventController.Callback?, + exception: Exception? = null): DefaultItem? { val text = if (exception == null) { "${event.root.getClearType()} events are not yet handled" } else { "an exception occurred when rendering the event ${event.root.eventId}" } + + val informationData = informationDataFactory.create(event, null) + return DefaultItem_() .text(text) + .avatarRenderer(avatarRenderer) .highlighted(highlight) + .informationData(informationData) + .readReceiptsCallback(callback) } } \ No newline at end of file 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 b1ae595e..e9ce37f2 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,12 +20,7 @@ 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 @@ -71,7 +66,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me // Unhandled event types (yet) EventType.STATE_ROOM_THIRD_PARTY_INVITE, - EventType.STICKER -> defaultItemFactory.create(event, highlight) + EventType.STICKER -> defaultItemFactory.create(event, highlight, callback) else -> { Timber.v("Type ${event.root.getClearType()} not handled") null @@ -79,7 +74,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me } } catch (e: Exception) { Timber.e(e, "failed to create message item") - defaultItemFactory.create(event, highlight, e) + defaultItemFactory.create(event, highlight, callback, e) } return (computedModel ?: EmptyItem_()) } 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 e43c0a95..ea1aa064 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 @@ -16,23 +16,38 @@ package im.vector.riotx.features.home.room.detail.timeline.item +import android.view.View 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.utils.DebouncedClickListener +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 DefaultItem : BaseEventItem() { + @EpoxyAttribute + lateinit var informationData: MessageInformationData + + @EpoxyAttribute + lateinit var avatarRenderer: AvatarRenderer + + @EpoxyAttribute + var readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null + + private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener { + readReceiptsCallback?.onReadReceiptsClicked(informationData.readReceipts) + }) + @EpoxyAttribute var text: CharSequence? = null override fun bind(holder: Holder) { holder.messageView.text = text - // TODO We should handle read receipt here as well - holder.readReceiptsView.isVisible = false + holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener) } override fun getViewType() = STUB_ID From c95223f5d24188363a4529d883ddf83ec88c72cf Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 28 Aug 2019 18:17:37 +0200 Subject: [PATCH 34/40] Add long click support on unsupported event --- .../room/detail/timeline/factory/DefaultItemFactory.kt | 1 + .../home/room/detail/timeline/item/DefaultItem.kt | 8 ++++++++ 2 files changed, 9 insertions(+) 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 9adbfb8a..ec7d9e16 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 @@ -44,6 +44,7 @@ class DefaultItemFactory @Inject constructor(private val avatarRenderer: AvatarR .avatarRenderer(avatarRenderer) .highlighted(highlight) .informationData(informationData) + .baseCallback(callback) .readReceiptsCallback(callback) } 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 ea1aa064..b4919494 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 @@ -34,6 +34,13 @@ abstract class DefaultItem : BaseEventItem() { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer + @EpoxyAttribute + var baseCallback: TimelineEventController.BaseCallback? = null + + private var longClickListener = View.OnLongClickListener { + return@OnLongClickListener baseCallback?.onEventLongClicked(informationData, null, it) == true + } + @EpoxyAttribute var readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null @@ -47,6 +54,7 @@ abstract class DefaultItem : BaseEventItem() { override fun bind(holder: Holder) { holder.messageView.text = text + holder.view.setOnLongClickListener(longClickListener) holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener) } From ce5570105d8770904dfe02aadd8fd7840e190714 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 29 Aug 2019 10:36:45 +0200 Subject: [PATCH 35/40] Privacy: remove log of notifiable event (#519) --- CHANGES.md | 1 + .../riotx/features/notifications/PushRuleTriggerListener.kt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index dd9cc737..415cdff4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,6 +16,7 @@ Bugfix: - Slide-in reply icon is distorted (#423) - Regression / e2e replies not encrypted - Some video won't play + - Privacy: remove log of notifiable event (#519) Translations: - diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/PushRuleTriggerListener.kt b/vector/src/main/java/im/vector/riotx/features/notifications/PushRuleTriggerListener.kt index d404b64c..555c8737 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/PushRuleTriggerListener.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/PushRuleTriggerListener.kt @@ -48,7 +48,7 @@ class PushRuleTriggerListener @Inject constructor( //TODO } else { notifiableEvent.noisy = !notificationAction.soundName.isNullOrBlank() - Timber.v("New event to notify $notifiableEvent tweaks:$notificationAction") + Timber.v("New event to notify") notificationDrawerManager.onNotifiableEventReceived(notifiableEvent) } } else { From 75266f42bbfce9da67378b035d22afc24e885f8a Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 29 Aug 2019 16:49:22 +0200 Subject: [PATCH 36/40] Fix / EmojiCompat not initialized --- .../java/im/vector/riotx/EmojiCompatHelper.kt | 68 +++++++++++++++++++ .../java/im/vector/riotx/VectorApplication.kt | 16 +---- .../action/ViewReactionsEpoxyController.kt | 3 +- .../reactions/widget/ReactionButton.kt | 5 +- 4 files changed, 73 insertions(+), 19 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/EmojiCompatHelper.kt diff --git a/vector/src/main/java/im/vector/riotx/EmojiCompatHelper.kt b/vector/src/main/java/im/vector/riotx/EmojiCompatHelper.kt new file mode 100644 index 00000000..9e4a6087 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/EmojiCompatHelper.kt @@ -0,0 +1,68 @@ +/* + * 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 android.content.Context +import androidx.core.provider.FontRequest +import androidx.emoji.text.EmojiCompat +import androidx.emoji.text.FontRequestEmojiCompatConfig +import timber.log.Timber + +object EmojiCompatHelper { + + var initialized = false + + fun init(context: Context) { + val fontRequest = FontRequest( + "com.google.android.gms.fonts", + "com.google.android.gms", + "Noto Color Emoji Compat", + R.array.com_google_android_gms_fonts_certs + ) + //Use emoji compat for the benefit of emoji spans + val config = FontRequestEmojiCompatConfig(context, fontRequest) + // we want to replace all emojis with selected font + .setReplaceAll(true) + //Debug options +// .setEmojiSpanIndicatorEnabled(true) +// .setEmojiSpanIndicatorColor(Color.GREEN) + EmojiCompat.init(config) + .registerInitCallback(object : EmojiCompat.InitCallback() { + override fun onInitialized() { + Timber.v("Emoji compat onInitialized success ") + initialized = true + } + + override fun onFailed(throwable: Throwable?) { + Timber.e(throwable, "Failed to init EmojiCompat") + } + }) + } + + fun safeEmojiSpanify(sequence: CharSequence): CharSequence { + if (initialized) { + try { + return EmojiCompat.get().process(sequence) + } catch (throwable: Throwable) { + //Defensive coding against error (should not happend as it is initialized) + Timber.e(throwable, "Failed to init EmojiCompat") + return sequence + } + } else { + return sequence + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/VectorApplication.kt b/vector/src/main/java/im/vector/riotx/VectorApplication.kt index 4e09d790..36b4d589 100644 --- a/vector/src/main/java/im/vector/riotx/VectorApplication.kt +++ b/vector/src/main/java/im/vector/riotx/VectorApplication.kt @@ -109,21 +109,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration. FontsContractCompat.requestFont(this, fontRequest, emojiCompatFontProvider, getFontThreadHandler()) vectorConfiguration.initConfiguration() - //Use emoji compat for the benefit of emoji spans - val config = FontRequestEmojiCompatConfig(this, fontRequest) - .setReplaceAll(true) // we want to replace all emojis with selected font -// .setEmojiSpanIndicatorEnabled(true) -// .setEmojiSpanIndicatorColor(Color.GREEN) - EmojiCompat.init(config) - .registerInitCallback(object : EmojiCompat.InitCallback() { - override fun onInitialized() { - Timber.v("Emoji compat onInitialized success ") - } - - override fun onFailed(throwable: Throwable?) { - Timber.e(throwable,"Failed to init EmojiCompat") - } - }) + EmojiCompatHelper.init(this) NotificationUtils.createNotificationChannels(applicationContext) if (authenticator.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) { 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/action/ViewReactionsEpoxyController.kt index 33e1d4df..3c2ff93e 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/action/ViewReactionsEpoxyController.kt @@ -24,6 +24,7 @@ import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Incomplete import com.airbnb.mvrx.Success +import im.vector.riotx.EmojiCompatHelper import im.vector.riotx.R import im.vector.riotx.core.ui.list.genericFooterItem import im.vector.riotx.core.ui.list.genericLoaderItem @@ -52,7 +53,7 @@ class ViewReactionsEpoxyController(private val context: Context) reactionInfoSimpleItem { id(it.eventId) timeStamp(it.timestamp) - reactionKey(EmojiCompat.get().process(it.reactionKey)) + reactionKey(EmojiCompatHelper.safeEmojiSpanify(it.reactionKey)) authorDisplayName(it.authorName ?: it.authorId) } } diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/widget/ReactionButton.kt b/vector/src/main/java/im/vector/riotx/features/reactions/widget/ReactionButton.kt index 0644e962..c9bfa2df 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/widget/ReactionButton.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/widget/ReactionButton.kt @@ -21,7 +21,6 @@ import android.animation.AnimatorSet import android.animation.ObjectAnimator import android.content.Context import android.content.res.TypedArray -import android.graphics.Typeface import android.graphics.drawable.Drawable import android.util.AttributeSet import android.view.LayoutInflater @@ -35,7 +34,7 @@ import android.widget.TextView import androidx.annotation.ColorInt import androidx.annotation.ColorRes import androidx.core.content.ContextCompat -import androidx.emoji.text.EmojiCompat +import im.vector.riotx.EmojiCompatHelper import im.vector.riotx.R import im.vector.riotx.core.utils.TextUtils @@ -78,7 +77,7 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut set(value) { field = value //maybe cache this for performances? - val emojiSpanned = EmojiCompat.get().process(value) + val emojiSpanned = EmojiCompatHelper.safeEmojiSpanify(value) emojiView?.text = emojiSpanned } From 35817245cb80ecaf748d68adcc28efe42d60a830 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 29 Aug 2019 17:27:49 +0200 Subject: [PATCH 37/40] refactoring, code review --- ...ojiCompatHelper.kt => EmojiCompatWrapper.kt} | 17 ++++++++--------- .../java/im/vector/riotx/VectorApplication.kt | 6 ++---- .../im/vector/riotx/core/di/ScreenComponent.kt | 3 +++ .../im/vector/riotx/core/di/VectorComponent.kt | 3 +++ .../timeline/action/ViewReactionBottomSheet.kt | 4 +--- .../action/ViewReactionsEpoxyController.kt | 15 ++++++++------- .../features/reactions/widget/ReactionButton.kt | 14 ++++++++++++-- 7 files changed, 37 insertions(+), 25 deletions(-) rename vector/src/main/java/im/vector/riotx/{EmojiCompatHelper.kt => EmojiCompatWrapper.kt} (86%) diff --git a/vector/src/main/java/im/vector/riotx/EmojiCompatHelper.kt b/vector/src/main/java/im/vector/riotx/EmojiCompatWrapper.kt similarity index 86% rename from vector/src/main/java/im/vector/riotx/EmojiCompatHelper.kt rename to vector/src/main/java/im/vector/riotx/EmojiCompatWrapper.kt index 9e4a6087..b1832840 100644 --- a/vector/src/main/java/im/vector/riotx/EmojiCompatHelper.kt +++ b/vector/src/main/java/im/vector/riotx/EmojiCompatWrapper.kt @@ -16,22 +16,21 @@ package im.vector.riotx import android.content.Context +import androidx.appcompat.app.AppCompatActivity import androidx.core.provider.FontRequest import androidx.emoji.text.EmojiCompat import androidx.emoji.text.FontRequestEmojiCompatConfig import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton -object EmojiCompatHelper { +@Singleton +class EmojiCompatWrapper @Inject constructor(private val context: Context) { - var initialized = false + private var initialized = false + + fun init(fontRequest: FontRequest) { - fun init(context: Context) { - val fontRequest = FontRequest( - "com.google.android.gms.fonts", - "com.google.android.gms", - "Noto Color Emoji Compat", - R.array.com_google_android_gms_fonts_certs - ) //Use emoji compat for the benefit of emoji spans val config = FontRequestEmojiCompatConfig(context, fontRequest) // we want to replace all emojis with selected font diff --git a/vector/src/main/java/im/vector/riotx/VectorApplication.kt b/vector/src/main/java/im/vector/riotx/VectorApplication.kt index 36b4d589..b62c44f5 100644 --- a/vector/src/main/java/im/vector/riotx/VectorApplication.kt +++ b/vector/src/main/java/im/vector/riotx/VectorApplication.kt @@ -19,13 +19,10 @@ package im.vector.riotx import android.app.Application import android.content.Context import android.content.res.Configuration -import android.graphics.Color import android.os.Handler import android.os.HandlerThread import androidx.core.provider.FontRequest import androidx.core.provider.FontsContractCompat -import androidx.emoji.text.EmojiCompat -import androidx.emoji.text.FontRequestEmojiCompatConfig import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.OnLifecycleEvent @@ -69,6 +66,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration. @Inject lateinit var authenticator: Authenticator @Inject lateinit var vectorConfiguration: VectorConfiguration @Inject lateinit var emojiCompatFontProvider: EmojiCompatFontProvider + @Inject lateinit var emojiCompatWrapper: EmojiCompatWrapper @Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler @Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var notificationDrawerManager: NotificationDrawerManager @@ -109,7 +107,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration. FontsContractCompat.requestFont(this, fontRequest, emojiCompatFontProvider, getFontThreadHandler()) vectorConfiguration.initConfiguration() - EmojiCompatHelper.init(this) + emojiCompatWrapper.init(fontRequest) NotificationUtils.createNotificationChannels(applicationContext) if (authenticator.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) { 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 ffde2bc2..6bfbddba 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 @@ -58,6 +58,7 @@ import im.vector.riotx.features.rageshake.BugReportActivity import im.vector.riotx.features.rageshake.BugReporter import im.vector.riotx.features.rageshake.RageShake import im.vector.riotx.features.reactions.EmojiReactionPickerActivity +import im.vector.riotx.features.reactions.widget.ReactionButton import im.vector.riotx.features.roomdirectory.PublicRoomsFragment import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity @@ -181,6 +182,8 @@ interface ScreenComponent { fun inject(displayReadReceiptsBottomSheet: DisplayReadReceiptsBottomSheet) + fun inject(reactionButton: ReactionButton) + @Component.Factory interface Factory { fun create(vectorComponent: VectorComponent, 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 d1b87f0b..7cbbc306 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 @@ -24,6 +24,7 @@ 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.EmojiCompatFontProvider +import im.vector.riotx.EmojiCompatWrapper import im.vector.riotx.VectorApplication import im.vector.riotx.core.pushers.PushersManager import im.vector.riotx.features.configuration.VectorConfiguration @@ -70,6 +71,8 @@ interface VectorComponent { fun emojiCompatFontProvider(): EmojiCompatFontProvider + fun emojiCompatWrapper() : EmojiCompatWrapper + fun eventHtmlRenderer(): EventHtmlRenderer fun navigator(): Navigator 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/action/ViewReactionBottomSheet.kt index 9d21c89e..83d61782 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/action/ViewReactionBottomSheet.kt @@ -46,9 +46,7 @@ class ViewReactionBottomSheet : VectorBaseBottomSheetDialogFragment() { @BindView(R.id.bottom_sheet_display_reactions_list) lateinit var epoxyRecyclerView: EpoxyRecyclerView - private val epoxyController by lazy { - ViewReactionsEpoxyController(requireContext()) - } + @Inject lateinit var epoxyController: ViewReactionsEpoxyController override fun injectWith(screenComponent: ScreenComponent) { screenComponent.inject(this) 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/action/ViewReactionsEpoxyController.kt index 3c2ff93e..904a2395 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/action/ViewReactionsEpoxyController.kt @@ -17,22 +17,23 @@ package im.vector.riotx.features.home.room.detail.timeline.action import android.content.Context -import android.graphics.Typeface -import android.text.format.DateUtils -import androidx.emoji.text.EmojiCompat import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Incomplete import com.airbnb.mvrx.Success -import im.vector.riotx.EmojiCompatHelper +import im.vector.riotx.EmojiCompatWrapper import im.vector.riotx.R +import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.ui.list.genericFooterItem import im.vector.riotx.core.ui.list.genericLoaderItem +import javax.inject.Inject /** * Epoxy controller for reaction event list */ -class ViewReactionsEpoxyController(private val context: Context) +class ViewReactionsEpoxyController @Inject constructor( + private val stringProvider: StringProvider, + private val emojiCompatWrapper: EmojiCompatWrapper ) : TypedEpoxyController() { override fun buildModels(state: DisplayReactionsViewState) { @@ -45,7 +46,7 @@ class ViewReactionsEpoxyController(private val context: Context) is Fail -> { genericFooterItem { id("failure") - text(context.getString(R.string.unknown_error)) + text(stringProvider.getString(R.string.unknown_error)) } } is Success -> { @@ -53,7 +54,7 @@ class ViewReactionsEpoxyController(private val context: Context) reactionInfoSimpleItem { id(it.eventId) timeStamp(it.timestamp) - reactionKey(EmojiCompatHelper.safeEmojiSpanify(it.reactionKey)) + reactionKey(emojiCompatWrapper.safeEmojiSpanify(it.reactionKey)) authorDisplayName(it.authorName ?: it.authorId) } } diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/widget/ReactionButton.kt b/vector/src/main/java/im/vector/riotx/features/reactions/widget/ReactionButton.kt index c9bfa2df..483165ab 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/widget/ReactionButton.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/widget/ReactionButton.kt @@ -34,9 +34,11 @@ import android.widget.TextView import androidx.annotation.ColorInt import androidx.annotation.ColorRes import androidx.core.content.ContextCompat -import im.vector.riotx.EmojiCompatHelper +import im.vector.riotx.EmojiCompatWrapper import im.vector.riotx.R +import im.vector.riotx.core.di.HasScreenInjector import im.vector.riotx.core.utils.TextUtils +import javax.inject.Inject /** * An animated reaction button. @@ -46,6 +48,12 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut defStyleAttr: Int = 0) : FrameLayout(context, attrs, defStyleAttr), View.OnClickListener, View.OnLongClickListener { + init { + if (context is HasScreenInjector) { + context.injector().inject(this) + } + } + companion object { private val DECCELERATE_INTERPOLATOR = DecelerateInterpolator() private val ACCELERATE_DECELERATE_INTERPOLATOR = AccelerateDecelerateInterpolator() @@ -53,6 +61,8 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut } + @Inject lateinit var emojiCompatWrapper: EmojiCompatWrapper + private var emojiView: TextView? = null private var countTextView: TextView? = null @@ -77,7 +87,7 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut set(value) { field = value //maybe cache this for performances? - val emojiSpanned = EmojiCompatHelper.safeEmojiSpanify(value) + val emojiSpanned = emojiCompatWrapper.safeEmojiSpanify(value) emojiView?.text = emojiSpanned } From 47d22a3d5e761277bf72274e91a7ec2918665cdf Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 30 Aug 2019 11:21:26 +0200 Subject: [PATCH 38/40] Import translation from Riot and MatrixSDK --- .../src/main/res/values-bg/strings.xml | 5 + .../src/main/res/values-de/strings.xml | 61 + .../src/main/res/values-eu/strings.xml | 5 + .../src/main/res/values-fi/strings.xml | 5 + .../src/main/res/values-fr/strings.xml | 5 + .../src/main/res/values-hu/strings.xml | 5 + .../src/main/res/values-it/strings.xml | 5 + .../src/main/res/values-ko/strings.xml | 169 +- .../src/main/res/values-nl/strings.xml | 5 + .../src/main/res/values-pl/strings.xml | 41 +- .../src/main/res/values-pt/strings.xml | 6 + .../src/main/res/values-ru/strings.xml | 9 +- .../src/main/res/values-sk/strings.xml | 91 ++ .../src/main/res/values-sq/strings.xml | 22 + .../src/main/res/values-vls/strings.xml | 23 +- .../src/main/res/values-zh-rCN/strings.xml | 5 + .../src/main/res/values-zh-rTW/strings.xml | 5 + vector/src/main/res/values-bg/strings.xml | 15 +- vector/src/main/res/values-ca/strings.xml | 213 ++- vector/src/main/res/values-de/strings.xml | 111 +- vector/src/main/res/values-eu/strings.xml | 15 +- vector/src/main/res/values-fi/strings.xml | 81 +- vector/src/main/res/values-fr/strings.xml | 15 +- vector/src/main/res/values-hu/strings.xml | 15 +- vector/src/main/res/values-it/strings.xml | 15 +- vector/src/main/res/values-ko/strings.xml | 1423 ++++++++++++++++- vector/src/main/res/values-nl/strings.xml | 14 +- vector/src/main/res/values-pl/strings.xml | 30 +- vector/src/main/res/values-ru/strings.xml | 137 +- vector/src/main/res/values-sq/strings.xml | 49 +- vector/src/main/res/values-vls/strings.xml | 9 + vector/src/main/res/values-zh-rTW/strings.xml | 13 + vector/src/main/res/values/strings.xml | 13 + vector/src/main/res/values/strings_riotX.xml | 1 - 34 files changed, 2542 insertions(+), 94 deletions(-) diff --git a/matrix-sdk-android/src/main/res/values-bg/strings.xml b/matrix-sdk-android/src/main/res/values-bg/strings.xml index 743f38e6..85643c7a 100644 --- a/matrix-sdk-android/src/main/res/values-bg/strings.xml +++ b/matrix-sdk-android/src/main/res/values-bg/strings.xml @@ -167,4 +167,9 @@ Начална синхронизация: \nИмпортиране на данни за профила + %s обнови тази стая. + + Изпращане на съобщение… + Изчисти опашката за изпращане + 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 3c1b61b3..3f37ec35 100644 --- a/matrix-sdk-android/src/main/res/values-de/strings.xml +++ b/matrix-sdk-android/src/main/res/values-de/strings.xml @@ -105,4 +105,65 @@ Schwein Elefant Hase + %s hat diesen Raum aufgewertet. + + Panda + Hahn + Pinguin + Schildkröte + Fisch + Tintenfisch + Schmetterling + Blume + Baum + Kaktus + Pilz + Globus + Mond + Wolke + Feuer + Banane + Apfel + Erdbeere + Mais + Kuchen + Herz + Lächeln + Roboter + Hut + Brille + Schraubenschlüssel + Nikolaus + Daumen hoch + Regenschirm + Sanduhr + Uhr + Geschenk + Glühbirne + Buch + Stift + Büroklammer + Scheren + sperren + Schlüssel + Hammer + Telefon + Flagge + Zug + Fahrrad + Flugzeug + Rakete + Pokal + Ball + Gitarre + Trompete + Glocke + Anker + Kopfhörer + Ordner + Stecknadel + + Sende eine Nachricht… + Sendewarteschlange leeren + 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 5339cf01..25e0df10 100644 --- a/matrix-sdk-android/src/main/res/values-eu/strings.xml +++ b/matrix-sdk-android/src/main/res/values-eu/strings.xml @@ -167,4 +167,9 @@ Hasierako sinkronizazioa: \nKontuaren datuak inportatzen + %s erabiltzaileak gela hau eguneratu du. + + Mezua bidaltzen… + Garbitu bidalketa-ilara + 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 2f3e73c7..1cfc4993 100644 --- a/matrix-sdk-android/src/main/res/values-fi/strings.xml +++ b/matrix-sdk-android/src/main/res/values-fi/strings.xml @@ -168,4 +168,9 @@ Alkusynkronointi: \nTuodaan tilin tietoja + %s päivitti tämän huoneen. + + Lähetetään viestiä… + Tyhjennä lähetysjono + 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 5d211e01..9541555d 100644 --- a/matrix-sdk-android/src/main/res/values-fr/strings.xml +++ b/matrix-sdk-android/src/main/res/values-fr/strings.xml @@ -167,4 +167,9 @@ Synchronisation initiale : \nImportation des données du compte + %s a mis à niveau ce salon. + + Envoi du message… + Vider la file d’envoi + 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 b183a1b4..e3d4e88c 100644 --- a/matrix-sdk-android/src/main/res/values-hu/strings.xml +++ b/matrix-sdk-android/src/main/res/values-hu/strings.xml @@ -166,4 +166,9 @@ Induló szinkronizáció: \nFiók adatok betöltése + %s frissítette ezt a szobát. + + Üzenet küldése… + Küldő sor ürítése + 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 3f067c2a..1edc0381 100644 --- a/matrix-sdk-android/src/main/res/values-it/strings.xml +++ b/matrix-sdk-android/src/main/res/values-it/strings.xml @@ -167,4 +167,9 @@ Sync iniziale: \nImportazione dati account + %s ha aggiornato questa stanza. + + Invio messaggio in corso … + Cancella la coda di invio + 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 faf1840a..30145e31 100644 --- a/matrix-sdk-android/src/main/res/values-ko/strings.xml +++ b/matrix-sdk-android/src/main/res/values-ko/strings.xml @@ -1,6 +1,173 @@ %1$s: %2$s - %s\'의 초대 + %s님의 초대 헤드폰 + %1$s님이 사진을 보냈습니다. + %1$s님이 스티커를 보냈습니다. + + %1$s님이 %2$s님을 초대했습니다 + %1$s님이 당신을 초대했습니다 + %1$s님이 참가했습니다 + %1$s님이 떠났습니다 + %1$s님이 초대를 거부했습니다 + %1$s님이 %2$s님을 추방했습니다 + %1$s님이 %2$s님의 차단을 풀었습니다 + %1$s님이 %2$s님을 차단했습니다 + %1$s님이 %2$s님의 초대를 취소했습니다 + %1$s님이 아바타를 변경했습니다 + %1$s님이 표시 이름을 %2$s(으)로 설정했습니다 + %1$s님이 표시 이름을 %2$s에서 %3$s(으)로 변경했습니다 + %1$s님이 표시 이름을 삭제했습니다 (%2$s) + %1$s님이 주제를 다음으로 변경했습니다: %2$s + %1$s님이 방 이름을 다음으로 변경했습니다: %2$s + %s님이 영상 통화를 걸었습니다. + %s님이 음성 통화를 걸었습니다. + %s님이 전화를 받았습니다. + %s님이 전화를 끊었습니다. + %1$s님이 이후 %2$s에게 방 기록을 공개했습니다 + 초대된 시점부터 모든 방 구성원. + 들어온 시점부터 모든 방 구성원. + 모든 방 구성원. + 누구나. + 알 수 없음 (%s). + %1$s님이 종단 간 암호화를 켰습니다 (%2$s) + %s님이 방을 업그레이드했습니다. + + %1$s님이 VoIP 회의를 요청했습니다 + VoIP 회의가 시작했습니다 + VoIP 회의가 끝났습니다 + + (아바타도 변경됨) + %1$s님이 방 이름을 삭제했습니다 + %1$s님이 방 주제를 삭제했습니다 + 메시지가 삭제되었습니다 + 메시지가 %1$s님에 의해 삭제되었습니다 + 메시지가 삭제되었습니다 [이유: %1$s] + 메시지가 %1$s님에 의해 삭제되었습니다 [이유: %2$s] + %1$s님이 프로필 %2$s을(를) 업데이트했습니다 + %1$s님이 %2$s님에게 방 초대를 보냈습니다 + %1$s님이 %2$s의 초대를 수락했습니다 + + ** 암호를 해독할 수 없음: %s ** + 발신인의 기기에서 이 메시지의 키를 보내지 않았습니다. + + 이 답장의 질문 + + 검열할 수 없습니다 + 메시지를 보낼 수 없습니다 + + 사진 업로드에 실패했습니다 + + 네트워크 오류 + Matrix 오류 + + 현재 빈 방에 다시 들어갈 수 없습니다. + + 암호화된 메시지 + + 이메일 주소 + 전화번호 + + 사진을 보냈습니다. + 동영상을 보냈습니다. + 오디오 파일을 보냈습니다. + 파일을 보냈습니다. + + %s에서 초대함 + 방 초대 + + %1$s님과 %2$s님 + + + %1$s님 외 %2$d명 + + + 빈 방 + + + + 고양이 + 사자 + + 유니콘 + 돼지 + 코끼리 + 토끼 + 판다 + 수탉 + 펭귄 + 거북 + 물고기 + 문어 + 나비 + + 나무 + 선인장 + 버섯 + 지구본 + + 구름 + + 바나나 + 사과 + 딸기 + 옥수수 + 피자 + 케이크 + 하트 + 웃음 + 로봇 + 모자 + 안경 + 스패너 + 산타클로스 + 좋아요 + 우산 + 모래시계 + 시계 + 선물 + 전구 + + 연필 + 클립 + 가위 + 자물쇠 + 열쇠 + 망치 + 전화기 + 깃발 + 기차 + 자전거 + 비행기 + 로켓 + 트로피 + + 기타 + 트럼펫 + + + 폴더 + + + 초기 동기화: +\n계정 가져오는 중… + 초기 동기화: +\n암호 가져오는 중 + 초기 동기화: +\n방 가져오는 중 + 초기 동기화: +\n들어간 방 가져오는 중 + 초기 동기화: +\n초대받은 방 가져오는 중 + 초기 동기화: +\n떠난 방 가져오는 중 + 초기 동기화: +\n커뮤니티 가져오는 중 + 초기 동기화: +\n계정 데이터 가져오는 중 + + 메시지 보내는 중… + 전송 대기 열 지우기 + 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 b46b79ea..c2d524e4 100644 --- a/matrix-sdk-android/src/main/res/values-nl/strings.xml +++ b/matrix-sdk-android/src/main/res/values-nl/strings.xml @@ -176,4 +176,9 @@ Initiële synchronisatie: \nAccountgegevens worden geïmporteerd + %s heeft dit gesprek opgewaardeerd. + + Bericht wordt verstuurd… + Uitgaande wachtrij legen + diff --git a/matrix-sdk-android/src/main/res/values-pl/strings.xml b/matrix-sdk-android/src/main/res/values-pl/strings.xml index 4a807028..4e1d2567 100644 --- a/matrix-sdk-android/src/main/res/values-pl/strings.xml +++ b/matrix-sdk-android/src/main/res/values-pl/strings.xml @@ -7,7 +7,7 @@ %1$s zaprosił(a) %2$s %1$s zaprosił(a) Cię %1$s dołączył(a) - %1$s wyszedł(-ła) + %1$s opuścił(a) %1$s odrzucił(a) zaproszenie %1$s wyrzucił(a) %2$s %1$s odblokował(a) %2$s @@ -17,11 +17,11 @@ %1$s zmienił(a) wyświetlaną nazwę z %2$s na %3$s %1$s usunął(-ęła) swoją wyświetlaną nazwę (%2$s) %1$s zmienił(a) temat na: %2$s - Nie udało się wysłać wiadomości + Nie można wysłać wiadomości - Nie udało się wysłać zdjęcia + Przesyłanie zdjęcia nie powiodło się - ogólne błędy + Błąd sieci Błąd Matrixa Wiadomość zaszyfrowana @@ -31,7 +31,7 @@ wszyscy członkowie pokoju. wszyscy. - %1$s zmienił(a) znawę pokoju na: %2$s + %1$s zmienił(a) nazwę pokoju na: %2$s %s zakończył(a) rozmowę. %1$s usunął(-ęła) nazwę pokoju %1$s usunął(-ęła) temat pokoju @@ -57,9 +57,9 @@ ** Nie można odszyfrować: %s ** - %s umieścił wideo rozmowe. - %s umieścił połączenie głosowe. - %1$s uczynił historię pokoju widoczną do %2$s + %s wykonał(a) rozmowę wideo. + %s wykonał(a) połączenie głosowe. + %1$s uczynił(a) przyszłą historię pokoju widoczną dla %2$s wszyscy członkowie pokoju, od momentu w którym zostali zaproszeni. wszyscy członkowie pokoju, od momentu w którym dołączyli. nieznane (%s). @@ -147,4 +147,29 @@ Mikołaj Prezent Młotek + %s zakutalizował(a) ten pokój. + + Kciuk w górę + Zamek + Piłka + Synchronizacja początkowa: +\nImportowanie konta… + Synchronizacja początkowa: +\nImportowanie kryptografii + Synchronizacja początkowa: +\nImportowanie Pokoi + Synchronizacja początkowa: +\nImportowanie dołączonych Pokoi + Synchronizacja początkowa: +\nImportowanie zaproszonych Pokoi + Synchronizacja początkowa: +\nImportowanie opuszczonych Pokoi + Synchronizacja początkowa: +\nImportowanie Społeczności + Synchronizacja początkowa: +\nImportowanie danych Konta + + Wysyłanie wiadomości… + Wyczyść kolejkę wysyłania + diff --git a/matrix-sdk-android/src/main/res/values-pt/strings.xml b/matrix-sdk-android/src/main/res/values-pt/strings.xml index adfed47e..4bc90cf0 100644 --- a/matrix-sdk-android/src/main/res/values-pt/strings.xml +++ b/matrix-sdk-android/src/main/res/values-pt/strings.xml @@ -78,4 +78,10 @@ Sala vazia + %1$s enviou um sticker. + + %s fez o upgrade da sala. + + Mensagem removida + Mensagem removida por %1$s 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 4563fa89..e0637ea6 100644 --- a/matrix-sdk-android/src/main/res/values-ru/strings.xml +++ b/matrix-sdk-android/src/main/res/values-ru/strings.xml @@ -169,9 +169,9 @@ \nИмпорт криптографии Начальная синхронизация: \nИмпорт комнат - Начальная синхронизация: + Синхронизация начата: \nИмпорт присоединенных комнат - Начальная синхронизация: + Синхронизация начата: \nИмпорт приглашенных комнат Начальная синхронизация: \nИмпорт покинутых комнат @@ -180,4 +180,9 @@ Начальная синхронизация: \nИмпорт данных учетной записи + %s обновил эту комнату. + + Отправка сообщения… + Очистить очередь отправки + diff --git a/matrix-sdk-android/src/main/res/values-sk/strings.xml b/matrix-sdk-android/src/main/res/values-sk/strings.xml index b7e9bb41..d5c7d95f 100644 --- a/matrix-sdk-android/src/main/res/values-sk/strings.xml +++ b/matrix-sdk-android/src/main/res/values-sk/strings.xml @@ -82,4 +82,95 @@ + %s aktualizoval túto miestnosť. + + Správa odstránená + Správa odstránená používateľom %1$s + Správa odstránená [dôvod: %1$s] + Správa odstránená používateľom %1$s [dôvod: %2$s] + Pes + Mačka + Lev + Kôň + Jednorožec + Prasa + Slon + Zajac + Panda + Kohút + Tučniak + Korytnačka + Ryba + Chobotnica + Motýľ + Kvetina + Strom + Kaktus + Hríb + Zemeguľa + Mesiac + Oblak + Oheň + Banán + Jablko + Jahoda + Kukurica + Pizza + Koláč + Srdce + Úsmev + Robot + Klobúk + Okuliare + Skrutkovač + Mikuláš + Palec nahor + Dáždnik + Presýpacie hodiny + Hodiny + Darček + Žiarovka + Kniha + Ceruzka + Kancelárska sponka + Nožnice + Zámok + Kľúč + Kladivo + Telefón + Vlajka + Vlak + Bicykel + Lietadlo + Raketa + Trofej + Lopta + Gitara + Trúbka + Zvonček + Kotva + Schlúchadlá + Priečinok + Pin + + Úvodná synchronizácia: +\nPrebieha import účtu… + Úvodná synchronizácia: +\nPrebieha import šifrovacích kľúčov + Úvodná synchronizácia: +\nPrebieha import miestností + Úvodná synchronizácia: +\nPrebieha import miestností, do ktorých ste vstúpili + Úvodná synchronizácia: +\nPrebieha import pozvánok + Úvodná synchronizácia: +\nPrebieha import opustených miestností + Úvodná synchronizácia: +\nPrebieha import komunít + Úvodná synchronizácia: +\nPrebieha import údajov účtu + + Odosielanie správy… + Vymazať správy na odoslanie + 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 7c182df3..03d5df2b 100644 --- a/matrix-sdk-android/src/main/res/values-sq/strings.xml +++ b/matrix-sdk-android/src/main/res/values-sq/strings.xml @@ -146,4 +146,26 @@ Spirancë Kufje Dosje + %s e përmirësoi këtë dhomë. + + Njëkohësimi Fillestar: +\nPo importohet llogaria… + Njëkohësimi Fillestar: +\nPo importohet kriptografi + Njëkohësimi Fillestar: +\nPo importohen Dhoma + Njëkohësimi Fillestar: +\nPo importohen Dhoma Ku Është Bërë Hyrje + Njëkohësimi Fillestar: +\nPo importohen Dhoma Me Ftesë + Njëkohësimi Fillestar: +\nPo importohen Dhoma të Braktisura + Njëkohësimi Fillestar: +\nPo importohen Bashkësi + Njëkohësimi Fillestar: +\nPo importohet të Dhëna Llogarie + + Po dërgohet mesazh… + Spastro radhë pritjeje + 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 29e66940..eb533e15 100644 --- a/matrix-sdk-android/src/main/res/values-vls/strings.xml +++ b/matrix-sdk-android/src/main/res/values-vls/strings.xml @@ -48,7 +48,7 @@ %1$s èt d’uutnodigienge vo %2$s anveird ** Kun nie ountsleuteln: %s ** - ’t Toestel van den afzender èt geen sleutels vo dit bericht gesteurd. + ’t Toestel van den afzender èt geen sleutels vo da bericht hier gesteurd. Als antwoord ip @@ -150,21 +150,26 @@ Mappe Pinne - Initiële synchronisoatie: + Initiële synchronisoasje: \nAccount wor geïmporteerd… - Initiële synchronisoatie: + Initiële synchronisoasje: \nCrypto wor geïmporteerd - Initiële synchronisoatie: + Initiële synchronisoasje: \nGesprekkn wordn geïmporteerd - Initiële synchronisoatie: + Initiële synchronisoasje: \nDeelgenoomn gesprekken wordn geïmporteerd - Initiële synchronisoatie: + Initiële synchronisoasje: \nUutgenodigde gesprekkn wordn geïmporteerd - Initiële synchronisoatie: + Initiële synchronisoasje: \nVerloatn gesprekkn wordn geïmporteerd - Initiële synchronisoatie: + Initiële synchronisoasje: \nGemeenschappn wordn geïmporteerd - Initiële synchronisoatie: + Initiële synchronisoasje: \nAccountgegeevns wordn geïmporteerd + %s èt da gesprek hier ipgewoardeerd. + + Bericht wor verstuurd… + Uutgoande wachtreeke leegn + diff --git a/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml b/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml index 70c99089..3aed8858 100644 --- a/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml +++ b/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml @@ -162,4 +162,9 @@ 初始化同步: \n正在导入账号数据 + %s 升级了聊天室。 + + 正在发送消息… + 清除正在发送队列 + 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 0a8f0b45..e0e66a80 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 @@ -165,4 +165,9 @@ 初始化同步: \n正在匯入帳號資料 + %s 已升級此聊天室。 + + 正在傳送訊息…… + 清除傳送佇列 + diff --git a/vector/src/main/res/values-bg/strings.xml b/vector/src/main/res/values-bg/strings.xml index 7f93b37a..d8570ef0 100644 --- a/vector/src/main/res/values-bg/strings.xml +++ b/vector/src/main/res/values-bg/strings.xml @@ -326,7 +326,7 @@ Съобщенията не са изпратени. %1$s или %2$s? Съобщението не е изпратено поради наличието на непознати устройства. %1$s или %2$s? Изпрати всички отново - откажи всички + Откажи всички Изпрати отново неизпратените съобщения Изтрий неизпратените съобщения Файлът не е намерен @@ -1600,4 +1600,17 @@ Връзката беше копирана + Мениджър на интеграции + + Не е конфигуриран мениджър на интеграции. + Добави по Matrix идентификатор + Създаване на стая… + Не са намерени резултати, използва \"Добави по Matrix идентификатор\" за търсене на сървъра. + Започнете да пишете, за да получите резултати + Филтрирай по потребител или ID… + + Присъединяване в стая… + + Виж историята на редакциите + diff --git a/vector/src/main/res/values-ca/strings.xml b/vector/src/main/res/values-ca/strings.xml index a73b7991..ca7a2287 100644 --- a/vector/src/main/res/values-ca/strings.xml +++ b/vector/src/main/res/values-ca/strings.xml @@ -12,7 +12,7 @@ Tema negre - Sincronitzant + S\'està sincronitzant… Escolta esdeveniments Notificacions sorolloses Notificacions silencioses @@ -46,9 +46,10 @@ Reanomena Informa del contingut Trucada activa - " Conferència en curs.\nUniu-vos hi per %1$s o %2$s." - veu - vídeo + Conferència en curs. +\nUniu-vos hi per %1$s o %2$s. + Veu + Vídeo La trucada no es pot iniciar, prova-ho més tard Pot ser que algunes funcions no apareguin per manca de permisos… Es necessiten permisos per convidar a iniciar una conferència en aquesta sala @@ -84,11 +85,11 @@ Sales - Cerca sales - Cerca preferits - Cerca persones - Cerca sales - Cerca comunitats + Filtrar noms de sales + Filtrar preferits + Filtrar persones + Filtrar per noms de sala + Filtrar per nom de comunitats Convida @@ -137,7 +138,7 @@ Uneix-te a la sala Nom d\'usuari - Registra\'m + Crear un compte Entra Desconnecta URL del servidor @@ -235,7 +236,7 @@ - "Envia com " + Envia com Original Gran Mitjana @@ -283,7 +284,9 @@ Per tal de fer trucades de veu, el Riot necessita permís d\'accés al microfon. \n\nA la següent finestra emergent, doneu permís d\'accés per tal de poder fer la trucada. Per tal de fer vídeotrucades, el Riot necessita permís d\'accés a la càmera i al microfon.\n\nA la següent finestra emergent, doneu permís d\'accés per tal de poder fer la trucada. - Per tal de trobar altres usuaris de Matrix a partir dels seus correus electrònics o dels seus números de telefon, el Riot necessita permís d\'accés a l\'agenda de contactes.\n\nA la següent finestra emergent, doneu permís d\'accés per trobar amb quins contactes de la teva agenda pots contactar a partir de Riot. + Riot pot comprovar la vostra agenda de contactes per tal de trobar altres usuaris de Matrix basant-se en les seves adreces de correu i números de telèfon. +\n +\nSi accepteu compartir la vostra agenda de contactes amb aquesta finalitat, si us plau permeteu l\'accés de la següent finestra emergent. Per tal de trobar altres usuaris de Matrix a partir dels seus correus electrònics o dels seus números de telefon, el Riot necessita permís d\'accés a l\'agenda de contactes.\n\nPermeteu que Riot accedeixi als vostres contactes? No s\'ha realitzat l\'acció per falta de permisos @@ -370,7 +373,7 @@ Els missatges no s\'han enviat. %1$s o %2$s ara? Els missatges no s\'han enviat perquè hi ha disposistius desconeguts. %1$s o %2$s ara? Reenvia-ho tot - cancel·la-ho tot + Cancel·la-ho tot Reenvia els missatges no enviats Elimina els missatges no enviats No s\'ha trobat el fitxer @@ -542,9 +545,9 @@ Tingueu en compte que aquesta acció reiniciarà l\'aplicació i que pot trigar Aquest número de telèfon ja està en ús. Canvia la contrasenya - Contrasenya antiga + Contrasenya actual Contrasenya nova - Confirmeu la contrasenya + Confirmeu la nova contrasenya No s\'ha pogut actualitzar la contrasenya La contrasenya s\'ha actualitzat Mostra tots els missatges des de %s? @@ -889,7 +892,7 @@ Atenció: es podria eliminar aquest fitxer si es desinstal·la l\'aplicació.Si us plau, engega Riot a un altre dispositiu que pugui desencriptar el missatge de manera que pugui enviar la clau a aquest dispositiu. Normal - Motiu Status.im + Tema Status.im Manquen permisos per a dur a terme aquesta acció. Error @@ -924,7 +927,7 @@ En voleu afegir algun? Mostrar tots els missatges d\'aquest usuari? Tingueu en compte que aquesta acció reiniciarà l\'aplicació i pot trigar una estona. - "%1$s, " + "%1$s,· " %1$s i %2$s %1$s %2$s @@ -938,7 +941,7 @@ Tingueu en compte que aquesta acció reiniciarà l\'aplicació i pot trigar una Notificació de privacitat Privacitat reduïda L\'aplicació necessita permisos per funcionar en segon pla - • Les notificacions s\'envien via Google Cloud Messaging + • Les notificacions s\'envien via Firebase Cloud Messaging • Les notificacions contenen només meta dades • El contingut dels missatges de les notificacions s\'obté de forma segura des del servidor de Matrix • Les notificacions contenen meta dades i dades de missatges @@ -1246,7 +1249,7 @@ Aquest error és fora del control del Riot. No hi ha cap compte de Google al tel Les restriccions de rerefons són habilitades per al Riot. Les tasques que l\'aplicació intenta fer estaran restringides agressivament mentre estigui al rerefons, i això pot afectar les notificacions. %1$s - "Si un usuari deixa un dispositiu sense endollar i immòbil durant un període de temps, amb la pantalla apagada, el dispositiu entra en el mode Doze. Això impedeix les aplicacions d\'accedir a la xarxa i ajorna les seves tasques, sincronitzacions i alarmes estàndard. " + Si un usuari deixa un dispositiu sense endollar i immòbil durant un període de temps, amb la pantalla apagada, el dispositiu entra en el mode d\'estalvi d\'energia. Això impedeix les aplicacions d\'accedir a la xarxa i ajorna les seves tasques, sincronitzacions i alarmes estàndard. "Una aplicació de xat, sota el vostre control i totalment flexible. El Riot us permet comunicar-vos de la manera que preferiu. Fet per al [matrix] - l’estàndard per a la comunicació oberta i descentralitzada. Obteniu un compte gratuït de matrix.org, executeu el vostre servidor propi a https://modular.im, o empreu un altre servidor de Matrix. @@ -1385,7 +1388,7 @@ Per què triar Riot.im? Inicialitzar servei Ignorar - Registrar-se amb Single Sign-on + Iniciar sessió amb Single Sign-on Aquesta URL no està disponible , si us plau verifiqueu-la El vostre dispositiu està usant una versió obsoleta del protocol de seguretat TLS, vulnerable a atacs. Per a la vostra seguretat no us podreu connectar Envieu un missatge amb Enter @@ -1403,4 +1406,174 @@ Per què triar Riot.im? Resposta no vàlida en descobrir homeservers Usar Config + Verificar dispositiu + + Marcar com a llegit + Les app no necessita connectar-se al HomeServer en segon pla, hauria de reduir el consum de bateria + Administrador d\'integracions + + Reproduir el so de disparador + + IP desconeguda + + %1$s: 1 missatge + %1$s: %2$d missatges + + + %d notificacion + %d notificacions + + + Nou esdeveniment + Sala + Missatges nous + Nova invitació + Jo + ** Error en enviar - Si us plau obriu la sala + + Ho sentim, els dispositius amb SO Android inferior a 5.0 no suporten trucades multi-usuari amb Jitsi + + No heu configurat cap administrador d\'integracions. + Un nou dispositiu està sol·licitant claus d\'encriptació. +\nNom del dispositiu: %1$s +\nVist per última vegada: %s$s +\nSi no heu iniciat sessió en un altre dispositiu, ignoreu la sol·licitud. + Un dispositiu no verificat està sol·licitant claus d\'encriptació. +\nNom del dispositiu: %1$s +\nVist per última vegada: %s$s +\nSi no heu iniciat sessió en un altre dispositiu, ignoreu la sol·licitud. + + Verificar + Compartir + Sol·licitud de compartició de clau + Ignorar + + Ja existeix una còpia de seguretat al vostre HomeServer + Sembla que ja heu configurat una còpia de seguretat de claus des d\'un altre dispositiu. Voleu reemplaçar-la amb la que esteu creant\? + Reemplaçar + Aturar + + Comprovant l\'estat de la còpia de seguretat + Opcions d\'autocompleció del servidor + Riot ha detectat una configuració de servidor personalitzat pel domini del seu identificador d\'usuari \"%1$s\": +\n%2$s + Us heu desconnectat a causa de credencials incorrectes o caducades. + + Verificar comparant una cadena de text curta. + Per la màxima seguretat us recomanem fer això en persona o usar un altre medi de comunicació confiable. + Començar la verificació + Sol·licitud de verificació entrant + Verificar aquest dispositiu per marcar-lo com a confiable. Confiar en dispositius d\'amistats us dona un alleujament addicional quan useu missatges encriptats end-to-end. + Verificant aquest dispositiu el marcareu com a confiable, i també marcareu el vostre dispositiu com a confiable pel vostre company. + + Verificar aquest dispositiu confirmant els següents emojis que apareguin a la pantalla del vostre company + Verificar aquest dispositiu confirmant els següents números que sortiran a la pantalla del vostre company + + Heu rebut una sol·licitud de verificació entrant. + Veure sol·licitud + Esperant que el vostre company confirmi… + + Verificat! + Heu verificat aquest dispositiu amb èxit. + Els missatges segurs amb aquest usuari estan encriptats end-to-end i no serà possible llegir-los per tercers. + Entesos + + No surt res\? Encara no tots els clients suporten la verificació interactiva. Useu el mètode de verificació antic. + Useu el mètode antic de verificació. + + Verificació de clau + Sol·licitud cancel·lada + L\'altre part ha cancel·lat la verificació. +\n%s + S\'ha cancel·lat la verificació. +\nMotiu: %s + + Verificació de dispositiu interactiva + Sol·licitud de verificació + %s vol verificar el vostre dispositiu + + L\'usuari ha cancel·lat la verificació + El marge de temps pel procés de verificació ha expirat + El dispositiu no coneix la transacció + El dispositiu no pot acceptar un acord de claus amb mètodes hash, MAC o SAS + El compromís del hash no ha coincidit + El SAS no ha coincidit + El dispositiu ha rebut un missatge inesperat + S\'ha rebut un missatge invàlid + La clau no coincideix + L\'usuari no coincideix + Error desconegut + + + Editar + Respondre + + Tornar-ho a provar + Unir-se a una sala per començar usant l\'app. + Se t\'ha enviat una invitació + Convidat per %s + + Esteu al dia! + No teniu més missatges sense llegir + Benvingut a casa! + Posar-se al dia dels missatges sense llegir + Converses + Els vostres missatges directes es mostraran aquí + Sales + Les vostres sales es mostraran aquí + + Reaccions + Confirmar + M\'agrada + Afegir reacció + Veure reaccions + Reaccions + + Esdeveniment eliminat per l\'usuari + Esdeveniment moderat per l\'administrador de la sala + Última edició per %s el %s + + + Esdeveniment mal format, no es pot mostrar + Crear sala nova + No hi ha xarxa. Si us plau comproveu la vostra connexió a internet. + Canviar + Canviar de xarxa + Espereu, si us plau… + Totes les comunitats + + Aquesta sala no es pot pre-visualitzar + RiotX encara no suporta la pre-visualització de sales llegibles per tothom + + Sales + Missatges directes + + Sala nova + CREAR + Nom de la sala + Públic + Qualsevol podrà unir-se a aquesta sala + Directori de sales + Publicar aquesta sala al directori de sales + + Hi ha hagut un error rebent informació de confança + Hi ha hagut un error rebent dades de la còpia de seguretat de les claus + + Benvinguts a la beta! + Mentre RiotX estigui en les primeres etapes de desenvolupament, faltaran algunes funcions i podríeu experimentar alguns errors. + L\'última llista de característiques està sempre a %1$s, i si trobeu errors si us plau reporteu informe des de la part superior esquerra del menú d\'Inici, i el resoldrem tan aviat com puguem. + Descripció de la Play Store + Si trobeu errors si us plau envieu un informe d\'errors des de la part superior esquerra del menú d\'Inici, i el resoldrem tan aviat com puguem. + + Importar claus e2e des del fitxer \"%1$s\". + + Versió de l\'SDK de Matrix + Ja esteu veient aquesta sala! + + Reaccions ràpides + + General + Preferències + Seguretat i privadesa + Expert diff --git a/vector/src/main/res/values-de/strings.xml b/vector/src/main/res/values-de/strings.xml index 912063a6..313ead3a 100644 --- a/vector/src/main/res/values-de/strings.xml +++ b/vector/src/main/res/values-de/strings.xml @@ -210,7 +210,7 @@ Du wurdest auf allen Geräten abgemeldet und wirst keine Push-Benachrichtigungen - "Sende als " + Sende als Original Groß Mittel @@ -1551,4 +1551,113 @@ Wenn du diese neue Wiederherstellungsmethode nicht eingerichtet hast, kann ein A Raumverzeichnis Veröffentliche diesen Raum ins Raumverzeichnis + Integrationsmanager + + Kein Integrationsmanager konfiguriert. + Schlüsselfreigabe Anfordern + Es sieht so aus, als hätten Sie bereits ein Setup-Schlüssel-Backup von einem anderen Gerät. Möchten Sie es durch das ersetzen, das Sie gerade erstellen\? + Für maximale Sicherheit empfehlen wir, dies persönlich zu tun oder ein anderes vertrauenswürdiges Kommunikationsmittel zu verwenden. + Überprüfen Sie dieses Gerät, um es als vertrauenswürdig zu markieren. Das Vertrauen auf Geräte von Partnern gibt Ihnen zusätzliche Sicherheit, wenn Sie verschlüsselte End-to-End-Nachrichten verwenden. + Das Verifizieren dieses Benutzers wird seine Geräte als \"vertraut\" markieren und dein Gerät bei ihnen als \"vertraut\" markieren. + + Verifizieren Sie diesen Benutzer, indem Sie bestätigen, dass folgendes Emoji auf dessen Bildschirm erscheint. + Verifizieren Sie diesen Benutzer, indem Sie bestätigen, dass die folgende Nummer auf dessen Bildschirm erscheint. + + Es ist nichts aufgetaucht\? Noch nicht alle Clients unterstützen die interaktive Verifikation. . + Verwenden Sie die Alte-Überprüfung + + Das Gerät kennt diese Transaktion nicht + Die Hash-Verpflichtung stimmte nicht überein + Die SAS stimmte nicht überein + Wiederherstellungsschlüssel passt nicht + Der Benutzer Wiederherstellungsschlüssel passt nicht + Informieren Sie sich hier über ungelesene Nachrichten + Ihre direkte Konversation wird hier angezeigt + Fehlerhaftes Ereignis, kann nicht angezeigt werden + Beim Abrufen der Vertrauensinformationen ist ein Fehler aufgetreten + Beim Abrufen der Schlüsselsicherungsdaten ist ein Fehler aufgetreten + + Willkommen in der Beta! + Während sich RiotX in der frühen Entwicklung befindet, fehlen möglicherweise einige Funktionen und es können Fehler auftreten. + Play Store Beschreibung + Matrix SDK Version + Sonstige Hinweise Dritter + Sie sehen diesen Raum bereits! + + Schnelle Reaktionen + + Allgemein + Einstellungen + Sicherheit & Privatsphäre + Experte + Push-Regeln + Keine Push-Regeln definiert + Keine registrierten Push-Gateways + + Sprache & Video + Hilfe & Über + + + Token registrieren + + Mache einen Vorschlag + Bitte schreiben Sie Ihren Vorschlag unten. + Beschreiben Sie hier Ihren Vorschlag + Versteckte Ereignisse in der Zeitleiste anzeigen + + Direkte Nachrichten + + Warten… + Miniaturbild wird verschlüsselt… + Datei wird verschlüsselt… + (bearbeitet) + + Nachrichtenbearbeitung + Keine Änderungen gefunden + + Gespräche filtern… + Senden Sie eine neue Direktnachricht + Das Raumverzeichnis anzeigen + + Link in die Zwischenablage kopiert + + Nach Matrix-ID hinzufügen + Raum erstellen… + Bearbeitungsverlauf anzeigen + + Die andere Partei hat die Überprüfung abgebrochen. +\n%s + Das Gerät kann sich nicht auf eine Schlüsselvereinbarung, eine Hash-, eine MAC- oder eine SAS-Methode einigen + Die neueste Featureliste befindet sich immer in %1$s. Wenn Sie Fehler finden, senden Sie uns bitte einen Bericht im Menü oben links von \"Startseite\". Wir werden den Fehler so schnell wie möglich beheben. + Wenn Sie Fehler finden, senden Sie uns bitte einen Bericht im Menü oben links von \"Startseite\". Wir werden diese so schnell wie möglich beheben. + + Importieren Sie e2e-Schlüssel aus der Datei \"%1$s\". + + Vielen Dank, der Vorschlag wurde erfolgreich gesendet + Der Vorschlag konnte nicht gesendet werden (%s) + + RiotX - Matrix Client der nächsten Generation + Ein schnellerer und leichterer Client für Matrix mit den neuesten Android-Frameworks + Miniaturbild wird gesendet (%1$s / %2$s) + Datei wird gesendet (%1$s / %2$s) + + Datei %1$s wird heruntergeladen … + Die Datei %1$s wurde heruntergeladen! + + %1$s , um ein Konto zu erstellen. + Benutze die alte App + + + Können Sie nicht finden, wonach Sie suchen\? + Erstelle einen neuen Raum + Name oder ID (#Beispiel: matrix.org) + + Aktivieren Sie das Streichen, um in der Zeitleiste zu antworten + + Kein Ergebnis gefunden. Verwenden Sie Nach Matrix-ID hinzufügen, um auf dem Server zu suchen. + Beginnen Sie mit der Eingabe, um Ergebnisse zu erhalten + Filtern nach Benutzername oder ID… + + Raum betreten… + diff --git a/vector/src/main/res/values-eu/strings.xml b/vector/src/main/res/values-eu/strings.xml index 3fa89265..f4c2b81e 100644 --- a/vector/src/main/res/values-eu/strings.xml +++ b/vector/src/main/res/values-eu/strings.xml @@ -331,7 +331,7 @@ Ziur zaude? Bidali gabeko mezuak daude. %1$s edo %2$s orain? Mezuak ez dira bidali gailu ezezagunak daudelako. %1$s edo %2$s orain? Birbidali dena - utzi dena + Utzi dena Birbidali bidali gabeko mezuak Ezabatu bidali gabeko mezuak Ez da fitxategia aurkitu @@ -1607,4 +1607,17 @@ Abisua: Fitxategi hau ezabatu daiteke aplikazioa desinstalatzen bada. Esteka arbelera kopiatu da + Integrazio kudeatzailea + + Ez da integrazio kudeatzailerik konfiguratu. + Gehitu matrix ID bidez + Gela sortzen… + Ez da emaitzarik aurkitu, erabili gehitu matrix ID bidez zerbitzarian bilatzeko. + Hasi idazten emaitzak jasotzeko + Iragazi erabiltzaile-izena edo ID-a erabiliz… + + Gelara elkartzen… + + Ikusi edizioen historiala + diff --git a/vector/src/main/res/values-fi/strings.xml b/vector/src/main/res/values-fi/strings.xml index 88553114..60ddb267 100644 --- a/vector/src/main/res/values-fi/strings.xml +++ b/vector/src/main/res/values-fi/strings.xml @@ -111,7 +111,7 @@ Virheraportti lähetettiin Virheraporttia ei voitu lähettää (%s) Lähetetään (%s%%) - Sovellus kaatui viime kerralla. Haluaisitko tehdä kaatumisesta virheilmoituksen\? + Sovellus kaatui viime kerralla. Haluatko tehdä kaatumisesta virheilmoituksen\? Lähetä Luettu @@ -1533,4 +1533,83 @@ Jotta et menetä mitään, automaattiset päivitykset kannattaa pitää käytös Asetukset Tietoturva ja yksityisyys + Olet ajan tasalla! + Viimeksi muokannut %s %s + + + Vaihda verkkoa + Katselet jo tätä huonetta! + + Yleiset + Ääni ja video + Ohje ja tietoa + + + Tee ehdotus + Kirjoita ehdotuksesi alle. + Kuvaile ehdotuksesi tässä + Kiitos, ehdotus on lähetetty + Ehdotuksen lähettäminen epäonnistui (%s) + + Näytä piilotetut tapahtumat aikajanalla + + RiotX - seuraavan sukupolven Matrix-asiakasohjelma + Nopeampi ja kevyempi Matrix-asiakasohjelma + Yksityisviestit + + Odotetaan… + Salataan tiedostoa… + Lähetetään tiedostoa (%1$s / %2$s) + + Ladataan tiedostoa %1$s… + Tiedosto %1$s ladattu! + + (muokattu) + + Käytä vanhaa sovellusta + + + Muokkauksia ei löytynyt + + Suodata keskusteluja… + Etkö löydä, mitä etsit\? + Luo uusi huone + Lähetä uusi yksityisviesti + Näytä huoneluettelo + + Linkki kopioitu leikepöydälle + + Luodaan huonetta… + Näet tuloksia kirjoittamalla jotain + Liitytään huoneeseen… + + Näytä muokkaushistoria + + Vahvista laite + + Vahvista + Avaimen jakopyyntö + Aloita vahvistaminen + Saapuva vahvistuspyyntö + Vastaanotit saapuvan vahvistuspyynnön. + Vahvistettu! + Vahvistit tämän laitteen onnistuneesti. + Avaimen vahvistus + Toinen osapuoli perui vahvistuksen. +\n%s + Vahvistus on peruttu. +\nSyy: %s + + Vuorovaikutteinen laitteen vahvistus + Vahvistuspyyntö + %s haluaa vahvistaa laitteesi + + Käyttäjä perui vahvistuksen + Vahvistustoimenpide aikakatkaistiin + Huoneluettelo + Julkaise tämä huone huoneluettelossa + + Play Storen kuvaus + Push-säännöt + %1$s luodaksesi tilin. diff --git a/vector/src/main/res/values-fr/strings.xml b/vector/src/main/res/values-fr/strings.xml index 1b60f5a1..5e7dbe8e 100644 --- a/vector/src/main/res/values-fr/strings.xml +++ b/vector/src/main/res/values-fr/strings.xml @@ -219,7 +219,7 @@ Envoyer un message chiffré… Envoyer un message (non chiffré)… Tout renvoyer - tout annuler + Tout annuler Renvoyer les messages non envoyés Supprimer les messages non envoyés Fichier non trouvé @@ -1612,4 +1612,17 @@ Si vous n’avez pas configuré de nouvelle méthode de récupération, un attaq Lien copié dans le presse-papiers + Gestionnaire d’intégrations + + Aucun gestionnaire d’intégrations n’est configuré. + Ajouter par identifiant matrix + Création du salon… + Aucun résultat trouvé, utilisez Ajouter par identifiant matrix pour chercher sur le serveur. + Entrez du texte pour avoir des résultats + Filtrer par nom d’utilisateur ou identifiant… + + En train de rejoindre le salon… + + Voir l’historique des éditions + diff --git a/vector/src/main/res/values-hu/strings.xml b/vector/src/main/res/values-hu/strings.xml index 9f477da3..3d0174b9 100644 --- a/vector/src/main/res/values-hu/strings.xml +++ b/vector/src/main/res/values-hu/strings.xml @@ -329,7 +329,7 @@ Kérlek engedélyezd a hozzáférést a következő felugró ablakban, hogy tudj Üzenetek nem kerültek elküldésre. %1$s vagy %2$s most? Üzenetek nem kerültek elküldésre mert egy ismeretlen eszköz van jelen. %1$s vagy %2$s most? Összes újraküldése - Összes megszüntetése + Összes elvetése El nem küldött üzenetek újraküldése El nem küldött üzenetek törlése Fájl nem található @@ -1611,4 +1611,17 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró Hivatkozás a vágólapra másolva + Integrációs Menedzser + + Integrációs Menedzser nincs beállítva. + Hozzáadás matrix ID-vel + Szoba létrehozása… + Nincs találat, használd a „Hozzáadás matrix ID-vel”-t a szerveren való kereséshez. + A találatokhoz kezdj gépelni + Szűrés felhasználói névre vagy azonosítóra… + + Szobába belépés… + + Szerkesztési napló megtekintése + diff --git a/vector/src/main/res/values-it/strings.xml b/vector/src/main/res/values-it/strings.xml index c414f38d..3719ba0f 100644 --- a/vector/src/main/res/values-it/strings.xml +++ b/vector/src/main/res/values-it/strings.xml @@ -362,7 +362,7 @@ Sicuro di procedere? Messaggi non inviati. %1$s o %2$s ora? Messaggi non inviati a causa di dispositivi sconosciuti qui presenti. %1$s o %2$s ora? Rispedisci tutto - annulla tutto + Annulla tutto Rinvia messaggi non spediti Elimina messaggi non spediti File non trovato @@ -1656,4 +1656,17 @@ Per essere certo di non perdere nulla, mantieni gli aggiornamenti attivi."Collegamento copiato negli appunti + Gestore dell\'integrazione + + Nessun gestore integrazione configurato. + Aggiungi per ID matrix + Creare una stanza … + Nessun risultato trovato, usa \"Aggiungi per ID matrix\" per cercare sul server. + Inizia a digitare per ottenere risultati + Filtra per nome utente o ID … + + Sto entrando nella stanza … + + Visualizza Modifica cronologia + diff --git a/vector/src/main/res/values-ko/strings.xml b/vector/src/main/res/values-ko/strings.xml index 5e357eb6..188dd6ce 100644 --- a/vector/src/main/res/values-ko/strings.xml +++ b/vector/src/main/res/values-ko/strings.xml @@ -1,36 +1,36 @@ ko - KO + KR 밝은 테마 어두운 테마 검정 테마 - 동기화하는중… + 동기화 중… 메시지 설정 - 보관 + 기록 버그 보고서 스티커 보내기 - 제3자 라이센스 + 제 3자 라이선스 - 불러오는중… + 불러오는 중… 취소 저장 떠나기 보내기 - 복사 + 복사하기 다시 보내기 - 지우기 + 삭제하기 인용 받기 공유 - 고유주소 + 퍼머링크 출처 보기 해독된 출처 보기 지우기 @@ -43,7 +43,7 @@ 권한이 없어졌기 때문에, 이 행동을 할 수 없습니다. 전화를 걸 수 없습니다 기기 정보 - 그래도 보내기 + 무시하고 보내기 또는 초대 미접속 @@ -53,8 +53,8 @@ 로그아웃 음성 통화하기 영상 통화하기 - 모두 읽음 표시하기 - 보관 + 모두 읽음으로 표시 + 기록 빠른 답장 열기 닫기 @@ -71,10 +71,10 @@ 커뮤니티 - + 방 이름 필터 시스템 알림 - 결과가 없습니다 + 결과 없음 로그 보내기 @@ -90,14 +90,14 @@ 로그인 로그아웃 홈서버 URL - 아이덴티티 서버 URL - 찾기 + ID 서버 URL + 검색 새 대화 시작하기 음성 통화 시작하기 영상 통화 시작하기 - 정말로 %s와 대화하고 싶으세요? + 정말로 %s님과 새 대화를 시작하시겠습니까\? 정말 음성통화를 시작하시겠어요? 정말 영상통화를 시작하시겠어요? @@ -111,12 +111,12 @@ 계정 만들기 제출하기 건너뛰기 - 이메일이나 이용자 이름 + 이메일이나 사용자 이름 비밀번호 새 비밀번호 - 이용자 이름 - 이용자가 당신을 찾고 재설정 비밀번호를 보낼 수 있게 이메일 주소를 적어주세요. - 이용자가 당신을 찾을 수 있게 계정에 전화번호를 추가해주세요. + 사용자 이름 + 사용자가 당신을 찾고 재 설정 비밀번호를 보낼 수 있게 이메일 주소를 적어주세요. + 사용자가 당신을 찾을 수 있게 계정에 전화번호를 추가해주세요. Status.im 테마 서비스 초기화 중 @@ -132,22 +132,22 @@ 키 백업이 끝나지 않았습니다, 기다려주세요… 지금 로그아웃하면 암호화된 메세지가 사라집니다 키 백업하기 - 정말이세요\? + 확신합니까\? 백업 머물기 넘기기 완료 정말 로그아웃하시겠어요\? - 읽었다고 표시 + 읽음으로 표시 Riot이 주소록에 접근할 수 없게 되어 있습니다 - %d 사람들 + %d 명의 사용자 초대 커뮤니티 최근에 애플리케이션이 충돌한 것 같습니다. 충돌 보고서를 열까요\? - 읽기 + 읽음 사용자 이름 음성 보내기 @@ -165,11 +165,11 @@ 새 비밀번호 확인 알맞지 않은 사용자 이름이나 비밀번호 비밀번호가 너무 짧아요 (최소 6자) - 유효한 이메일 주소가 아닌 것 같아요 - 유효한 전화번호가 아닌 것 같아요 + 올바른 이메일 주소가 아닙니다 + 올바른 전화번호가 아닙니다 비밀번호가 맞지 않아요 비밀번호를 잊어버리셨나요\? - 사용중인 사용자 이름입니다 + 사용 중인 사용자 이름입니다 홈서버: 비밀번호를 초기화하려면, 계정에 이메일을 등록해야합니다: 새 비밀번호를 입력해야합니다. @@ -178,4 +178,1373 @@ \n모든 기기에서 로그아웃되고 알림도 가지 않을 거에요. 다시 알림을 받으려면, 각 기기에 다시 로그인하세요. 모바일 + 이벤트 청취하기 + 키 백업이 진행 중입니다. 지금 로그아웃하면 암호화된 메시지에 접근할 수 없습니다. + 암호화된 메시지에 대한 접근을 읽지 않도록 모든 장치에서 보안 키 백업이 활성화되어 있어야 합니다. + 암호화된 메시지를 원하지 않습니다 + 키 백업 중… + 로그아웃하기 전에 키를 백업하지 않으면 암호화된 메시지에 접근할 수 없습니다. + + 말하기 + 지우기 + + 가져오기 + 현재 전화 + 회의 전화 진행 중. +\n%1$s 또는 %2$s로 참가하세요 + 이 방에서 회의를 시작하려면 초대할 권한이 필요합니다 + 회의 전화는 암호화된 방에서 지원하지 않습니다 + 무시하고 통화 + 수락하기 + 중단 + 무시 + + 세계 검색 + 즐겨찾기 필터 + 사람 필터 + 방 이름 필터 + 커뮤니티 이름 필터 + + 초대 + 중요하지 않음 + 대화 + 로컬 주소록 + 사용자 디렉토리 + Matrix 주소록만 + 대화 안 함 + 방 디렉토리 + 방 없음 + 이용할 수 있는 공공 방이 없음 + 그룹 없음 + + 문제를 진단하기 위해, 이 클라이언트의 로그는 버그 보고서와 함께 전송됩니다. 이 버그 보고서에는 로그와 스크린샷이 포함되면 공개적으로 표시되지 않습니다. 위의 텍스트만 보내려면 다음을 선택 해제하세요: + 좌절감에 휴대 전화를 흔들고 있는 것 같네요. 버그 보고서 화면을 열어보겠습니까\? + 분노의 흔들기로 버그 보고하기 + + 진행 (%s%%) + + 여기로 보내기 + 방 들어가기 + 계속… + 죄송합니다, 이 행동을 완료하기 위한 외부 애플리케이션이 없습니다. + + 통합 인증으로 로그인 + 재 설정 이메일 보내기 + 사용자가 당신을 찾을 수 있도록 계정에 이메일 주소 그리고/또는 전화번호를 추가하세요. +\n +\n이메일 주소로 비밀번호를 재 설정할 수도 있습니다. + 사용자가 당신을 찾을 수 있도록 계정에 이메일 주소 그리고 전화번호를 추가하세요. +\n +\n이메일 주소로 비밀번호를 재 설정할 수도 있습니다. + 사용자 이름은 문자, 숫자, 점, 하이픈 및 밑줄만 포함할 수 있습니다 + 비밀번호가 누락되었습니다 + 이 이메일 주소는 이미 정의되었습니다. + 이메일 주소가 누락되었습니다 + 전화번호가 누락되었습니다 + 이메일 주소나 전화번호가 누락되었습니다 + 옳지 않은 토큰 + 맞춤 서버 옵션을 사용하기 (고급) + 등록을 계속하려면 이메일을 확인하세요 + API가 존재하기 전까지는 이메일과 전화번호로 한 번에 등록할 수 없습니다. 오직 전화번호만 고려됩니다. +\n +\n설정에서 프로필에 이메일을 추가할 수 있습니다. + 이 홈서버는 당신이 로봇인지 아닌 지를 확인하고 싶습니다 + ID 서버: + 제 이메일 주소를 인증했습니다 + 계정에 연결된 이메일 주소를 입력해야 합니다. + 이메일 %s(으)로 전송했습니다. 이메일에 포함된 링크를 들어갔다면, 아래를 클릭해주세요. + 이메일 주소를 인증할 수 없습니다: 이메일에 있는 링크를 클릭했는 지 확인하세요 + 이 홈서버의 규칙을 숙지한 후 수락하세요: + + URL은 http[s]://로 시작해야 합니다 + 로그인할 수 없음: 네트워크 오류 + 로그인할 수 없음 + 등록할 수 없음: 네트워크 오류 + 등록할 수 없음 + 등록할 수 없음: 이메일 인증 실패 + 올바른 URL을 입력하세요 + 이 URL에 연결할 수 없습니다, 확인해주세요 + 기기가 오래된 TLS 보안 프로토콜을 사용하고 있습니다, 공격에 취약하며, 보안 상의 이유로 연결할 수 없습니다 + 옳지 않은 사용자 이름/비밀번호 + 지정된 액세스 토큰을 인식할 수 없습니다 + 잘못된 JSON + 올바른 JSON을 갖고 있지 않음 + 너무 많은 요청을 보냈습니다 + 이 사용자 이름은 이미 사용 중입니다 + 이메일 링크를 아직 클릭하지 않았습니다 + + 이 기기를 위한 종단간 암호화 키를 생성하고 공개 키를 홈서버에 제출하려면 다시 로그인해야 합니다. +\n한 번이면 됩니다. +\n불편을 드려 죄송합니다. + + 다른 기기에서 온 다시 요청된 암호 키. + + 키 요청을 보냈습니다. + + 요청 보냄 + 다른 기기에서 Riot을 설치해서 메시지를 암호화하고 이 기기로 키를 보내도록 합니다. + + 읽은 기록 읽기 + + 그룹 목록 + + + %d 구성원십 변경 + + + 이렇게 보내기 + 원본 + 크게 + 중간 + 작게 + + 다운로드를 취소하겠습니까\? + 업로드를 취소하겠습니까\? + %d초 + %1$d분 %2$d초 + + 어제 + 오늘 + + 방 이름 + 방 주제 + + 전화 + 오는 전화에 Riot 기본 벨소리를 사용합니다 + 오는 전화 벨소리 + 전화에 사용할 벨소리를 선택하세요: + + 전화 + 전화 연결됨 + 전화 연결 중… + 전화 종료됨 + 전화 중… + 오는 전화 + 오는 영상 통화 + 오는 음성 통화 + 전화 진행 중… + 영상 통화 진행 중… + + 상대방이 전화를 받지 못했습니다. + 미디어 연결 실패 + 카메라를 초기화할 수 없습니다 + 다른 곳에서 전화 응답 + + 사진이나 영상 찍기 + 영상을 촬영할 수 없음 + + 정보 + 첨부 파일을 보내고 저장하려면 Riot은 영상과 사진 보관함에 접근하는 권한이 필요합니다. +\n +\n당신의 휴대 전화에서 파일을 보내려면 다음 팝업에서 접근을 허용해주세요. + 사진을 찍고 영상 통화를 하려면 Riot은 카메라에 접근하는 권한이 필요합니다. + " +\n +\n전화를 하려면 다음 팝업에서 접근을 허용해주세요." + 음성 통화를 하려면 Riot은 마이크에 접근하는 권한이 필요합니다. + " +\n +\n전화를 하려면 다은 팝업에서 접근을 허용하세요." + 영상 통화를 하려면 Riot은 카메라와 마이크에 접근하는 권한이 필요합니다. +\n +\n전화를 하려면 다음 팝업에서 접근을 허용해주세요. + Riot은 당신의 주소록을 확인해서 이메일과 전화번호를 기반으로 다른 Matrix 사용자를 찾을 수 있습니다. 이런 이유로 주소록을 공유하는 것을 허용한다면, 다음 팝업에서 접근을 허용해주세요. + "Riot은 당신의 주소록을 확인하여 이메일과 전화번호를 기반으로 다른 Matrix 사용자를 찾을 수 있습니다. +\n +\n이런 이유로 주소록을 공유하는 것을 허용하겠습니까\?" + + 죄송합니다. 권한이 없어서, 작업이 수행되지 않았습니다 + + 저장됨 + 다운로드 폴더에 저장하겠습니까\? + + 아니오 + 계속 + + 삭제 + 참가 + 미리보기 + 받지 않기 + + 구성원 목록 + 헤더 열기 + 동기화 중… + 읽지 않은 첫 부분으로 이동하기. + + %s님이 이 방알 초대했습니다 + 이 초대장은 %s님이 보냈습니다, 이 계정과는 관련이 없습니다. +\n다른 계정으로 로그인하거나, 이 이메일을 계정에 추가할 수 있습니다. + %s 방으로 접근하려고 했습니다. 토론에 참여하기 위해 참가하는 것입니까\? + + 이 방의 미리보기입니다. 방 상호작용이 비활성화됩니다. + + 새 대화 + 구성원 추가 + + %d명의 활동 중인 구성원 + + + %d명의 구성원 + + 1명의 구성원 + + + %d초 + + + %d분 + + + %d시 + + + %d일 + + + 방 떠나기 + 방을 떠나겠습니까\? + 이 대화에서 %s을(를) 삭제하겠습니까\? + 만들기 + + 온라인 + 미접속 + 휴식 + %1$s 상태 + %2$s 전 %1$s 상태였음 + + 관리자 도구 + 전화 + 다이렉트 대화 + 기기 + + 초대 + 이 방 떠나기 + 이 방에서 삭제하기 + 차단 + 차단 해제 + 추방 + 일반 사용자로 재 설정 + 중재자 만들기 + 관리자 만들기 + 이 사용자의 모든 메시지 숨기기 + 이 사용자의 모든 메시지 보여주기 + 이 사용자의 모든 메시지를 보이겠습니까\? +\n +\n이 동작은 앱을 재시작하며 일정 시간이 걸릴 수 있습니다. + 사용자 ID, 이름 혹은 이메일 + 언급 + 기기 목록 보여주기 + 사용자를 자신과 동일한 권한 등급으로 승격시키는 것은 취소할 수 없습니다. +\n확신합니까\? + + + 이 사용자들을 이 대화에서 추방하겠습니까\? + + 이 사용자를 이 대화에서 차단하겠습니까\? + 이유 + + %s님을 이 대화에 초대하겠습니까\? + "%1$s님, " + %1$s님과 %2$s님 + %1$s %2$s님 + + ID로 초대 + 로컬 연락처 (%d) + 사용자 디렉토리 (%s) + Matrix 사용자만 + + ID로 사용자 초대 + 이메일 주소나 Matrix ID를 입력해주세요 + 이메일 혹은 Matrix ID + + 검색 + %s님이 입력 중… + %1$s님과 %2$s님이 입력 중… + %1$s님과 %2$s님 외 구성원들이 입력 중… + 암호화된 메시지 보내기… + (암호화 안 된) 메시지 보내기… + 암호화된 답장 보내기… + (암호화 안 된) 답장 보내기… + 서버와의 연결이 끊어졌습니다. + 메시지가 보내지지 않았습니다. %1$s, %2$s, 둘 중 어떤 것을 하겠습니까\? + 알 수 없는 기기가 있어 메시지가 보내지지 않았습니다. %1$s, %2$s, 둘 중 어느 것을 하겠습니까\? + 모두 다시 보내기 + 모두 취소하기 + 보내지지 않은 메시지 다시 보내기 + 보내지지 않은 메시지 삭제하기 + 파일을 찾을 수 없음 + 이 방에 글을 올릴 권한이 없습니다 + + %d개의 새 메시지 + + + 신뢰함 + 신뢰하지 않음 + 로그아웃 + 무시하기 + 핑거프린트 (%s): + 원격 서버의 신원을 인증할 수 없습니다. + 이는 누군가가 당신의 트래픽을 악의적으로 가로채고 있거나, 휴대 전화가 원격 서버에서 제공한 인증서를 신뢰하지 않는 것입니다. + 서버 관리자가 이것이 예상된다고 말한다면, 아래 핑거프린트가 해당 핑거프린트와 일치하는 지 확인하세요. + 인증서가 휴대 전화가 신뢰하는 인증서에서 변경되었습니다. 이것은 매우 비정상적입니다. 새 인증서에 수락하지 않는 것을 권합니다. + 이전 신뢰하던 인증서에서 신뢰하지 않는 인증서로 변경되었습니다. 서버가 인증서를 새로 갱신했을 수 있습니다. 예상되는 핑거프린트는 서버 관리자에게 문의하세요. + 서버 관리자가 위의 핑거프린트와 일치하는 것을 게시하는 경우에만 인증서를 수락할 수 있습니다. + + 방 세부 사항 + 사람 + 파일 + 설정 + + %d개 선택됨 + + 잘못된 ID입니다. 이메일 주소나 \'@localpart:domain\'와 같은 Matrix ID이어야 합니다 + 초대받음 + 참가함 + + 이 콘텐츠를 보고하는 이유 + 이 사용자의 모든 메시지를 숨기겠습니까\? +\n +\n이 동작은 앱을 다시 시작하며 일정 시간이 걸릴 수 있습니다. + 업로드 취소 + 다운로드 취소 + + 검색 + 방 구성원 필터 + 결과 없음 + + 메시지 + 사람 + 파일 + + 참가 + 디렉토리 + 즐겨찾기 + + 중요하지 않음 + 초대 + 대화 시작 + 방 만들기 + 방 참가하기 + 방 참가하기 + 방 ID나 방 별칭을 입력 + + 디렉토리 찾기 + + %d개의 방 + + + %2$s 검색 결과로 %1$s개의 방을 찾음 + + 디렉토리 검색 중… + + 모든 메시지 (소리) + 모든 메시지 + 언급만 + 무음 + 즐겨찾기 + 중요하지 않음 + 다이렉트 대화 + 대화 떠나기 + 잊기 + 홈 화면에 단축 아이콘 추가 + + 메시지 + 설정 + 버전 + 버전 %s + 이용 약관 + 제 3자 공지 + 저작권 + 개인 정보 정책 + + 프로필 사진 + 표시 이름 + 이메일 + 이메일 주소 추가 + 휴대 전화 + 전화번호 추가 + 애플리케이션 정보 + 시스템 설정에서 애플리케이션 정보를 표시하세요. + + + 고급 알림 설정 + 이벤트 별로 알림 중요도를 설정하고, 소리, LED, 진동을 설정하세요 + 이벤트 별 알림 중요도 + + 알림 개인 정보 + 알림 문제 해결 + 문제 해결 진단 + 테스트 실행 + 실행 중… (%2$d개 중 %1$d개 째) + 기본 진단은 괜찮습니다. 여전히 알림을 받지 못하고 있다면, 버그를 보고해서 우리가 조사할 수 있도록 도와주세요. + 1개 이상의 테스트가 실패했습니다, 제안된 수정을 시도하세요. + 1개 이상의 테스트가 실패했습니다, 버그를 보고해서 우리가 조사할 수 있도록 도와주세요. + + 시스템 설정. + 알림이 시스템 설정에서 켜집니다. + 알림이 시스템 설정에서 꺼집니다. +\n시스템 설정을 확인해주세요. + 설정 열기 + + 계정 설정. + 알림이 당신의 계정에서 켜집니다. + 알림이 당신의 계정에서 꺼집니다. +\n계정 설정을 확인해주세요. + 켜기 + + 기기 설정. + 알림이 이 기기에서 켜집니다. + 알림이 이 기기에서 허용되지 않습니다. +\nRiot 설정을 확인해주세요. + 켜기 + + 맞춤 설정. + 일부 메시지 유형은 조용하게 설정되어 있습니다 (소리가 없는 알림을 생성합니다). + 일부 알림이 맞춤 설정에서 꺼집니다. + 맞춤 규칙을 불러오는 데 실패했습니다, 다시 시도해주세요. + 설정 확인 + + Play 서비스 확인 + Google Play 서비스 APK는 최신 버전입니다. + Riot은 Google Play 서비스를 사용해 푸시 메시지를 보내지만 올바르게 설정되지 않은 모양입니다: +\n%1$s + Play 서비스 고치기 + + Firebase 토큰 + FCM 토큰이 성공적으로 검색되었습니다: +\n%1$s + FCM 토큰을 검색하는데 실패했습니다: +\n%1$s + [%1$s] +\n이 오류는 Riot의 통제 밖에 있으며 Google과 관련이 있습니다, 이 오류는 기기가 FCM에 등록된 앱이 너무 많다는 것을 나타냅니다. 오류는 수 많은 앱이 있는 경우에만 발생하고, 일반 사용자에게 영향을 미치지 않아야 합니다. + [%1$s] +\n이 오류는 Riot의 통제 밖입니다. 여러 이유로 발생할 수 있습니다. 나중에 다시 시도하면 작동할 지도 모릅니다, 시스템 설정에서 Google Play 서비스의 데이터 사용이 제한되었는지, 기기의 시간은 맞는 지 확인해보세요, 혹은 커스텀 롬 환경에서 발생할 수 있습니다. + [%1$s] +\n이 오류는 Riot의 통제 밖에 있습니다. 휴대 전화에 Google 계정이 없습니다. 계정 관리자를 열어 Google 계정을 추가하세요. + 계정 추가 + + 토큰 등록 + FCM 토큰이 성공적으로 홈서버에 등록되었습니다. + FCM 토큰을 홈서버에 등록 실패: +\n%1$s + + 알림 서비스 + 알림 서비스가 실행 중입니다. + 알림 서비스가 실행 중이 아닙니다. +\n애플리케이션을 다시 실행해보세요. + 서비스 시작 + + 알림 서비스 자동 다시 시작 + 서비스가 종료되고 자동으로 다시 시작되었습니다. + 서비스 다시 시작에 실패함 + + 부팅 시 시작 + 기기가 다시 시작되면 서비스가 시작됩니다. + 기기가 다시 시작될 때 서비스가 시작되지 않습니다, 다시 시작한 후 Riot을 한 번이라도 열지 않으면 알림을 받을 수 없습니다. + 부팅 시 시작 활성화 + + 백그라운드 제한 사항 확인 + Riot에 대한 백그라운드 제한 사항을 비활성화합니다. 이 테스트는 모바일 데이터를 사용해야 합니다 (WIFI 없음). +\n%1$s + Riot에 대한 백그라운드 제한 사항이 활성화됩니다. +\n앱이 백그라운드에서 작업하는 동안 앱이 시도하는 작업은 적극적으로 제한되며, 이는 알림에 영향을 줄 수 있습니다. +\n%1$s + 제한 사항 비활성화 + + 배터리 최적화 + Riot은 배터리 최적화의 영향을 받지 않습니다. + 사용자가 기기 화면을 끈 상태로 일정 시간 동안 연결되지 않은 상태로 두면, 기기는 Doze 모드에 들어갑니다. 이렇게 하면 앱이 네트워크에 접근하지 못하고 작업, 동기화 및 표준 경보가 지연됩니다. + 최적화 무시하기 + + 보통 + 감소된 개인 정보 보호 + 앱을 백그라운드에서 실행하려면 권한이 필요합니다 + 앱은 백그라운드에서 홈서버로 연결할 필요가 없고, 이는 배터리 사용량을 줄입니다 + • 알림은 Firebase 클라우드 메시징을 통해 보내집니다 + • 알림은 오직 메타데이터만 갖습니다 + • 알림의 메시지 내용은 Matrix 홈서버에서 안전하게 직접 배치됩니다 + • 알림은 메타데이터와 메시지 데이터를 갖습니다 + • 알림은 메시지 내용을 보여주지 않습니다 + + 알림 소리 + 이 계정에서 알림 켜기 + 이 기기에서 알림 켜기 + 3초 동안 화면을 켬 + 소리 알림 설정 + 전화 알림 설정 + 조용한 알림 설정 + LED 색상, 진동, 소리를 고르세요… + + + 내 표시 이름이 들어있는 메시지 + 내 사용자 이름이 들어있는 메시지 + 1:1 대화 메시지 + 그룹 대화 메시지 + 방에 초대받았을 때 + 전화 초대 + 봇에게 받은 메시지 + + 백그라운드 동기화 + 부팅 시 시작 + 백그라운드 동기화 켜기 + 동기화 요청 시간 초과 + 각 요청 간 딜레이 + + + + 버전 + olm 버전 + 이용 약관 + 제 3자 공지 + 저작권 + 개인 정보 정책 + 미디어 유지 + 캐시 지우기 + 미디어 캐시 지우기 + + 사용자 설정 + 알림 + 무시한 사용자 + 기타 + 고급 + 암호화 + 암호화 키 관리 + 알림 대상 + 로컬 주소록 + 주소록 권한 + 국제전화 나라 번호 + 홈 표시 + 알림을 놓친 방을 고정 + 읽지 않은 메시지가 있는 방 고정 + 기기 + 인라인 URL 미리보기 + 홈서버가 이 기능을 지원한다면 대화 내에서 링크를 미리 볼 수 있습니다. + 입력 중 알림 보내기 + 다른 사용자가 당신이 입력 중인 것을 알게 합니다. + 마크다운 형식 + 마크다운 문법을 사용해 메시지를 보내기 전에 형식을 지정합니다. 별표를 사용해 기울임 꼴 문자를 표시하는 고급 서식을 지정할 수 있습니다. + 모든 메시지에 타임스탬프 보여주기 + 12시간 단위를 사용하는 타임스탬프 보여주기 + 읽은 기록 보여주기 + 세부적인 목록으로 읽은 목록을 클릭하세요. + 참가 및 떠남 이벤트 보여주기 + 초대, 추방, 그리고 차단은 영향이 없습니다. + 계정 이벤트 보여주기 + 아바타와 표시 이름 변경도 포함합니다. + 사용자가 언급할 때 진동 + 보내기 전 미디어 미리보기 + 엔터 키로 메시지 보내기 + 가상 키보드의 엔터 버튼으로 줄 바꿈을 하는 대신 메시지를 보냅니다 + + 계정 비활성화 + 내 계정 비활성화 + + 알림 개인 정보 + Riot은 백그라운드에서 실행되어 알림을 안전하고 은밀하게 관리할 수 있습니다. 이것은 배터리 사용량에 영향을 줄 수 있습니다. + 권한 부여 + 다른 옵션을 선택하세요 + + 백그라운드 연결 + Riot은 신뢰가 있는 알림을 위해 낮은 영향의 백그라운드 연결을 유지해야 합니다. +\n다른 화면에서 Riot이 항상 백그라운드에서 실행하도록 허용하는 메시지가 표시됩니다, 수락해주세요. + 권한 부여 + + 정보 분석 + 정보 분석 데이터 보내기 + Riot은 애플리케이션을 개선할 수 있도록 익명의 분석을 수집합니다. + 분석을 활성화해서 Riot이 개선할 수 있도록 도와주세요. + 예, 저도 돕고 싶습니다! + + 데이터 절약 모드 + 데이터 절약 모드는 특정 필터를 적용하여 현재 상태 업데이트와 입력 중 알림을 걸러냅니다. + + 기기 세부 사항 + ID + 이름 + 기기 이름 + 마지막으로 본 순간 + %1$s @ %2$s + 이 작업은 추가 인증이 필요합니다. +\n계속하려면, 비밀번호를 입력하세요. + 인증 + 비밀번호: + 제출하기 + + 이것으로 로그인 + 홈서버 + ID 서버 + 통합 관리자 + + 사용자 인터페이스 + 언어 + 언어를 선택하세요 + + 인증 보류 중 + 이메일을 인증해서 거기에 있는 링크를 클릭하세요. 모두 끝나면, 계속하기를 클릭하세요. + 이메일 주소를 인증할 수 없습니다. 이메일을 인증해서 거기에 있는 링크를 클릭하세요. 모두 끝나면, 계속하기를 클릭하세요. + 이 이메일 주소는 이미 사용 중입니다. + 이 이메일 주소를 찾을 수 없습니다. + 이 전화번호는 이미 사용 중입니다. + 이메일 주소를 인증하는 중 오류가 발생했습니다. + + 비밀번호 + 비밀번호 변경 + 현재 비밀번호 + 새 비밀번호 + 새 비밀번호 확인 + 비밀번호 갱신 + 비밀번호 갱신 실패 + 비밀번호가 올바르지 않습니다 + 당신의 비밀번호가 갱신되었습니다 + %s님의 모든 메시지를 보이겠습니까\? +\n +\n이 동작은 앱을 다시 시작하고 일정 시간이 걸릴 수 있습니다. + 비밀번호가 맞지 않음 + + 이 알림 대상을 제거하겠습니까\? + + %1$s %2$s를 제거하겠습니까\? + + 나라를 선택하세요 + + 나라 + 나라를 선택해주세요 + 전화번호 + 선택한 나라에 올바르지 않은 전화번호 + 휴대 전화 인증 + 활성 코드가 담신 SMS를 보냈습니다. 코드를 아래에 입력하세요. + 활성 코드 입력 + 전화번호를 인증하는 중 오류 + 코드 + 전화번호를 인증하는 중 오류가 발생했습니다. + 추가 정보: %s + + 미디어 + 기본 압축 + 선택 + 기본 미디어 소스 + 선택 + 셔터 소리 재생하기 + + 재능 + 현재는 어떤 커뮤니티의 구성원이 아닙니다. + + 3일 + 1주 + 1달 + 영원히 + + 방 사진 + 방 이름 + 주제 + 방 태그 + 이것으로 태그됨: + + 즐겨찾기 + 중요하지 않음 + 없음 + + 접근성 및 가시성 + 이 방을 방 디렉토리에 놓기 + 알림 + 방 접근성 + 방 기록 읽기 권한 + 누가 기록을 읽을 수 있나요\? + 누가 이 방에 접근할 수 있나요\? + + 누구나 + (이 옵션을 선택한 시점부터) 구성원만 + (초대받은 시점부터) 구성원만 + (참가한 시점부터) 구성원만 + + 이 방에 링크를 타고 오려면 주소가 있어야 합니다. + 초대받은 사람들만 + 방의 링크를 아는 누구나, 손님 제외 + 방의 링크를 아는 누구나, 손님 포함 + + 차단한 사용자 + + 고급 + 이 방의 내부 ID + 주소 + 실험실 + 예기치 않은 방식으로 중단될 수 있는 실험적인 기능들입니다. 주의해서 사용하세요. + 종단간 암호화 + 종단간 암호화가 켜졌습니다 + 암호화를 켜기 위해 로그아웃을 해야 합니다. + 인증된 기기로만 암호화 + 이 기기로는 방에서 인증되지 않은 기기로 암호화된 메시지를 절대 보내지 마세요. + + 이 방은 로컬 주소가 없습니다 + 새 주소 (예: #foo:matrix.org) + + 이 방은 어떤 커뮤니티에도 재능을 표시하지 않습니다 + 새 커뮤니니 ID (예: +foo:matrix.org) + 올바르지 않은 커뮤니티 ID + \'%s\'은(는) 올바르지 않은 커뮤니티 ID입니다 + + + 올바르지 않은 별칭 형식 + \'%s\'은(는) 별칭에 올바르지 않은 형식입니다 + 이 방에 지정된 메인 주소가 없습니다. + 메인 주소 경고 + + 메인 주소로 설정 + 메인 주소로 설정 해제 + 방 ID 복사 + 방 주소 복사 + + 이 방에서 암호화가 켜졌습니다. + 이 방에서 암호화가 꺼졌습니다. + 암호화 활성화 +\n(경고: 다시 비활성화할 수 없음!) + + 디렉토리 + 테마 + + %s님이 이 방의 특정 지점을 불러오려 했으나 찾을 수 없었습니다. + + 종단간 암호화 정보 + + 이벤트 정보 + 사용자 ID + Curve25519 ID 키 + 청구된 Ed25519 핑거프린트 키 + 알고리즘 + 세션 ID + 암호 해독 오류 + + 발신자 기기 정보 + 기기 이름 + 이름 + 기기 ID + 기기 키 + 인증 + Ed25519 핑거프린트 + + 종단간 암호화 방 키 내보내기 + 방 키 내보내기 + 로컬 파일로 키 내보내기 + 내보내기 + 내보낸 키를 암호화하는 암호를 만드세요. 키를 가져오려면 같은 암호를 입력해야 합니다. + 종단간 암호화 방 키가 \'%s\'에 저장되었습니다. +\n +\n경고: 애플리케이션을 삭제하면 이 파일도 삭제됩니다. + + 암호화된 메시지 복구 + 키 백업 관리 + + 종단간 암호화 방 키 가져오기 + 방 키 가져오기 + 로컬 파일에서 키 가져오기 + 가져오기 + 인증된 기기로만 암호화 + 이 기기에서 인증되지 않은 기기로 절대 암호화된 메시지를 보내기 마세요. + %1$d/%2$d 키를 성공적으로 가져왔습니다. + + 인증되지 않음 + 인증됨 + 블랙리스트 대상 + + 알 수 없는 기기 + 알 수 없는 ip + 없음 + + 인증 + 인증하지 않음 + 블랙리스트 + 블랙리스트 제외 + + 인증 기기 + 이 기기가 신뢰할 수 있는 지 인증하려면, 다른 방법을 사용하려 소유자에게 문의하세요 (예: 현실에서 혹은 전화로) 그리고 이 기기의 사용자 설정에서 표시된 키가 아래에 있는 키와 맞는지 물어보세요: + 그것이 맞다면, 아래의 인증 버튼을 누르세요. 맞지 않다면, 다른 사람이 이 기기를 가로채고 있는 것이고 블랙리스트에 올려야 합니다. 앞으로 이 인증 절차는 더 정교해질 것입니다. + 키가 맞다는 것은 인증합니다 + + Riot은 이제 종단간 암호화를 지원하지만 활성화하려면 다시 로그인해야 합니다. +\n +\n지금 바로 하거나 나중에 애플리케이션 설정에서 할 수 있습니다. + + 알 수 없는 기기가 있는 방 + 이 방에는 인증되지 않은 알 수 없는 기기가 있습니다. +\n즉, 사용자에 속해 있다고 주장하는 기기라는 보장이 없습니다. +\n저희는 계속하기 전에 각 기기에 인증 절차를 하기를 권합니다, 하지만 원한다면 인증하지 않고 바로 메시지를 보낼 수 있습니다. +\n +\n알 수 없는 기기: + + 방 디렉토리 선택 + 서버를 이용할 수 없거나 과부하 상태입니다 + 공개 서버를 표시할 홈서버를 입력하세요 + 홈서버 URL + %s 서버의 모든 방 + 모든 기본 %s 방 + + 여기에 입력… + + + %d개의 읽지 않은 알림 메시지 + + + %d개의 읽지 않은 알림 메시지 + + + %d개의 방 + + + + %1$s: %2$d개의 메시지 + + + %d개의 알림 + + + %2$s에서 %1$s님 + 새 이벤트 + + 새 메시지 + 새 초대 + + ** 보내기 실패 - 방을 열어주세요 + + 기록 검색 + + 글씨 크기 + 매우 작게 + 작게 + 보통 + 크게 + 더 크게 + 매우 크게 + 가장 크게 + + 이 방에서 위젯을 다루려면 권한이 있어야 합니다 + 위젯 생성 실패 + %1$s을(를) %2$s님이 추가함 + %1$s을(를) %2$s님이 삭제함 + Jitsi로 회의 전화 만들기 + 이 방에서 위젯을 삭제하겠습니까\? + + %d개의 활성 위젯 + + + 죄송합니다, Jitsi로 회의 전화는 오래된 기기에서 지원하지 않습니다 (안드로이드 OS가 5.0 이하인 기기) + + 위젯을 만들 수 없습니다. + 요청을 보낼 수 없습니다. + 권한 등급은 양의 정수이어야 합니다. + 당신은 이 방에 등록되지 않았습니다. + 이 방에서 해당 작업을 할 권한이 없습니다. + 요청에 room_id가 없습니다. + 요청에 user_id가 없습니다. + 방 %s이(가) 보이지 않습니다. + 필수 매개 변수가 없습니다. + 매개 변수가 올바르지 않습니다. + 통합 관리자가 설정되지 않았습니다. + Matrix 앱 추가 + 기본 카메라 사용 + 맞춤 카메라 화면 대신 시스템 카메라를 실행합니다. + 키보드 엔터 키로 메시지 보내기 + 음성 메시지 보내기 + 이 옵션은 메시지를 기록하기 위한 제 3자 애플리케이션이 필요합니다. + + 새 기기 \'%s\'을(를) 추가했습니다, 여기에는 암호화 키가 필요합니다. + 새 기기에는 암호화 키가 필요합니다. +\n기기 이름: %1$s +\n마지막으로 본 순간: %2$s +\n다른 기기에서 로그인하지 않았다면, 이 요청을 무시하세요. + 인증되지 않은 기기 \'%s\'가 암호화 키를 요청했습니다. + 인증되지 않은 기기가 암호화 키를 요청했습니다. +\n기기 이름: %1$s +\n마지막으로 본 순간: %2$s +\n다른 기기에서 로그인하지 않았다면, 이 요청을 무시하세요. + + 인증 시작 + 인증 + 인증 없이 공유 + 공유 + 키 공유 요청 + 요청 무시하기 + 무시 + + 경고! + 회의 전화는 개발 중이며 신뢰할 수 없을 수 있습니다. + + 명령어 오류 + 인식할 수 없는 명령어: %s + \"%s\" 명령어는 더 많은 매개 변수가 필요하거나, 일부 매개 변수가 옳지 않습니다. + 동작 표시하기 + 주어진 ID로 사용자 차단하기 + 주어진 ID로 사용자 차단 해제하기 + 사용자의 권한 등급 정의하기 + 주어진 ID로 사용자 강등하기 + 주어진 ID 현재 방에 사용자 초대하기 + 주어진 별칭으로 방에 참가하기 + 방 떠나기 + 방 주제 설정하기 + 주어진 ID로 사용자 추방하기 + 표시 별명 바꾸기 + 마크다운 On/Off + Matrix 앱 관리 문제를 해결하려면 + + 마크다운이 켜졌습니다. + 마크다운이 꺼졌습니다. + + + 조용히 + 소리 + + 암호화된 메시지 + + 만들기 + 커뮤니티 만들기 + 커뮤니티 이름 + 예시 + 커뮤니티 ID + 예시 + + + 사람 + + 사용자 없음 + + + 참가함 + 초대받음 + 구성원 그룹 필터 + 방 그룹 필터 + + + %d명의 구성원 + + + + %d개의 방 + + 커뮤니티 관리자가 이 커뮤니티에 대한 자세한 설명을 제공하지 않았습니다. + + %2$s님에 의해 %1$s에서 추방당했습니다 + %2$s님에 의해 %1$s에서 차단됬습니다 + 이유: %1$s + 다시 참가하기 + 방 잊어버리기 + + 아바타 메모 + 아바타 공지 + 아바타 + + %1$s 홈서버를 계속 사용하려면 이용 약관을 검토하고 승인해야 합니다. + 지금 검토하기 + + 계정 비활성화 + 이것으로 계정은 영구적으로 사용할 수 없게 됩니다. 로그인할 수 없고 누구도 같은 사용자 ID로 다시 가입할 수 없게 됩니다. 이 계정으로 참가한 모든 방에서 떠나게 되고, ID 서버의 계정 세부 사항도 삭제됩니다. 이 행동은 돌이킬 수 없습니다. +\n +\n계정을 비활성화해도 기본적으로 보낸 메시지를 잊지 않습니다. 메시지를 잊기를 원한다면, 아래 상자를 선택하세요. +\n +\nMatrix의 메시지 가시성은 이메일과 유사합니다. 우리가 메시지를 잊는 것은 보낸 메시지가 모든 새 사용자 혹은 등록하지 않은 사용자와 공유하지 않는다는 것입니다, 하지만 이 메시지에 접근한 등록된 사용자는 이 사본으로 여전히 접근할 수 있을 것입니다. + 내 계정을 비활성화하면 내가 보낸 모든 메시지는 잊어주세요 (경고: 이것은 미래 사용자가 불완전한 대화를 읽게 됩니다) + 계속하려면, 비밀번호를 입력하세요: + 계정 비활성화 + + 사용자 이름을 입력하세요. + 비밀번호를 입력하세요. + 이 방은 교체되었으며 더 이상 활동하지 않습니다 + 대화는 여기서 계속됩니다 + 이 방은 다른 대화의 연장선입니다 + 오래된 메시지를 보려면 여기를 클릭 + + 리소스 한도 초과됨 + 관리자 연락 + + 서비스 관리자에게 연락 + + 이 홈서버가 리소스 한도를 초과해서 일부 사용자는 로그인할 수 없습니다. + 이 홈서버가 리소스 한도를 초과했습니다. + + 이 홈서버가 월 간 활성 사용자 한도를 초과해서 일부 사용자는 로그인할 수 없습니다. + 이 홈서버가 월 간 사용자 한도를 초과했습니다. + + 한도를 높이려면 %s하세요. + 이 서비스 사용을 계속하려면 %s하세요. + + 방 구성원 불러오기 지연 + 첫 화면에서 방 멤버만 불러옴으로써 퍼포먼스를 향상시킵니다. + 아직 홈서버가 방 구성원 불러오기 지연을 지원하지 않습니다. 나중에 시도하세요. + + 죄송합니다, 오류가 발생했습니다 + + 펼치기 + 접기 + + 정보 영역 보여주기 + 항상 + 메시지와 오류일 시 + 오류만 + + %1$s: + %1$s: %2$s + +%d + %d+ + 올바른 Google Play 서비스 APK를 찾을 수 없습니다. 알림이 제대로 작동하지 않을 수 있습니다. + + 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분실하지 않도록 키를 안전하게 백업해두세요. + 키 백업 시작 + (고급) + 수동으로 키 내보내기 + + 암호로 백업을 보호하세요. + 홈서버에 암호화된 키의 사본을 저장합니다. 보안을 유지하기 위해 암호로 백업을 보호하세요. +\n +\n보안을 최대한 끌어내려면, 암호는 계정 비밀번호와 다른 것이 좋습니다. + 암호 설정 + 백업 만들기 + 혹은 복구 키로 백업을 보호하여 안전한 곳에 보관하세요. + (고급) 복구 키와 함께 설정 + 성공 ! + 당신의 키가 백업됬습니다. + 복구 키는 안전망입니다 - 암호를 잊어버린다면 이걸로 암호화된 메시지에 대한 접근을 복구할 수 있습니다. +\n복구 키가 비밀번호 관리자 (혹은 금고)같은 어딘가에 매우 안전하게 있도록 유지하세요 + 복구 키가 비밀번호 관리자 (혹은 금고)같은 어딘가에 매우 안전하게 있도록 유지하세요 + 완료 + 사본을 만들었습니다 + 복구 키 저장 + 공유 + 파일로 저장 + 복구 키가 \'%s\'(으)로 저장되었습니다. +\n +\n경고: 이 파일은 애플리케이션이 삭제되면 함께 삭제됩니다. + + 백업이 이미 홈서버에 존재합니다 + 이미 키 백업을 다른 기기에 설정한 모양입니다. 만들고 있는 것으로 바꾸겠습니까\? + 바꾸기 + 멈추기 + + 사본을 만들어 주세요 + 복구 키 공유… + 암호를 사용해 복구 키를 생성하는 것은 시간이 걸릴 수 있습니다. + 복구 키 + 예기치 않은 오류 + 백업 시작함 + 이제 암호화 키가 백그라운드에서 홈서버로 백업됩니다. 처음 백업에는 몇 분이 걸릴 수 있습니다. + + + 확신합니까\? + 로그아웃을 하거나 이 기기를 잃어버리면 메시지에 접근할 수 없게 될지도 모릅니다. + + 백업 버전 가져오는 중… + 복구 암호를 사용해서 암호화된 메시지 기록을 풀기 + 복구 키를 사용 + 복구 암호가 기억나지 않는다면, %s할 수 있습니다. + + 복구 키를 사용해서 암호화된 메시지 기록을 풀기 + 복구 키 입력 + + 메시지 복구 + + 복구 키를 잃어버렸나요\? 설정에서 새로운 키를 만들 수 있습니다. + 이 암호로 백업을 해독할 수 없습니다: 올바른 복구 암호를 입력해서 인증해주세요. + 네트워크 오류: 인터넷 연결 상태를 확인하고 다시 시도해주세요. + + 백업 복구: + 복구 키 계산 중… + 키 다운로드 중… + 키 가져오는 중… + 기록 풀기 + 복구 키를 입력하세요 + 이 복구 키로 백업을 해독할 수 없습니다: 올바른 복구 키를 입력해서 인증하세요. + + 백업이 복구되었습니다 %s ! + %1$d개의 세션 키를 복구했고, 이 기기에서 알려지지 않은 %2$d개의 새 키를 추가함 + + %d개의 키와 함께 백업을 복구했습니다. + + + %d개의 새 키가 이 기기에 추가되었습니다. + + + 최신 복구 키 버전을 가져오는 데 실패했습니다 (%s). + 세션 암호화가 활성화되지 않았습니다 + + + 백업에서 복구 + 백업 삭제 + + 이 기기를 위한 키 백업이 올바르게 설정되었습니다. + 이 기기에서 키 백업이 활성화되지 않았습니다. + 이 기기에서 키가 아직 백업되지 않았습니다. + + 백업이 ID %s의 알 수 없는 기기의 서명이 있습니다. + 백업이 이 기기의 올바른 서명이 있습니다. + 백업이 인증된 기기 %s의 올바른 서명이 있습니다. + 백업이 인증되지 않은 기기 %s의 올바른 서명이 있습니다 + 백업이 인증된 기기 %s의 올바르지 않은 서명이 있습니다 + 백업이 인증되지 않은 기기 %s의 올바르지 않은 서명이 있습니다 + 백업에 대한 신뢰 정보를 주지 못했습니다 (%s). + + 이 기기에서 키 백업을 사용하려면, 지금 암호나 복구 키를 복구하세요. + 백업 삭제 중… + 백업 삭제에 실패함 (%s) + + 백업 상태 확인하기 + 백업 삭제 + 서버에서 백업한 암호화 키를 삭제하겠습니까\? 더 이상 복구 키를 사용해 암호화된 메시지 기록을 읽을 수 없습니다. + + 새 키 백업 + 새 보안 메시지 키 백업이 삭제되었습니다. +\n +\n새 복구 방법을 설정하지 않으면, 공격하는 사람이 계정에 접근하려 할 수도 있습니다. 설정에 가서 계정 비밀번호를 바꾸고 새로운 복구 방법을 설정하세요. + 접니다 + 암호화된 메시지를 잃지 마세요 + 키 백업 시작 + + 암호화된 메시지를 잃지 마세요 + 키 백업하기 + + 새 암호화된 메시지 키 + 키 백업에서 관리 + + 키 백업 중… + + 모든 키가 백업됨 + + %d개의 키를 백업 중… + + + 버전 + 알고리즘 + 서명 + + 잘못된 홈서버 검색 응답 + 자동 완성 서버 설정 + Riot이 userId 도메인 \"%1$s\"에 대한 맞춤 서버 설정을 감지했습니다: +\n%2$s + 설정 사용 + + 올바르지 않거나 만료된 자격 때문에 로그아웃되었습니다. + + 짧은 문장과 비교하여 확인하세요. + 보안을 최대화하려면, 상대방과 직접 대면하거나 신뢰할 수 있는 다른 대화 수단을 사용하는 것이 좋습니다. + 인증 시작 + 수신 확인 요청 + 이 기기를 확인하여 신뢰할 수 있는 것으로 표시하세요. 종단간 암호화 메시지를 사용하는 경우 상대방의 기기를 신뢰하면 안심할 수 있습니다. + 이 기기를 확인하여 신뢰할 수 있는 것으로 표시하세요, 그리고 당신의 기기도 상대방에게 신뢰할 수 있는 것으로 표시하세요. + + 다음 이모지가 상대방의 화면에 나타나는 것을 확인하는 것으로 이 기기를 인증합니다 + 다음 숫자가 상대방의 화면에 나타나는 것을 확인하는 것으로 이 기기를 인증합니다 + + 수신 확인 요청을 받았습니다. + 요청 보기 + 상대방이 확인하기를 기다리는 중… + + 인증되었습니다! + 성공적으로 이 기기를 인증했습니다. + 이 사용자 간의 보안 메시지는 종단간 암호화 되며 제 3자가 읽을 수 없습니다. + 알겠습니다 + + 아무것도 안 나타나나요\? 일부 클라이언트는 아직 대화 식 인증을 지원하지 않습니다. 옛날 인증 방식을 사용하세요. + 옛날 인증 방식 사용하기. + + 키 인증 + 요청 취소됨 + 상대방이 확인을 취소했습니다. +\n%s + 인증이 취소되었습니다. +\n이유: %s + + 상호작용 기기 인증 + 인증 요청 + %s님이 당신의 기기를 인증하고 싶습니다 + + 사용자가 인증을 취소했습니다 + 인증 과정 시간이 초과되었습니다 + 기기는 이 처리에 대해 모릅니다 + 기기는 키 계약, 해시, MAC 또는 SAS 방식에 동의할 수 없습니다 + 해시 커밋이 일치하지 않습니다 + SAS가 일치하지 않습니다 + 기기가 예기치 못한 메시지를 받았습니다 + 올바르지 않은 메시지를 받았습니다 + 키 맞지 않음 + 사용자 맞지 않음 + 알 수 없는 오류 + + + 편집 + 답장 + + 다시 시도 + 앱을 시작하면 방에 참가하기. + 초대장을 보냈습니다 + %1$s 님이 초대했습니다 + + 모두 읽었습니다! + 더 이상 읽지 않은 메시지가 없습니다 + 홈에 온 것을 환영합니다! + 여기부터 읽지 않은 메시지를 읽으세요 + 대화 + 다이렉트 메시지 대화가 여기에 표시됩니다 + + 방이 여기 표시됩니다 + + 리액션 + 동의 + 좋아요 + 리액션 추가 + 리액션 보기 + 리액션 + + 사용자가 삭제한 이벤트 + 방 관리자가 중재한 이벤트 + %s님이 %s에 마지막으로 편집함 + + + 잘못된 이벤트, 표시할 수 없음 + 새 방 만들기 + 네트워크 없음. 인터넷 연결 상태를 확인하세요. + 변경 + 네트워크 변경 + 기다려주세요… + 모든 커뮤니티 + + 이 방은 미리 볼 수 없습니다 + 세계가 읽을 수 있는 방의 미리보기는 아직 RiotX에서 지원하지 않습니다 + + + 다이렉트 메시지 + + 새 방 + 만들기 + 방 이름 + 공공 + 누구나 이 방에 참가할 수 있습니다 + 방 디렉토리 + 이 방을 방 디렉토리에 게시 + + 신뢰 정보를 얻는 과정에서 오류가 발생했습니다 + 키 백업 데이터를 얻는 과정에서 오류가 발생했습니다 + + 베타 버전에 온 것을 환영합니다! + RiotX가 개발 중에 있기에, 일부 기능이 부족하고 버그가 나올 수 있습니다. + 최신 기능 목록은 항상 %1$s에 있고, 버그를 발견했다면 홈의 왼쪽 위 메뉴에서 보고해주세요, 그러면 가능한 한 빨리 고치겠습니다. + Play 스토어 설명 + 버그를 발견했다면 홈의 왼쪽 위 메뉴에서 보고해주세요, 그러면 가능한 한 빨리 고치겠습니다. + + 파일 \"%1$s\"에서 종단간 암호화 키 가져옴. + + Matrix SDK 버전 + 다른 제 3자 공지 + 이 방을 이미 봤습니다! + + 빠른 리액션 + + 기본 + 환경 설정 + 보안 & 개인 + 전문가 + 푸시 규칙 + 푸시 규칙이 정의되지 않음 + 등록된 푸시 게이트웨이가 없음 + + app_id: + push_key: + app_display_name: + device_name: + Url: + Format: + + 음성 & 영상 + 도움 & 정보 + + + 등록 토큰 + + 제안하기 + 아래에 당신의 제안을 적어주세요. + 여기에 당신의 제안을 설명 + 감사합니다, 제안을 성공적으로 보냈습니다 + 제안을 보내는 데 실패함 (%s) + + 타임라인에서 숨겨진 이벤트 보여주기 + + RiotX - 차세대 Matrix 클라이언트 + 최신 안드로이드 프레임워크를 사용해 더 빠르고 가벼운 Matrix를 위한 클라이언트 + RiotX는 Matrix 프로토콜 (Matrix.org)을 위한 새 클라이언트입니다: 안전한 분산 통신을 위한 개방형 네트워크. RiotX는 Matrix 안드로이드 SDK의 Riot 안드로이드 클라이언트 전체 개정판을 기반으로 한 Riot 안드로이드 클라이언트의 전체 개정판입니다. +\n +\n면책 조항: 이것은 베타 버전입니다. RiotX는 현재 개발 중이고 한계가 있으며 (많지 않으면 좋겠지만) 버그가 있습니다. 모든 피드백은 환영합니다! +\n +\nRiotX 지원: • 존재하는 계정으로 로그인 • 방을 만들고 공공 방에 참가 • 초대를 수락하거나 거절 • 사용자 방 목록 • 방 세부 정보 보기 • 문자 메시지 보내기 • 첨부 파일 보내기 • 암호화된 방에서 메시지 읽고 쓰기 • 암호화: 종단간 암호화 키 백업, 고급 기기 인증, 키 공유 요청과 답장 • 푸시 알림 • 밝은 테마, 어두운 테마 그리고 검정 테마 +\n +\n아직 Riot의 모든 기능이 RiotX에 구현되지 않았습니다. 주요 없는 (그리고 곧 나올!) 기능: • 계정 만들기 • 방 설정 (방 구성원 목록 등) • 다이렉트 대화 방 만들기 • 전화 • 위젯 • … + + 다이렉트 메시지 + + 기다리세요… + 썸네일 암호화 중… + 썸네일 보내는 중 (%1$s / %2$s) + 파일 암호화 중… + 파일 보내는 중 (%1$s / %2$s) + + 파일 %1$s 다운로드하는 중… + 파일 %1$s을(를) 다운로드했습니다! + + (편집됨) + + 계정을 만들려면 %1$s하세요. + 예전 앱을 사용 + + + 메시지 편집 + 수정 사항이 없음 + + 대화 필터… + 원하는 것을 찾을 수 없나요\? + 새 방 만들기 + 새 다이렉트 메시지 보내기 + 방 디렉토리 보기 + + 이름 혹은 ID (#예시:matrix.org) + + 타임라인에서 스와이프로 답장 기능 켜기 + + 클립보드에 링크 복사 + + Matrix ID로 추가 + 방 만드는 중… + 결과가 없습니다, Matrix ID로 추가를 사용하여 서버를 검색하세요. + 결과를 얻기 위해 검색 시작 + 사용자 이름 또는 ID로 필터… + + 방에 참가하는 중… + + 편집 기록 보기 + diff --git a/vector/src/main/res/values-nl/strings.xml b/vector/src/main/res/values-nl/strings.xml index 5d57a54d..778746b0 100755 --- a/vector/src/main/res/values-nl/strings.xml +++ b/vector/src/main/res/values-nl/strings.xml @@ -155,7 +155,7 @@ Wachtwoord herhalen Nieuw wachtwoord bevestigen Verkeerde gebruikersnaam en/of wachtwoord - Gebruikersnamen mogen alleen letters, cijfers, punten en afbreek- en lage streepjes bevatten + Gebruikersnamen mogen alleen letters, cijfers, punten, koppeltekens en underscores bevatten Wachtwoord te kort (min 6) Wachtwoord ontbreekt Dit is geen geldig e-mailadres @@ -292,7 +292,7 @@ Afwijzen - Spring naar het eerste ongelezen bericht. + Ga naar het eerste ongelezen bericht. %s heeft u uitgenodigd in dit gesprek @@ -484,7 +484,7 @@ Cryptografie Meldingsdoelen Lokale contacten - Contacten-toestemming + Toegang geven tot lokale contacten Land voor telefoonboek Startscherm Gesprekken met gemiste meldingen vastprikken @@ -689,7 +689,7 @@ Alle lokale gesprekken op %s - Zoeken naar historische + Zoeken in de historiek Offline Gebruikerscatalogus @@ -903,8 +903,8 @@ Avatar - 1 ongelezen bericht waarin u vermeld staat - %d ongelezen berichten waarin u vermeld staat + 1 ongelezen bericht waarin u vermeld bent + %d ongelezen berichten waarin u vermeld bent Vermeldingsavatar Verstuur een sticker @@ -1208,7 +1208,7 @@ Als een gebruiker een apparaat los van de oplader een tijd laat stilliggen, met het scherm uitgeschakeld, gaat het apparaat in slaapmodus. Dit verhindert apps de toegang tot het netwerk, en stelt hun taken, synchronisaties en standaardalarmen uit. Optimalisatie negeren - De app heeft geen verbinding met de homeserver nodig in de achtergrond, dit zou accuverbruik moeten verlagen + De app heeft geen verbinding met de homeserver nodig in de achtergrond, dit zou het gebruik van de batterij moeten verlagen Lawaaiierige meldingen configureren Oproepmeldingen configureren Stille meldingen configureren diff --git a/vector/src/main/res/values-pl/strings.xml b/vector/src/main/res/values-pl/strings.xml index 3298e5dd..2b2198cd 100644 --- a/vector/src/main/res/values-pl/strings.xml +++ b/vector/src/main/res/values-pl/strings.xml @@ -28,7 +28,7 @@ Przychodzące połączenie grupowe. \nDołącz z %1$s lub %2$s Głos - kamerą + Wideo Nie można rozpocząć połączenia, spróbuj ponownie później Ze względu na brak pewnych uprawnień, niektóre funkcje mogą nie działać… Musisz być uprawniony, aby rozpocząć połączenie grupowe @@ -287,7 +287,7 @@ Przyznaj dostęp w następnym oknie. Wyślij wiadomość (niezaszyfrowaną)… Utracono połączenie z serwerem. Wyślij ponownie wszystko - anuluj wszystko + Anuluj wszystko Wyślij niewysłane wiadomości ponownie Usuń niewysłane wiadomości Nie znaleziono pliku @@ -430,7 +430,7 @@ Zauważ, że ta czynność spowoduje ponowne uruchomienie aplikacji i może to t Kraj Wybierz kraj Numer telefonu - Weryfikacja numeru + Weryfikacja numeru telefonu Wprowadź kod aktywacyjny Wystąpił błąd podczas weryfikowania numeru telefonu Kod @@ -615,9 +615,9 @@ Zezwól na dostęp w następnym oknie aby móc wykonać połączenie. Przyznaj dostęp w następnym oknie. Riot może sprawdzić Twoją książkę adresową, aby znajdywać innych użytkowników Matrixa bazując na ich adresie e-mail i numerze telefonu. Jeśli zgadzasz się na udostępnienie Twojej książki adresowej w tym celu, zezwól na dostęp w następnym okienku. - Riot wymaga dostępu do kontaktów, aby znajdywać innych użytkowników Matrixa bazując na adresie e-mail i numerze telefonu. - -Zezwolić Riot na dostęp do kontaktów? + Riot wymaga dostępu do kontaktów, aby znajdywać innych użytkowników Matrixa bazując na adresie e-mail i numerze telefonu. +\n +\nZezwolić Riot na dostęp do kontaktów\? Lista uczestników Otwarty nagłówek @@ -1232,4 +1232,22 @@ Spróbuj uruchomić ponownie aplikację. Utwórz kopię zapasową Twoje urządzenie używa przestarzałego protokołu bezpieczeństwa TSL, podatnego na ataki, dlatego dla Twojego bezpieczeństwa nie będziesz mógł się połączyć Kopia zapasowa klucza została prawidłowo skonfigurowana dla tego urządzenia. + Inicjalizacja usługi + Zaloguj się za pomocą logowania jednorazowego + Ustaw ważność powiadomienia za pomocą wydarzeń, skonfiguruj dźwięk, diodę LED, wibracje + Ważność powiadomień ze względu na wydarzenie + + Powiadomienia diagnostyczne + Rozwiązywanie problemów + Diagnostyka podstawowa nie wykazała problemów. Jeżeli wciąż nie otrzymujesz powiadomień, prosimy o przesłanie raportu o błędach, w celu ich rozwiązania. + Aplikacja nie potrzebuje łączyć się z serwerem domowym w tle, powinno to zredukować użycie baterii + Jeżeli nie pamiętasz swoich danych odzystkiwania, możesz %s. + + Zgubiłeś (-łaś) swój klucz odzyskiwania\? Możesz ustawić nowy w ustawieniach. + Kopia zapasowa posiada poprawną sygnaturę z niezweryfikowanego urządzenia %s + Wykryto nową, bezpieczną kopię kluczy wiadomości. +\n +\nJeżeli nie ustawiałeś nowej metody odzyskiwania, atakujący mogą uzyskać dostęp do Twojego konta. Zmień hasło konta i ustaw nową metodę odzyskiwania jak najszybciej w Ustawieniach. + Jesteś na bieżąco! + Ten adres URL jest nieosiągalny, proszę sprawdź jego poprawność diff --git a/vector/src/main/res/values-ru/strings.xml b/vector/src/main/res/values-ru/strings.xml index 90c2cba1..f62a760a 100644 --- a/vector/src/main/res/values-ru/strings.xml +++ b/vector/src/main/res/values-ru/strings.xml @@ -264,7 +264,7 @@ Email также позволит вам при необходимости во Riot необходимы разрешения на доступ к камере и микрофону для видеовызовов. Пожалуйста дайте разрешение в следующем окне для звонка. - Riot необходимы разрешения на доступ к контактам, чтобы найти других пользователей сети по email или телефонному номеру. Пожалуйста разрешите доступ в следующем окне, чтобы ваши контакты были доступны в приложении. + Riot может проверить вашу адресную книгу, чтобы найти других пользователей Matrix по их электронной почте и номерам телефонов. Если вы согласны поделиться своей адресной книгой для этой цели, пожалуйста, откройте доступ на следующем всплывающем окне. Riot может проверить Вашу адресную книгу, чтобы найти других пользователей сети по email или телефонному номеру. \n \nСогласны ли вы поделиться своей адресной книгой для этой цели\? @@ -352,7 +352,7 @@ Email также позволит вам при необходимости во Сообщения не отправлены. %1$s или %2$s сейчас? Сообщения, не отправлены из-за присутствия неизвестных устройств. %1$s или %2$s сейчас? Повторить отправку - удалить все + Удалить все Отправить неотправленные сообщения Удалить неотправленные сообщения Файл не найден @@ -917,9 +917,9 @@ Email также позволит вам при необходимости во Получить аватар Заметка аватара - Лычки + Флэр - Эта комната не показывает лычки сообществ + Эта комната не показывает флэр сообществ Конфиденциальность уведомлений Нормальный Приложение нуждается в разрешении на работу в фоновом режиме @@ -1584,6 +1584,133 @@ Email также позволит вам при необходимости во Реакция Соглашаться Нравиться - Добавить реакцию + Добавить действие Посмотреть реакции + Менеджер интеграции + + Менеджер интеграции не настроен. + Реакции + + Удаленное пользователем событие + Мероприятие, модерируемое администратором помещения + Последний раз редактировался %s на %s + + + Некорректное событие, не может быть отображено + Создать новую комнату + Сети нет. Пожалуйста, проверьте подключение к Интернету. + Изменить + Изменить сеть + Пожалуйста, подождите… + Все сообщества + + Эту комнату нельзя предварительно просмотреть + Комнаты + Прямые сообщения + + Новая комната + СОЗДАТЬ + Имя комнаты + Публичные + Любой сможет присоединиться к этой комнате + Каталог номеров + Опубликовать эту комнату в каталоге номеров + + Произошла ошибка при получении информации о доверии + Произошла ошибка при получении ключей резервного копирования данных + + Добро пожаловать в бету! + Пока RiotX находится в ранней стадии разработки, некоторые функции могут отсутствовать и вы можете столкнуться с ошибками. + Последний список функций всегда находится в %1$s, и если вы найдете ошибку, пожалуйста, отправьте отчет в верхнее левое меню Home, и мы исправим их так быстро, как только сможем. + Описание магазина + Если вы обнаружите ошибку, пожалуйста, отправьте сообщение в верхнее левое меню Home, и мы исправим ее так быстро, как только сможем. + + Импорт ключей e2e из файла \"%1$s\". + + Версия Matrix SDK + Другие уведомления третьих сторон + Вы уже просмотрели эту комнату! + + Быстрое реагирование + + Общее + Установки + Безопасность & Конфиденциальность + Профессионал + Нажать Правила + app_id: + push_key: + app_display_name: + device_name: + Url: + Формат: + + Голос & Видио + Помощь и информация + + + Регистрационный токен + + Сделайте предложение + Пожалуйста, напишите ваше предложение ниже. + Опишите ваше предложение здесь + Спасибо, предложение было успешно отправлено + Предложение не было отправлено (%s) + + Показать скрытые события в временной шкале + + 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 пока не поддерживается. + + Прямые сообщения + + Ждите… + Шифрование миниатюры… + Отправка миниатюр (%1$s / %2$s) + Файл шифруется… + Файл отправляется (%1$s / %2$s) + + Файл %1$s загружается… + Файл %1$s был загружен! + + (отредактированный) + + %1$s для создания учетной записи. + Используйте старое приложение + + + Редактирование сообщений + Редактирование не найдено + + Отфильтровывать разговоры… + Не можете найти то, что ищете\? + Создать новую комнату + Отправить новое прямое сообщение + Просмотр список комнат + + Имя или ID (#example:matrix.org) + + Включить свайп, для ответа в хронологии + + Ссылка скопирована в буфер обмена + + Добавить по matrix ID + Создание комнаты… + "Результат не найден, используйте добавить matrix ID для поиска на сервере." + Начните печатать, чтобы получить результат + Фильтр по имени пользователя или ID… + + Присоединение к комнате… + + Просмотреть историю изменений + diff --git a/vector/src/main/res/values-sq/strings.xml b/vector/src/main/res/values-sq/strings.xml index 8ab796fc..0b6381aa 100644 --- a/vector/src/main/res/values-sq/strings.xml +++ b/vector/src/main/res/values-sq/strings.xml @@ -314,7 +314,7 @@ Dërgoni një përgjigje (të pafshehtëzuar)… Humbi lidhja me shërbyesin. Ridërgoji krejt - anuloji krejt + Anuloji krejt Ridërgo mesazhet e padërguara Fshi mesazhet e padërguar S’u gjet kartelë @@ -1531,4 +1531,51 @@ Që të garantoni se s’ju shpëton gjë, thjesht mbajeni të aktivizuar mekani \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 • … + Përgjegjës Integrimesh + + Pa përgjegjës Integrimesh të formësuar. + app_id: + push_key: + app_display_name: + device_name: + Mesazhe të Drejtpërdrejtë + + Po pritet… + Po fshehtëzohet miniatura… + Po dërgohet miniaturë (%1$s / %2$s) + Po fshehtëzohet kartelë… + Po dërgohet kartelë (%1$s / %2$s) + + Po shkarkohet kartelë %1$s… + Kartela %1$s u shkarkua! + + (u përpunua) + + %1$s që të krijohet një llogari. + Përdor aplikacionin e dikurshëm + + + Përpunime Mesazhi + S’u gjetën përpunime + + Filtroni biseda… + S’gjeni dot ç’po kërkoni\? + Krijoni dhomë të re + Dërgoni një mesazh të drejtpërdrejtë + Shihni drejtori dhomash + + Emër ose ID (#example:matrix.org) + + Lidhja u kopjua në të papastër + + Shtoni sipas ID-je matrix + Po krijohet dhomë… + S’u gjetën përfundime, përdorni “Shtoni sipas ID-je matrix” që të kërkohet në shërbyes. + Filloni të shtypni që të merrni përfundime + Filtrojini sipas emrash përdoruesish ose ID-sh… + + Po hyhet në dhomë… + + Shihni Historik Përpunimesh + diff --git a/vector/src/main/res/values-vls/strings.xml b/vector/src/main/res/values-vls/strings.xml index e07f432d..ff27a8fa 100644 --- a/vector/src/main/res/values-vls/strings.xml +++ b/vector/src/main/res/values-vls/strings.xml @@ -37,4 +37,13 @@ Sleuterback-up gebruukn Zy je zeker\? Back-up moakn + Je goat den toegank tou je versleuterde berichtn kwytspeeln, tenzy da j’eerst een back-up van je sleuters makt vooraleer da je jen afmeldt. + + Licensjes van derde partyen + + Loadn… + + Oké + Annuleern + Ipsloan diff --git a/vector/src/main/res/values-zh-rTW/strings.xml b/vector/src/main/res/values-zh-rTW/strings.xml index 5be107e9..332e7a82 100644 --- a/vector/src/main/res/values-zh-rTW/strings.xml +++ b/vector/src/main/res/values-zh-rTW/strings.xml @@ -1564,4 +1564,17 @@ Matrix 中的消息可見度類似于電子郵件。我們忘記您的郵件意 連結已複製到剪貼簿 + 整合管理員 + + 未設定整合管理員。 + 透過 matrix ID 新增 + 正在建立聊天室…… + 找不到結果,在伺服器上使用透過 matrix ID 新增。 + 開始輸入以取得結果 + 透過使用者名稱或 ID 過濾…… + + 正在加入聊天室…… + + 檢視編輯歷史 + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index c701387d..8b831757 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -95,6 +95,8 @@ Done Abort Ignore + Review + Decline Exit @@ -1099,6 +1101,7 @@ Use keyboard enter key to send message Send voice messages This option requires a third party application to record the messages. + To continue you need to accept the Terms of this service. You added a new device \'%s\', which is requesting encryption keys. @@ -1467,6 +1470,8 @@ Why choose Riot.im? Unknown Error + "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" + Edit Reply @@ -1640,4 +1645,12 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming View Edit History + + Terms of Service + Review Terms + Be discoverable by others + Use Bots, bridges, widgets and sticker packs + + Read at + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 23ea1e2f..118fde84 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -2,6 +2,5 @@ - Read at \ No newline at end of file From 88095e4bd97354a531eab5b7aa47c9f0a27d5497 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 30 Aug 2019 14:54:15 +0200 Subject: [PATCH 39/40] Add entry in change file --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 415cdff4..85596e51 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,6 +17,7 @@ Bugfix: - Regression / e2e replies not encrypted - Some video won't play - Privacy: remove log of notifiable event (#519) + - Fix crash with EmojiCompat (#530) Translations: - From 58f878fca943877e23dfc6722294a28b4c8085f4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 30 Aug 2019 15:04:28 +0200 Subject: [PATCH 40/40] Prepare version 0.4.0 --- CHANGES.md | 9 --------- vector/build.gradle | 2 +- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 85596e51..d22665cf 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,9 +7,6 @@ Features: Improvements: - Reactions: Reinstate the ability to react with non-unicode keys (#307) -Other changes: - - - Bugfix: - Fix text diff linebreak display (#441) - Date change message repeats for each redaction until a normal message (#358) @@ -19,12 +16,6 @@ Bugfix: - Privacy: remove log of notifiable event (#519) - Fix crash with EmojiCompat (#530) -Translations: - - - -Build: - - - Changes in RiotX 0.3.0 (2019-08-08) =================================================== diff --git a/vector/build.gradle b/vector/build.gradle index f8c800d1..71d37da6 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -15,7 +15,7 @@ androidExtensions { } ext.versionMajor = 0 -ext.versionMinor = 3 +ext.versionMinor = 4 ext.versionPatch = 0 static def getGitTimestamp() {