Compare commits

..

34 Commits

Author SHA1 Message Date
a89f0ddd1d Merge branch 'release/0.4.0' 2019-08-30 15:04:43 +02:00
58f878fca9 Prepare version 0.4.0 2019-08-30 15:04:28 +02:00
88095e4bd9 Add entry in change file 2019-08-30 14:54:15 +02:00
47d22a3d5e Import translation from Riot and MatrixSDK 2019-08-30 11:21:43 +02:00
28e82cb8ea Merge pull request #531 from vector-im/feature/fix_crash_530
Fix / EmojiCompat not initialized
2019-08-29 17:46:51 +02:00
35817245cb refactoring, code review 2019-08-29 17:27:49 +02:00
75266f42bb Fix / EmojiCompat not initialized 2019-08-29 16:49:22 +02:00
95c4c9ce56 Merge pull request #527 from vector-im/feature/privacy
Privacy: remove log of notifiable event (#519)
2019-08-29 12:16:34 +02:00
ce5570105d Privacy: remove log of notifiable event (#519) 2019-08-29 10:36:45 +02:00
188a9aebfa Merge pull request #525 from vector-im/feature/read_receipt_cleanup
Feature/read receipt cleanup
2019-08-29 10:19:06 +02:00
c95223f5d2 Add long click support on unsupported event 2019-08-28 18:17:37 +02:00
ef0362ba9c Display Read Receipt on unsupported events 2019-08-28 17:31:31 +02:00
ea242f6737 Hide ReadReceipt View when it is not relevant 2019-08-28 17:17:37 +02:00
cbc08d834b Merge pull request #522 from vector-im/feature/fix_e2e_reply
Fix / regression on e2e reply and edit of reply
2019-08-28 10:38:22 +02:00
0ab6b33fb6 Merge branch 'develop' into feature/fix_e2e_reply 2019-08-28 10:38:12 +02:00
1b394527b6 cleaning + code review 2019-08-28 10:22:51 +02:00
a8f1388721 Merge pull request #520 from vector-im/feature/read_receipts_511
Improve read receipt design
2019-08-28 10:17:56 +02:00
166be4e289 Improve read receipt design 2019-08-28 09:56:10 +02:00
b49ccefe63 Merge pull request #521 from vector-im/feature/fix_dome_video_wont_play
Some video won't play
2019-08-28 03:43:35 -04:00
825760d17e Fix / regression on e2e reply and edit of reply 2019-08-27 17:05:04 +02:00
b5af62c3ea 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
2019-08-27 16:50:02 +02:00
a51d96bf00 Merge pull request #325 from vector-im/feature/non_unicode_reaction
Accept non unicode reactions
2019-08-27 08:10:51 -04:00
7e142d201d Use EmojiCompat to build EmojiSpans from text 2019-08-27 11:06:52 +02:00
2be6058971 accept non unicode reactions 2019-08-27 10:58:21 +02:00
49d73f360e Merge pull request #494 from vector-im/feature/fix_441
Fix text diff removed linebreak
2019-08-27 04:36:03 -04:00
bd88d85a21 Merge branch 'develop' into feature/fix_441 2019-08-27 04:35:17 -04:00
be4fc5cce6 Merge pull request #493 from vector-im/feature/fix_358
Date change message repeats for each redaction until a normal message
2019-08-27 04:34:35 -04:00
704da1be55 Merge branch 'develop' into feature/fix_358 2019-08-27 04:34:24 -04:00
5d002532d3 Merge pull request #495 from vector-im/feature/fix_423
Slide-in reply icon is distorted
2019-08-27 04:22:02 -04:00
d4161e9a1a Fix text diff removed linebreak 2019-08-27 10:17:42 +02:00
7966ebef03 Date change message repeats for each redaction until a normal message 2019-08-27 10:16:11 +02:00
ed5faca5d2 Slide-in reply icon is distorted 2019-08-27 10:06:20 +02:00
9cd69d1e33 Merge branch 'release/0.3.0' 2019-08-08 16:45:03 +02:00
df6080b1da Merge branch 'release/0.2.0' 2019-07-18 17:47:39 +02:00
126 changed files with 3517 additions and 1563 deletions

View File

@ -5,19 +5,16 @@ Features:
- Display read receipts in timeline (#81) - Display read receipts in timeline (#81)
Improvements: Improvements:
- - Reactions: Reinstate the ability to react with non-unicode keys (#307)
Other changes:
-
Bugfix: Bugfix:
- - Fix text diff linebreak display (#441)
- Date change message repeats for each redaction until a normal message (#358)
Translations: - Slide-in reply icon is distorted (#423)
- - Regression / e2e replies not encrypted
- Some video won't play
Build: - Privacy: remove log of notifiable event (#519)
- - Fix crash with EmojiCompat (#530)
Changes in RiotX 0.3.0 (2019-08-08) Changes in RiotX 0.3.0 (2019-08-08)
=================================================== ===================================================

View File

@ -16,7 +16,7 @@ org.gradle.jvmargs=-Xmx1536m
vector.debugPrivateData=false vector.debugPrivateData=false
vector.httpLogLevel=HEADERS vector.httpLogLevel=NONE
# Note: to debug, you can put and uncomment the following lines in the file ~/.gradle/gradle.properties to override the value above # Note: to debug, you can put and uncomment the following lines in the file ~/.gradle/gradle.properties to override the value above
#vector.debugPrivateData=true #vector.debugPrivateData=true

View File

@ -35,14 +35,9 @@ data class RoomSummary(
val highlightCount: Int = 0, val highlightCount: Int = 0,
val tags: List<RoomTag> = emptyList(), val tags: List<RoomTag> = emptyList(),
val membership: Membership = Membership.NONE, val membership: Membership = Membership.NONE,
val versioningState: VersioningState = VersioningState.NONE, val versioningState: VersioningState = VersioningState.NONE
val readMarkerId: String? = null
) { ) {
val isVersioned: Boolean val isVersioned: Boolean
get() = versioningState != VersioningState.NONE get() = versioningState != VersioningState.NONE
val hasNewMessages: Boolean
get() = notificationCount != 0
} }

View File

@ -1,25 +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.matrix.android.api.session.room.read
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class FullyReadContent(
@Json(name = "event_id") val eventId: String
)

View File

@ -42,10 +42,5 @@ interface ReadService {
fun isEventRead(eventId: String): Boolean fun isEventRead(eventId: String): Boolean
/**
* Returns a nullable read marker for the room.
*/
fun getReadMarkerLive(): LiveData<String?>
fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>> fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>>
} }

View File

@ -32,8 +32,6 @@ interface Timeline {
var listener: Listener? var listener: Listener?
val isLive: Boolean
/** /**
* This should be called before any other method after creating the timeline. It ensures the underlying database is open * This should be called before any other method after creating the timeline. It ensures the underlying database is open
*/ */
@ -44,10 +42,6 @@ interface Timeline {
*/ */
fun dispose() fun dispose()
fun restartWithEventId(eventId: String)
/** /**
* Check if the timeline can be enriched by paginating. * Check if the timeline can be enriched by paginating.
* @param the direction to check in * @param the direction to check in
@ -55,7 +49,6 @@ interface Timeline {
*/ */
fun hasMoreToLoad(direction: Direction): Boolean fun hasMoreToLoad(direction: Direction): Boolean
/** /**
* This is the main method to enrich the timeline with new data. * This is the main method to enrich the timeline with new data.
* It will call the onUpdated method from [Listener] when the data will be processed. * It will call the onUpdated method from [Listener] when the data will be processed.
@ -67,13 +60,6 @@ interface Timeline {
fun failedToDeliverEventCount() : Int fun failedToDeliverEventCount() : Int
fun getIndexOfEvent(eventId: String?): Int?
fun getTimelineEventAtIndex(index: Int): TimelineEvent?
fun getTimelineEventWithId(eventId: String?): TimelineEvent?
interface Listener { interface Listener {
/** /**
* Call when the timeline has been updated through pagination or sync. * Call when the timeline has been updated through pagination or sync.

View File

@ -39,8 +39,7 @@ data class TimelineEvent(
val isUniqueDisplayName: Boolean, val isUniqueDisplayName: Boolean,
val senderAvatar: String?, val senderAvatar: String?,
val annotations: EventAnnotationsSummary? = null, val annotations: EventAnnotationsSummary? = null,
val readReceipts: List<ReadReceipt> = emptyList(), val readReceipts: List<ReadReceipt> = emptyList()
val hasReadMarker: Boolean = false
) { ) {
val metadata = HashMap<String, Any>() val metadata = HashMap<String, Any>()

View File

@ -23,7 +23,6 @@ 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.mapper.toEntity
import im.vector.matrix.android.internal.database.model.ChunkEntity 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.EventAnnotationsSummaryEntity
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity 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.TimelineEventEntity
@ -158,6 +157,7 @@ internal fun ChunkEntity.add(roomId: String,
} }
} }
val eventEntity = TimelineEventEntity(localId).also { val eventEntity = TimelineEventEntity(localId).also {
it.root = event.toEntity(roomId).apply { it.root = event.toEntity(roomId).apply {
this.stateIndex = currentStateIndex this.stateIndex = currentStateIndex
@ -169,7 +169,6 @@ internal fun ChunkEntity.add(roomId: String,
it.roomId = roomId it.roomId = roomId
it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
it.readReceipts = readReceiptsSummaryEntity it.readReceipts = readReceiptsSummaryEntity
it.readMarker = ReadMarkerEntity.where(realm, roomId = roomId, eventId = eventId).findFirst()
} }
val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size
timelineEvents.add(position, eventEntity) timelineEvents.add(position, eventEntity)

View File

@ -26,8 +26,8 @@ import java.util.*
import javax.inject.Inject import javax.inject.Inject
internal class RoomSummaryMapper @Inject constructor( internal class RoomSummaryMapper @Inject constructor(
private val cryptoService: CryptoService, val cryptoService: CryptoService,
private val timelineEventMapper: TimelineEventMapper val timelineEventMapper: TimelineEventMapper
) { ) {
fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary { fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary {
@ -65,8 +65,7 @@ internal class RoomSummaryMapper @Inject constructor(
notificationCount = roomSummaryEntity.notificationCount, notificationCount = roomSummaryEntity.notificationCount,
tags = tags, tags = tags,
membership = roomSummaryEntity.membership, membership = roomSummaryEntity.membership,
versioningState = roomSummaryEntity.versioningState, versioningState = roomSummaryEntity.versioningState
readMarkerId = roomSummaryEntity.readMarkerId
) )
} }
} }

View File

@ -45,8 +45,7 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS
senderAvatar = timelineEventEntity.senderAvatar, senderAvatar = timelineEventEntity.senderAvatar,
readReceipts = readReceipts?.sortedByDescending { readReceipts = readReceipts?.sortedByDescending {
it.originServerTs it.originServerTs
} ?: emptyList(), } ?: emptyList()
hasReadMarker = timelineEventEntity.readMarker?.eventId?.isEmpty() == false
) )
} }

View File

@ -1,35 +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.matrix.android.internal.database.model
import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.annotations.LinkingObjects
import io.realm.annotations.PrimaryKey
internal open class ReadMarkerEntity(
@PrimaryKey
var roomId: String = "",
var eventId: String = ""
) : RealmObject() {
@LinkingObjects("readMarker")
val timelineEvent: RealmResults<TimelineEventEntity>? = null
companion object
}

View File

@ -35,8 +35,7 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
var otherMemberIds: RealmList<String> = RealmList(), var otherMemberIds: RealmList<String> = RealmList(),
var notificationCount: Int = 0, var notificationCount: Int = 0,
var highlightCount: Int = 0, var highlightCount: Int = 0,
var tags: RealmList<RoomTagEntity> = RealmList(), var tags: RealmList<RoomTagEntity> = RealmList()
var readMarkerId: String? = null
) : RealmObject() { ) : RealmObject() {
private var membershipStr: String = Membership.NONE.name private var membershipStr: String = Membership.NONE.name

View File

@ -43,7 +43,6 @@ import io.realm.annotations.RealmModule
PushConditionEntity::class, PushConditionEntity::class,
PusherEntity::class, PusherEntity::class,
PusherDataEntity::class, PusherDataEntity::class,
ReadReceiptsSummaryEntity::class, ReadReceiptsSummaryEntity::class
ReadMarkerEntity::class
]) ])
internal class SessionRealmModule internal class SessionRealmModule

View File

@ -31,8 +31,7 @@ internal open class TimelineEventEntity(var localId: Long = 0,
var isUniqueDisplayName: Boolean = false, var isUniqueDisplayName: Boolean = false,
var senderAvatar: String? = null, var senderAvatar: String? = null,
var senderMembershipEvent: EventEntity? = null, var senderMembershipEvent: EventEntity? = null,
var readReceipts: ReadReceiptsSummaryEntity? = null, var readReceipts: ReadReceiptsSummaryEntity? = null
var readMarker: ReadMarkerEntity? = null
) : RealmObject() { ) : RealmObject() {
@LinkingObjects("timelineEvents") @LinkingObjects("timelineEvents")

View File

@ -1,37 +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.matrix.android.internal.database.query
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
import im.vector.matrix.android.internal.database.model.ReadMarkerEntityFields
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.kotlin.where
internal fun ReadMarkerEntity.Companion.where(realm: Realm, roomId: String, eventId: String? = null): RealmQuery<ReadMarkerEntity> {
val query = realm.where<ReadMarkerEntity>()
.equalTo(ReadMarkerEntityFields.ROOM_ID, roomId)
if (eventId != null) {
query.equalTo(ReadMarkerEntityFields.EVENT_ID, eventId)
}
return query
}
internal fun ReadMarkerEntity.Companion.getOrCreate(realm: Realm, roomId: String): ReadMarkerEntity {
return where(realm, roomId).findFirst()
?: realm.createObject(ReadMarkerEntity::class.java, roomId)
}

View File

@ -20,7 +20,6 @@ import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptEntityFields import im.vector.matrix.android.internal.database.model.ReadReceiptEntityFields
import io.realm.Realm import io.realm.Realm
import io.realm.RealmQuery import io.realm.RealmQuery
import io.realm.RealmResults
import io.realm.kotlin.where import io.realm.kotlin.where
internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, userId: String): RealmQuery<ReadReceiptEntity> { internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, userId: String): RealmQuery<ReadReceiptEntity> {
@ -29,12 +28,6 @@ internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, use
.equalTo(ReadReceiptEntityFields.USER_ID, userId) .equalTo(ReadReceiptEntityFields.USER_ID, userId)
} }
internal fun ReadReceiptEntity.Companion.whereUserId(realm: Realm, userId: String): RealmQuery<ReadReceiptEntity> {
return realm.where<ReadReceiptEntity>()
.equalTo(ReadReceiptEntityFields.USER_ID, userId)
}
internal fun ReadReceiptEntity.Companion.createUnmanaged(roomId: String, eventId: String, userId: String, originServerTs: Double): ReadReceiptEntity { internal fun ReadReceiptEntity.Companion.createUnmanaged(roomId: String, eventId: String, userId: String, originServerTs: Double): ReadReceiptEntity {
return ReadReceiptEntity().apply { return ReadReceiptEntity().apply {
this.primaryKey = "${roomId}_$userId" this.primaryKey = "${roomId}_$userId"

View File

@ -31,12 +31,6 @@ internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = n
return query return query
} }
internal fun RoomSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: String): RoomSummaryEntity {
return where(realm, roomId).findFirst()
?: realm.createObject(RoomSummaryEntity::class.java, roomId)
}
internal fun RoomSummaryEntity.Companion.getDirectRooms(realm: Realm): RealmResults<RoomSummaryEntity> { internal fun RoomSummaryEntity.Companion.getDirectRooms(realm: Realm): RealmResults<RoomSummaryEntity> {
return RoomSummaryEntity.where(realm) return RoomSummaryEntity.where(realm)
.equalTo(RoomSummaryEntityFields.IS_DIRECT, true) .equalTo(RoomSummaryEntityFields.IS_DIRECT, true)

View File

@ -21,4 +21,4 @@ import javax.inject.Scope
@Scope @Scope
@MustBeDocumented @MustBeDocumented
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
internal annotation class MatrixScope annotation class MatrixScope

View File

@ -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.AccessTokenInterceptor
import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater 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.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.create.RoomCreateEventLiveObserver
import im.vector.matrix.android.internal.session.room.prune.EventsPruner import im.vector.matrix.android.internal.session.room.prune.EventsPruner
import im.vector.matrix.android.internal.session.room.tombstone.RoomTombstoneEventLiveObserver import im.vector.matrix.android.internal.session.room.tombstone.RoomTombstoneEventLiveObserver

View File

@ -21,4 +21,4 @@ import javax.inject.Scope
@Scope @Scope
@MustBeDocumented @MustBeDocumented
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
internal annotation class SessionScope annotation class SessionScope

View File

@ -28,10 +28,8 @@ 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.RealmLiveData
import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper
import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.query.find import im.vector.matrix.android.internal.database.query.find
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom 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.where
@ -95,15 +93,6 @@ internal class DefaultReadService @AssistedInject constructor(@Assisted private
return isEventRead return isEventRead
} }
override fun getReadMarkerLive(): LiveData<String?> {
val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm ->
ReadMarkerEntity.where(realm, roomId)
}
return Transformations.map(liveRealmData) { results ->
results.firstOrNull()?.eventId
}
}
override fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>> { override fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>> {
val liveEntity = RealmLiveData(monarchy.realmConfiguration) { realm -> val liveEntity = RealmLiveData(monarchy.realmConfiguration) { realm ->
ReadReceiptsSummaryEntity.where(realm, eventId) ReadReceiptsSummaryEntity.where(realm, eventId)

View File

@ -18,25 +18,19 @@ package im.vector.matrix.android.internal.session.room.read
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.room.read.FullyReadContent
import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.query.find 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.findLastLiveChunkFromRoom
import im.vector.matrix.android.internal.database.query.getOrCreate
import im.vector.matrix.android.internal.database.query.latestEvent import im.vector.matrix.android.internal.database.query.latestEvent
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory
import im.vector.matrix.android.internal.session.sync.RoomFullyReadHandler
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.awaitTransaction
import io.realm.Realm import io.realm.Realm
import io.realm.RealmConfiguration
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -55,8 +49,7 @@ private const val READ_RECEIPT = "m.read"
internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI: RoomAPI, internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI: RoomAPI,
private val credentials: Credentials, private val credentials: Credentials,
private val monarchy: Monarchy, private val monarchy: Monarchy
private val roomFullyReadHandler: RoomFullyReadHandler
) : SetReadMarkersTask { ) : SetReadMarkersTask {
override suspend fun execute(params: SetReadMarkersTask.Params) { override suspend fun execute(params: SetReadMarkersTask.Params) {
@ -64,7 +57,6 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
val fullyReadEventId: String? val fullyReadEventId: String?
val readReceiptEventId: String? val readReceiptEventId: String?
Timber.v("Execute set read marker with params: $params")
if (params.markAllAsRead) { if (params.markAllAsRead) {
val latestSyncedEventId = Realm.getInstance(monarchy.realmConfiguration).use { realm -> val latestSyncedEventId = Realm.getInstance(monarchy.realmConfiguration).use { realm ->
TimelineEventEntity.latestEvent(realm, roomId = params.roomId, includesSending = false)?.eventId TimelineEventEntity.latestEvent(realm, roomId = params.roomId, includesSending = false)?.eventId
@ -76,16 +68,16 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
readReceiptEventId = params.readReceiptEventId readReceiptEventId = params.readReceiptEventId
} }
if (fullyReadEventId != null && isReadMarkerMoreRecent(params.roomId, fullyReadEventId)) { if (fullyReadEventId != null) {
if (LocalEchoEventFactory.isLocalEchoId(fullyReadEventId)) { if (LocalEchoEventFactory.isLocalEchoId(fullyReadEventId)) {
Timber.w("Can't set read marker for local event ${params.fullyReadEventId}") Timber.w("Can't set read marker for local event ${params.fullyReadEventId}")
} else { } else {
updateReadMarker(params.roomId, fullyReadEventId)
markers[READ_MARKER] = fullyReadEventId markers[READ_MARKER] = fullyReadEventId
} }
} }
if (readReceiptEventId != null if (readReceiptEventId != null
&& !isEventRead(params.roomId, readReceiptEventId)) { && !isEventRead(params.roomId, readReceiptEventId)) {
if (LocalEchoEventFactory.isLocalEchoId(readReceiptEventId)) { if (LocalEchoEventFactory.isLocalEchoId(readReceiptEventId)) {
Timber.w("Can't set read receipt for local event ${params.fullyReadEventId}") Timber.w("Can't set read receipt for local event ${params.fullyReadEventId}")
} else { } else {
@ -101,30 +93,12 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
} }
} }
private fun updateNotificationCountIfNecessary(roomId: String, eventId: String) {
private fun isReadMarkerMoreRecent(roomId: String, fullyReadEventId: String): Boolean { monarchy.writeAsync { realm ->
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
val readMarkerEntity = ReadMarkerEntity.where(realm, roomId = roomId).findFirst()
val readMarkerEvent = readMarkerEntity?.timelineEvent?.firstOrNull()
val eventToCheck = TimelineEventEntity.where(realm, eventId = fullyReadEventId).findFirst()
val readReceiptIndex = readMarkerEvent?.root?.displayIndex ?: Int.MAX_VALUE
val eventToCheckIndex = eventToCheck?.root?.displayIndex ?: Int.MIN_VALUE
eventToCheckIndex > readReceiptIndex
}
}
private suspend fun updateReadMarker(roomId: String, eventId: String) {
monarchy.awaitTransaction { realm ->
roomFullyReadHandler.handle(realm, roomId, FullyReadContent(eventId))
}
}
private suspend fun updateNotificationCountIfNecessary(roomId: String, eventId: String) {
monarchy.awaitTransaction { realm ->
val isLatestReceived = TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId == eventId val isLatestReceived = TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId == eventId
if (isLatestReceived) { if (isLatestReceived) {
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
?: return@awaitTransaction ?: return@writeAsync
roomSummary.notificationCount = 0 roomSummary.notificationCount = 0
roomSummary.highlightCount = 0 roomSummary.highlightCount = 0
} }
@ -132,17 +106,19 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
} }
private fun isEventRead(roomId: String, eventId: String): Boolean { private fun isEventRead(roomId: String, eventId: String): Boolean {
return Realm.getInstance(monarchy.realmConfiguration).use { realm -> var isEventRead = false
val readReceipt = ReadReceiptEntity.where(realm, roomId, credentials.userId).findFirst() monarchy.doWithRealm {
?: return false val readReceipt = ReadReceiptEntity.where(it, roomId, credentials.userId).findFirst()
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) ?: return@doWithRealm
?: return false val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(it, roomId)
?: return@doWithRealm
val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex
?: Int.MIN_VALUE ?: Int.MIN_VALUE
val eventToCheckIndex = liveChunk.timelineEvents.find(eventId)?.root?.displayIndex val eventToCheckIndex = liveChunk.timelineEvents.find(eventId)?.root?.displayIndex
?: Int.MAX_VALUE ?: Int.MAX_VALUE
eventToCheckIndex <= readReceiptIndex isEventRead = eventToCheckIndex <= readReceiptIndex
} }
return isEventRead
} }
} }

View File

@ -69,15 +69,11 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
.also { .also {
saveLocalEcho(it) saveLocalEcho(it)
} }
val sendRelationWork = createSendRelationWork(event) val sendRelationWork = createSendEventWork(event, true)
TimelineSendEventWorkCommon.postWork(context, roomId, sendRelationWork) TimelineSendEventWorkCommon.postWork(context, roomId, sendRelationWork)
return CancelableWork(context, sendRelationWork.id) return CancelableWork(context, sendRelationWork.id)
} }
private fun createSendRelationWork(event: Event): OneTimeWorkRequest {
return createSendEventWork(event)
}
override fun undoReaction(reaction: String, targetEventId: String, myUserId: String)/*: Cancelable*/ { override fun undoReaction(reaction: String, targetEventId: String, myUserId: String)/*: Cancelable*/ {
val params = FindReactionEventForUndoTask.Params( val params = FindReactionEventForUndoTask.Params(
@ -134,42 +130,42 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
.also { .also {
saveLocalEcho(it) saveLocalEcho(it)
} }
if (cryptoService.isRoomEncrypted(roomId)) { return if (cryptoService.isRoomEncrypted(roomId)) {
val encryptWork = createEncryptEventWork(event, listOf("m.relates_to")) val encryptWork = createEncryptEventWork(event, listOf("m.relates_to"))
val workRequest = createSendEventWork(event) val workRequest = createSendEventWork(event, false)
TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, workRequest) TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, workRequest)
return CancelableWork(context, encryptWork.id) CancelableWork(context, encryptWork.id)
} else { } else {
val workRequest = createSendEventWork(event) val workRequest = createSendEventWork(event, true)
TimelineSendEventWorkCommon.postWork(context, roomId, workRequest) TimelineSendEventWorkCommon.postWork(context, roomId, workRequest)
return CancelableWork(context, workRequest.id) CancelableWork(context, workRequest.id)
} }
} }
override fun editReply(replyToEdit: TimelineEvent, override fun editReply(replyToEdit: TimelineEvent,
originalEvent: TimelineEvent, originalTimelineEvent: TimelineEvent,
newBodyText: String, newBodyText: String,
compatibilityBodyText: String): Cancelable { compatibilityBodyText: String): Cancelable {
val event = eventFactory val event = eventFactory
.createReplaceTextOfReply(roomId, .createReplaceTextOfReply(roomId,
replyToEdit, replyToEdit,
originalEvent, originalTimelineEvent,
newBodyText, true, MessageType.MSGTYPE_TEXT, compatibilityBodyText) newBodyText, true, MessageType.MSGTYPE_TEXT, compatibilityBodyText)
.also { .also {
saveLocalEcho(it) saveLocalEcho(it)
} }
if (cryptoService.isRoomEncrypted(roomId)) { return if (cryptoService.isRoomEncrypted(roomId)) {
val encryptWork = createEncryptEventWork(event, listOf("m.relates_to")) val encryptWork = createEncryptEventWork(event, listOf("m.relates_to"))
val workRequest = createSendEventWork(event) val workRequest = createSendEventWork(event, false)
TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, workRequest) TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, workRequest)
return CancelableWork(context, encryptWork.id) CancelableWork(context, encryptWork.id)
} else { } else {
val workRequest = createSendEventWork(event) val workRequest = createSendEventWork(event, true)
TimelineSendEventWorkCommon.postWork(context, roomId, workRequest) 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) saveLocalEcho(it)
} ?: return null } ?: return null
if (cryptoService.isRoomEncrypted(roomId)) { return if (cryptoService.isRoomEncrypted(roomId)) {
val encryptWork = createEncryptEventWork(event, listOf("m.relates_to")) val encryptWork = createEncryptEventWork(event, listOf("m.relates_to"))
val workRequest = createSendEventWork(event) val workRequest = createSendEventWork(event, false)
TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, workRequest) TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, workRequest)
return CancelableWork(context, encryptWork.id) CancelableWork(context, encryptWork.id)
} else { } else {
val workRequest = createSendEventWork(event) val workRequest = createSendEventWork(event, true)
TimelineSendEventWorkCommon.postWork(context, roomId, workRequest) TimelineSendEventWorkCommon.postWork(context, roomId, workRequest)
return CancelableWork(context, workRequest.id) CancelableWork(context, workRequest.id)
} }
} }
@ -208,10 +204,10 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
return TimelineSendEventWorkCommon.createWork<EncryptEventWorker>(sendWorkData, true) return TimelineSendEventWorkCommon.createWork<EncryptEventWorker>(sendWorkData, true)
} }
private fun createSendEventWork(event: Event): OneTimeWorkRequest { private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
val sendContentWorkerParams = SendEventWorker.Params(credentials.userId, roomId, event) val sendContentWorkerParams = SendEventWorker.Params(credentials.userId, roomId, event)
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
return TimelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, true) return TimelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)
} }
override fun getEventSummaryLive(eventId: String): LiveData<EventAnnotationsSummary> { override fun getEventSummaryLive(eventId: String): LiveData<EventAnnotationsSummary> {

View File

@ -27,15 +27,33 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
import im.vector.matrix.android.api.util.CancelableBag import im.vector.matrix.android.api.util.CancelableBag
import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper 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.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.query.* import im.vector.matrix.android.internal.database.model.ChunkEntityFields
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
import im.vector.matrix.android.internal.database.query.FilterContent
import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates
import im.vector.matrix.android.internal.database.query.findIncludingEvent
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.database.query.whereInRoom
import im.vector.matrix.android.internal.task.TaskConstraints import im.vector.matrix.android.internal.task.TaskConstraints
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.Debouncer import im.vector.matrix.android.internal.util.Debouncer
import im.vector.matrix.android.internal.util.createBackgroundHandler import im.vector.matrix.android.internal.util.createBackgroundHandler
import im.vector.matrix.android.internal.util.createUIHandler 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 timber.log.Timber
import java.util.* import java.util.*
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@ -49,7 +67,7 @@ private const val DISPLAY_INDEX_UNKNOWN = Int.MIN_VALUE
internal class DefaultTimeline( internal class DefaultTimeline(
private val roomId: String, private val roomId: String,
private var initialEventId: String? = null, private val initialEventId: String? = null,
private val realmConfiguration: RealmConfiguration, private val realmConfiguration: RealmConfiguration,
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val contextOfEventTask: GetContextOfEventTask, private val contextOfEventTask: GetContextOfEventTask,
@ -57,9 +75,8 @@ internal class DefaultTimeline(
private val cryptoService: CryptoService, private val cryptoService: CryptoService,
private val timelineEventMapper: TimelineEventMapper, private val timelineEventMapper: TimelineEventMapper,
private val settings: TimelineSettings, private val settings: TimelineSettings,
private val hiddenReadReceipts: TimelineHiddenReadReceipts, private val hiddenReadReceipts: TimelineHiddenReadReceipts
private val hiddenReadMarker: TimelineHiddenReadMarker ) : Timeline, TimelineHiddenReadReceipts.Delegate {
) : Timeline, TimelineHiddenReadReceipts.Delegate, TimelineHiddenReadMarker.Delegate {
private companion object { private companion object {
val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD") val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD")
@ -87,6 +104,7 @@ internal class DefaultTimeline(
private var prevDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN private var prevDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN
private var nextDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN private var nextDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN
private val isLive = initialEventId == null
private val builtEvents = Collections.synchronizedList<TimelineEvent>(ArrayList()) private val builtEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
private val builtEventsIdMap = Collections.synchronizedMap(HashMap<String, Int>()) private val builtEventsIdMap = Collections.synchronizedMap(HashMap<String, Int>())
private val backwardsPaginationState = AtomicReference(PaginationState()) private val backwardsPaginationState = AtomicReference(PaginationState())
@ -94,9 +112,6 @@ internal class DefaultTimeline(
private val timelineID = UUID.randomUUID().toString() private val timelineID = UUID.randomUUID().toString()
override val isLive
get() = initialEventId == null
private val eventDecryptor = TimelineEventDecryptor(realmConfiguration, timelineID, cryptoService) private val eventDecryptor = TimelineEventDecryptor(realmConfiguration, timelineID, cryptoService)
private val eventsChangeListener = OrderedRealmCollectionChangeListener<RealmResults<TimelineEventEntity>> { results, changeSet -> private val eventsChangeListener = OrderedRealmCollectionChangeListener<RealmResults<TimelineEventEntity>> { results, changeSet ->
@ -105,7 +120,10 @@ internal class DefaultTimeline(
} else { } else {
// If changeSet has deletion we are having a gap, so we clear everything // If changeSet has deletion we are having a gap, so we clear everything
if (changeSet.deletionRanges.isNotEmpty()) { if (changeSet.deletionRanges.isNotEmpty()) {
clearAllValues() prevDisplayIndex = DISPLAY_INDEX_UNKNOWN
nextDisplayIndex = DISPLAY_INDEX_UNKNOWN
builtEvents.clear()
builtEventsIdMap.clear()
} }
changeSet.insertionRanges.forEach { range -> changeSet.insertionRanges.forEach { range ->
val (startDisplayIndex, direction) = if (range.startIndex == 0) { val (startDisplayIndex, direction) = if (range.startIndex == 0) {
@ -131,9 +149,13 @@ internal class DefaultTimeline(
changeSet.changes.forEach { index -> changeSet.changes.forEach { index ->
val eventEntity = results[index] val eventEntity = results[index]
eventEntity?.eventId?.let { eventId -> eventEntity?.eventId?.let { eventId ->
hasChanged = rebuildEvent(eventId) { builtEventsIdMap[eventId]?.let { builtIndex ->
buildTimelineEvent(eventEntity) //Update an existing event
} || hasChanged builtEvents[builtIndex]?.let { te ->
builtEvents[builtIndex] = buildTimelineEvent(eventEntity)
hasChanged = true
}
}
} }
} }
if (hasChanged) postSnapshot() if (hasChanged) postSnapshot()
@ -141,17 +163,23 @@ internal class DefaultTimeline(
} }
private val relationsListener = OrderedRealmCollectionChangeListener<RealmResults<EventAnnotationsSummaryEntity>> { collection, changeSet -> private val relationsListener = OrderedRealmCollectionChangeListener<RealmResults<EventAnnotationsSummaryEntity>> { collection, changeSet ->
var hasChange = false var hasChange = false
(changeSet.insertions + changeSet.changes).forEach { (changeSet.insertions + changeSet.changes).forEach {
val eventRelations = collection[it] val eventRelations = collection[it]
if (eventRelations != null) { if (eventRelations != null) {
hasChange = rebuildEvent(eventRelations.eventId) { te -> builtEventsIdMap[eventRelations.eventId]?.let { builtIndex ->
te.copy(annotations = eventRelations.asDomain()) //Update the relation of existing event
} || hasChange builtEvents[builtIndex]?.let { te ->
builtEvents[builtIndex] = te.copy(annotations = eventRelations.asDomain())
hasChange = true
} }
} }
if (hasChange) postSnapshot() }
}
if (hasChange)
postSnapshot()
} }
@ -212,7 +240,7 @@ internal class DefaultTimeline(
if (settings.buildReadReceipts) { if (settings.buildReadReceipts) {
hiddenReadReceipts.start(realm, liveEvents, this) hiddenReadReceipts.start(realm, liveEvents, this)
} }
hiddenReadMarker.start(realm, liveEvents, this)
isReady.set(true) isReady.set(true)
} }
} }
@ -227,11 +255,9 @@ internal class DefaultTimeline(
roomEntity?.sendingTimelineEvents?.removeAllChangeListeners() roomEntity?.sendingTimelineEvents?.removeAllChangeListeners()
eventRelations.removeAllChangeListeners() eventRelations.removeAllChangeListeners()
liveEvents.removeAllChangeListeners() liveEvents.removeAllChangeListeners()
hiddenReadMarker.dispose()
if (settings.buildReadReceipts) { if (settings.buildReadReceipts) {
hiddenReadReceipts.dispose() hiddenReadReceipts.dispose()
} }
clearAllValues()
backgroundRealm.getAndSet(null).also { backgroundRealm.getAndSet(null).also {
it.close() it.close()
} }
@ -239,27 +265,6 @@ internal class DefaultTimeline(
} }
} }
override fun restartWithEventId(eventId: String) {
dispose()
initialEventId = eventId
start()
postSnapshot()
}
override fun getIndexOfEvent(eventId: String?): Int? {
return builtEventsIdMap[eventId]
}
override fun getTimelineEventAtIndex(index: Int): TimelineEvent? {
return builtEvents.getOrNull(index)
}
override fun getTimelineEventWithId(eventId: String?): TimelineEvent? {
return builtEventsIdMap[eventId]?.let {
getTimelineEventAtIndex(it)
}
}
override fun hasMoreToLoad(direction: Timeline.Direction): Boolean { override fun hasMoreToLoad(direction: Timeline.Direction): Boolean {
return hasMoreInCache(direction) || !hasReachedEnd(direction) return hasMoreInCache(direction) || !hasReachedEnd(direction)
} }
@ -267,39 +272,21 @@ internal class DefaultTimeline(
// TimelineHiddenReadReceipts.Delegate // TimelineHiddenReadReceipts.Delegate
override fun rebuildEvent(eventId: String, readReceipts: List<ReadReceipt>): Boolean { override fun rebuildEvent(eventId: String, readReceipts: List<ReadReceipt>): Boolean {
return rebuildEvent(eventId) { te -> return builtEventsIdMap[eventId]?.let { builtIndex ->
te.copy(readReceipts = readReceipts) //Update the relation of existing event
builtEvents[builtIndex]?.let { te ->
builtEvents[builtIndex] = te.copy(readReceipts = readReceipts)
true
} }
} ?: false
} }
override fun onReadReceiptsUpdated() { override fun onReadReceiptsUpdated() {
postSnapshot() postSnapshot()
} }
// TimelineHiddenReadMarker.Delegate
override fun rebuildEvent(eventId: String, hasReadMarker: Boolean): Boolean {
return rebuildEvent(eventId) { te ->
te.copy(hasReadMarker = hasReadMarker)
}
}
override fun onReadMarkerUpdated() {
postSnapshot()
}
// Private methods ***************************************************************************** // Private methods *****************************************************************************
private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean {
return builtEventsIdMap[eventId]?.let { builtIndex ->
//Update the relation of existing event
builtEvents[builtIndex]?.let { te ->
builtEvents[builtIndex] = builder(te)
true
}
} ?: false
}
private fun hasMoreInCache(direction: Timeline.Direction): Boolean { private fun hasMoreInCache(direction: Timeline.Direction): Boolean {
return Realm.getInstance(realmConfiguration).use { localRealm -> return Realm.getInstance(realmConfiguration).use { localRealm ->
val timelineEventEntity = buildEventQuery(localRealm).findFirst(direction) val timelineEventEntity = buildEventQuery(localRealm).findFirst(direction)
@ -408,9 +395,8 @@ internal class DefaultTimeline(
prevDisplayIndex = initialDisplayIndex prevDisplayIndex = initialDisplayIndex
nextDisplayIndex = initialDisplayIndex nextDisplayIndex = initialDisplayIndex
val currentInitialEventId = initialEventId if (initialEventId != null && shouldFetchInitialEvent) {
if (currentInitialEventId != null && shouldFetchInitialEvent) { fetchEvent(initialEventId)
fetchEvent(currentInitialEventId)
} else { } else {
val count = Math.min(settings.initialSize, liveEvents.size) val count = Math.min(settings.initialSize, liveEvents.size)
if (isLive) { if (isLive) {
@ -557,11 +543,10 @@ internal class DefaultTimeline(
} }
private fun findCurrentChunk(realm: Realm): ChunkEntity? { private fun findCurrentChunk(realm: Realm): ChunkEntity? {
val currentInitialEventId = initialEventId return if (initialEventId == null) {
return if (currentInitialEventId == null) {
ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
} else { } else {
ChunkEntity.findIncludingEvent(realm, currentInitialEventId) ChunkEntity.findIncludingEvent(realm, initialEventId)
} }
} }
@ -581,22 +566,10 @@ internal class DefaultTimeline(
} }
private fun postSnapshot() { private fun postSnapshot() {
BACKGROUND_HANDLER.post {
val snapshot = createSnapshot() val snapshot = createSnapshot()
val runnable = Runnable { listener?.onUpdated(snapshot) } val runnable = Runnable { listener?.onUpdated(snapshot) }
debouncer.debounce("post_snapshot", runnable, 50) debouncer.debounce("post_snapshot", runnable, 50)
} }
}
private fun clearAllValues() {
prevDisplayIndex = DISPLAY_INDEX_UNKNOWN
nextDisplayIndex = DISPLAY_INDEX_UNKNOWN
builtEvents.clear()
builtEventsIdMap.clear()
backwardsPaginationState.set(PaginationState())
forwardsPaginationState.set(PaginationState())
}
// Extension methods *************************************************************************** // Extension methods ***************************************************************************

View File

@ -59,8 +59,7 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
cryptoService, cryptoService,
timelineEventMapper, timelineEventMapper,
settings, settings,
TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings), TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings)
TimelineHiddenReadMarker(roomId)
) )
} }

View File

@ -1,96 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.room.timeline
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
import im.vector.matrix.android.internal.database.query.where
import io.realm.Realm
import io.realm.RealmObjectChangeListener
import io.realm.RealmResults
/**
* This class is responsible for handling the read marker for hidden events.
* When an hidden event has read marker, we want to transfer it on the first older displayed event.
* It has to be used in [DefaultTimeline] and we should call the [start] and [dispose] methods to properly handle realm subscription.
*/
internal class TimelineHiddenReadMarker constructor(private val roomId: String) {
interface Delegate {
fun rebuildEvent(eventId: String, hasReadMarker: Boolean): Boolean
fun onReadMarkerUpdated()
}
private var previousDisplayedEventId: String? = null
private var readMarkerEntity: ReadMarkerEntity? = null
private lateinit var liveEvents: RealmResults<TimelineEventEntity>
private lateinit var delegate: Delegate
private val readMarkerListener = RealmObjectChangeListener<ReadMarkerEntity> { readMarker, _ ->
var hasChange = false
previousDisplayedEventId?.also {
hasChange = delegate.rebuildEvent(it, false)
previousDisplayedEventId = null
}
val isEventHidden = liveEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, readMarker.eventId).findFirst() == null
if (isEventHidden) {
val hiddenEvent = readMarker.timelineEvent?.firstOrNull()
?: return@RealmObjectChangeListener
val displayIndex = hiddenEvent.root?.displayIndex
if (displayIndex != null) {
// Then we are looking for the first displayable event after the hidden one
val firstDisplayedEvent = liveEvents.where()
.lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex)
.findFirst()
// If we find one, we should rebuild this one with marker
if (firstDisplayedEvent != null) {
previousDisplayedEventId = firstDisplayedEvent.eventId
hasChange = delegate.rebuildEvent(firstDisplayedEvent.eventId, true)
}
}
}
if (hasChange) delegate.onReadMarkerUpdated()
}
/**
* Start the realm query subscription. Has to be called on an HandlerThread
*/
fun start(realm: Realm, liveEvents: RealmResults<TimelineEventEntity>, delegate: Delegate) {
this.liveEvents = liveEvents
this.delegate = delegate
// We are looking for read receipts set on hidden events.
// We only accept those with a timelineEvent (so coming from pagination/sync).
readMarkerEntity = ReadMarkerEntity.where(realm, roomId = roomId)
.findFirstAsync()
.also { it.addChangeListener(readMarkerListener) }
}
/**
* Dispose the realm query subscription. Has to be called on an HandlerThread
*/
fun dispose() {
this.readMarkerEntity?.removeAllChangeListeners()
}
}

View File

@ -1,51 +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.matrix.android.internal.session.sync
import im.vector.matrix.android.api.session.room.read.FullyReadContent
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.query.getOrCreate
import im.vector.matrix.android.internal.database.query.where
import io.realm.Realm
import timber.log.Timber
import javax.inject.Inject
internal class RoomFullyReadHandler @Inject constructor() {
fun handle(realm: Realm, roomId: String, content: FullyReadContent?) {
if (content == null) {
return
}
Timber.v("Handle for roomId: $roomId eventId: ${content.eventId}")
RoomSummaryEntity.getOrCreate(realm, roomId).apply {
readMarkerId = content.eventId
}
val readMarkerEntity = ReadMarkerEntity.getOrCreate(realm, roomId).apply {
eventId = content.eventId
}
// Remove the old marker if any
readMarkerEntity.timelineEvent?.firstOrNull()?.readMarker = null
// Attach to timelineEvent if known
val timelineEventEntity = TimelineEventEntity.where(realm, eventId = content.eventId).findFirst()
timelineEventEntity?.readMarker = readMarkerEntity
}
}

View File

@ -23,13 +23,8 @@ 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.events.model.toModel
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.tag.RoomTagContent import im.vector.matrix.android.api.session.room.model.tag.RoomTagContent
import im.vector.matrix.android.api.session.room.read.FullyReadContent
import im.vector.matrix.android.internal.crypto.CryptoManager import im.vector.matrix.android.internal.crypto.CryptoManager
import im.vector.matrix.android.internal.database.helper.add import im.vector.matrix.android.internal.database.helper.*
import im.vector.matrix.android.internal.database.helper.addOrUpdate
import im.vector.matrix.android.internal.database.helper.addStateEvent
import im.vector.matrix.android.internal.database.helper.lastStateIndex
import im.vector.matrix.android.internal.database.helper.updateSenderDataFor
import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields 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.RoomEntity
@ -42,11 +37,7 @@ import im.vector.matrix.android.internal.session.notification.DefaultPushRuleSer
import im.vector.matrix.android.internal.session.notification.ProcessEventForPushTask import im.vector.matrix.android.internal.session.notification.ProcessEventForPushTask
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync import im.vector.matrix.android.internal.session.sync.model.*
import im.vector.matrix.android.internal.session.sync.model.RoomSync
import im.vector.matrix.android.internal.session.sync.model.RoomSyncAccountData
import im.vector.matrix.android.internal.session.sync.model.RoomSyncEphemeral
import im.vector.matrix.android.internal.session.sync.model.RoomsSyncResponse
import im.vector.matrix.android.internal.session.user.UserEntityFactory import im.vector.matrix.android.internal.session.user.UserEntityFactory
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
@ -59,7 +50,6 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
private val readReceiptHandler: ReadReceiptHandler, private val readReceiptHandler: ReadReceiptHandler,
private val roomSummaryUpdater: RoomSummaryUpdater, private val roomSummaryUpdater: RoomSummaryUpdater,
private val roomTagHandler: RoomTagHandler, private val roomTagHandler: RoomTagHandler,
private val roomFullyReadHandler: RoomFullyReadHandler,
private val cryptoManager: CryptoManager, private val cryptoManager: CryptoManager,
private val tokenStore: SyncTokenStore, private val tokenStore: SyncTokenStore,
private val pushRuleService: DefaultPushRuleService, private val pushRuleService: DefaultPushRuleService,
@ -257,16 +247,11 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
} }
private fun handleRoomAccountDataEvents(realm: Realm, roomId: String, accountData: RoomSyncAccountData) { private fun handleRoomAccountDataEvents(realm: Realm, roomId: String, accountData: RoomSyncAccountData) {
for (event in accountData.events) { accountData.events
val eventType = event.getClearType() .asSequence()
if (eventType == EventType.TAG) { .filter { it.getClearType() == EventType.TAG }
val content = event.getClearContent().toModel<RoomTagContent>() .map { it.content.toModel<RoomTagContent>() }
roomTagHandler.handle(realm, roomId, content) .forEach { roomTagHandler.handle(realm, roomId, it) }
} else if (eventType == EventType.FULLY_READ) {
val content = event.getClearContent().toModel<FullyReadContent>()
roomFullyReadHandler.handle(realm, roomId, content)
}
}
} }
} }

View File

@ -167,4 +167,9 @@
<string name="initial_sync_start_importing_account_data">Начална синхронизация: <string name="initial_sync_start_importing_account_data">Начална синхронизация:
\nИмпортиране на данни за профила</string> \nИмпортиране на данни за профила</string>
<string name="notice_room_update">%s обнови тази стая.</string>
<string name="event_status_sending_message">Изпращане на съобщение…</string>
<string name="clear_timeline_send_queue">Изчисти опашката за изпращане</string>
</resources> </resources>

View File

@ -105,4 +105,65 @@
<string name="verification_emoji_pig">Schwein</string> <string name="verification_emoji_pig">Schwein</string>
<string name="verification_emoji_elephant">Elefant</string> <string name="verification_emoji_elephant">Elefant</string>
<string name="verification_emoji_rabbit">Hase</string> <string name="verification_emoji_rabbit">Hase</string>
<string name="notice_room_update">%s hat diesen Raum aufgewertet.</string>
<string name="verification_emoji_panda">Panda</string>
<string name="verification_emoji_rooster">Hahn</string>
<string name="verification_emoji_penguin">Pinguin</string>
<string name="verification_emoji_turtle">Schildkröte</string>
<string name="verification_emoji_fish">Fisch</string>
<string name="verification_emoji_octopus">Tintenfisch</string>
<string name="verification_emoji_butterfly">Schmetterling</string>
<string name="verification_emoji_flower">Blume</string>
<string name="verification_emoji_tree">Baum</string>
<string name="verification_emoji_cactus">Kaktus</string>
<string name="verification_emoji_mushroom">Pilz</string>
<string name="verification_emoji_globe">Globus</string>
<string name="verification_emoji_moon">Mond</string>
<string name="verification_emoji_cloud">Wolke</string>
<string name="verification_emoji_fire">Feuer</string>
<string name="verification_emoji_banana">Banane</string>
<string name="verification_emoji_apple">Apfel</string>
<string name="verification_emoji_strawberry">Erdbeere</string>
<string name="verification_emoji_corn">Mais</string>
<string name="verification_emoji_cake">Kuchen</string>
<string name="verification_emoji_heart">Herz</string>
<string name="verification_emoji_smiley">Lächeln</string>
<string name="verification_emoji_robot">Roboter</string>
<string name="verification_emoji_hat">Hut</string>
<string name="verification_emoji_glasses">Brille</string>
<string name="verification_emoji_wrench">Schraubenschlüssel</string>
<string name="verification_emoji_santa">Nikolaus</string>
<string name="verification_emoji_thumbsup">Daumen hoch</string>
<string name="verification_emoji_umbrella">Regenschirm</string>
<string name="verification_emoji_hourglass">Sanduhr</string>
<string name="verification_emoji_clock">Uhr</string>
<string name="verification_emoji_gift">Geschenk</string>
<string name="verification_emoji_lightbulb">Glühbirne</string>
<string name="verification_emoji_book">Buch</string>
<string name="verification_emoji_pencil">Stift</string>
<string name="verification_emoji_paperclip">Büroklammer</string>
<string name="verification_emoji_scissors">Scheren</string>
<string name="verification_emoji_lock">sperren</string>
<string name="verification_emoji_key">Schlüssel</string>
<string name="verification_emoji_hammer">Hammer</string>
<string name="verification_emoji_telephone">Telefon</string>
<string name="verification_emoji_flag">Flagge</string>
<string name="verification_emoji_train">Zug</string>
<string name="verification_emoji_bicycle">Fahrrad</string>
<string name="verification_emoji_airplane">Flugzeug</string>
<string name="verification_emoji_rocket">Rakete</string>
<string name="verification_emoji_trophy">Pokal</string>
<string name="verification_emoji_ball">Ball</string>
<string name="verification_emoji_guitar">Gitarre</string>
<string name="verification_emoji_trumpet">Trompete</string>
<string name="verification_emoji_bell">Glocke</string>
<string name="verification_emoji_anchor">Anker</string>
<string name="verification_emoji_headphone">Kopfhörer</string>
<string name="verification_emoji_folder">Ordner</string>
<string name="verification_emoji_pin">Stecknadel</string>
<string name="event_status_sending_message">Sende eine Nachricht…</string>
<string name="clear_timeline_send_queue">Sendewarteschlange leeren</string>
</resources> </resources>

View File

@ -167,4 +167,9 @@
<string name="initial_sync_start_importing_account_data">Hasierako sinkronizazioa: <string name="initial_sync_start_importing_account_data">Hasierako sinkronizazioa:
\nKontuaren datuak inportatzen</string> \nKontuaren datuak inportatzen</string>
<string name="notice_room_update">%s erabiltzaileak gela hau eguneratu du.</string>
<string name="event_status_sending_message">Mezua bidaltzen…</string>
<string name="clear_timeline_send_queue">Garbitu bidalketa-ilara</string>
</resources> </resources>

View File

@ -168,4 +168,9 @@
<string name="initial_sync_start_importing_account_data">Alkusynkronointi: <string name="initial_sync_start_importing_account_data">Alkusynkronointi:
\nTuodaan tilin tietoja</string> \nTuodaan tilin tietoja</string>
<string name="notice_room_update">%s päivitti tämän huoneen.</string>
<string name="event_status_sending_message">Lähetetään viestiä…</string>
<string name="clear_timeline_send_queue">Tyhjennä lähetysjono</string>
</resources> </resources>

View File

@ -167,4 +167,9 @@
<string name="initial_sync_start_importing_account_data">Synchronisation initiale : <string name="initial_sync_start_importing_account_data">Synchronisation initiale :
\nImportation des données du compte</string> \nImportation des données du compte</string>
<string name="notice_room_update">%s a mis à niveau ce salon.</string>
<string name="event_status_sending_message">Envoi du message…</string>
<string name="clear_timeline_send_queue">Vider la file denvoi</string>
</resources> </resources>

View File

@ -166,4 +166,9 @@
<string name="initial_sync_start_importing_account_data">Induló szinkronizáció: <string name="initial_sync_start_importing_account_data">Induló szinkronizáció:
\nFiók adatok betöltése</string> \nFiók adatok betöltése</string>
<string name="notice_room_update">%s frissítette ezt a szobát.</string>
<string name="event_status_sending_message">Üzenet küldése…</string>
<string name="clear_timeline_send_queue">Küldő sor ürítése</string>
</resources> </resources>

View File

@ -167,4 +167,9 @@
<string name="initial_sync_start_importing_account_data">Sync iniziale: <string name="initial_sync_start_importing_account_data">Sync iniziale:
\nImportazione dati account</string> \nImportazione dati account</string>
<string name="notice_room_update">%s ha aggiornato questa stanza.</string>
<string name="event_status_sending_message">Invio messaggio in corso …</string>
<string name="clear_timeline_send_queue">Cancella la coda di invio</string>
</resources> </resources>

View File

@ -1,6 +1,173 @@
<?xml version='1.0' encoding='UTF-8'?> <?xml version='1.0' encoding='UTF-8'?>
<resources> <resources>
<string name="summary_message">%1$s: %2$s</string> <string name="summary_message">%1$s: %2$s</string>
<string name="notice_room_invite_no_invitee">%s\'의 초대</string> <string name="notice_room_invite_no_invitee">%s의 초대</string>
<string name="verification_emoji_headphone">헤드폰</string> <string name="verification_emoji_headphone">헤드폰</string>
<string name="summary_user_sent_image">%1$s님이 사진을 보냈습니다.</string>
<string name="summary_user_sent_sticker">%1$s님이 스티커를 보냈습니다.</string>
<string name="notice_room_invite">%1$s님이 %2$s님을 초대했습니다</string>
<string name="notice_room_invite_you">%1$s님이 당신을 초대했습니다</string>
<string name="notice_room_join">%1$s님이 참가했습니다</string>
<string name="notice_room_leave">%1$s님이 떠났습니다</string>
<string name="notice_room_reject">%1$s님이 초대를 거부했습니다</string>
<string name="notice_room_kick">%1$s님이 %2$s님을 추방했습니다</string>
<string name="notice_room_unban">%1$s님이 %2$s님의 차단을 풀었습니다</string>
<string name="notice_room_ban">%1$s님이 %2$s님을 차단했습니다</string>
<string name="notice_room_withdraw">%1$s님이 %2$s님의 초대를 취소했습니다</string>
<string name="notice_avatar_url_changed">%1$s님이 아바타를 변경했습니다</string>
<string name="notice_display_name_set">%1$s님이 표시 이름을 %2$s(으)로 설정했습니다</string>
<string name="notice_display_name_changed_from">%1$s님이 표시 이름을 %2$s에서 %3$s(으)로 변경했습니다</string>
<string name="notice_display_name_removed">%1$s님이 표시 이름을 삭제했습니다 (%2$s)</string>
<string name="notice_room_topic_changed">%1$s님이 주제를 다음으로 변경했습니다: %2$s</string>
<string name="notice_room_name_changed">%1$s님이 방 이름을 다음으로 변경했습니다: %2$s</string>
<string name="notice_placed_video_call">%s님이 영상 통화를 걸었습니다.</string>
<string name="notice_placed_voice_call">%s님이 음성 통화를 걸었습니다.</string>
<string name="notice_answered_call">%s님이 전화를 받았습니다.</string>
<string name="notice_ended_call">%s님이 전화를 끊었습니다.</string>
<string name="notice_made_future_room_visibility">%1$s님이 이후 %2$s에게 방 기록을 공개했습니다</string>
<string name="notice_room_visibility_invited">초대된 시점부터 모든 방 구성원.</string>
<string name="notice_room_visibility_joined">들어온 시점부터 모든 방 구성원.</string>
<string name="notice_room_visibility_shared">모든 방 구성원.</string>
<string name="notice_room_visibility_world_readable">누구나.</string>
<string name="notice_room_visibility_unknown">알 수 없음 (%s).</string>
<string name="notice_end_to_end">%1$s님이 종단 간 암호화를 켰습니다 (%2$s)</string>
<string name="notice_room_update">%s님이 방을 업그레이드했습니다.</string>
<string name="notice_requested_voip_conference">%1$s님이 VoIP 회의를 요청했습니다</string>
<string name="notice_voip_started">VoIP 회의가 시작했습니다</string>
<string name="notice_voip_finished">VoIP 회의가 끝났습니다</string>
<string name="notice_avatar_changed_too">(아바타도 변경됨)</string>
<string name="notice_room_name_removed">%1$s님이 방 이름을 삭제했습니다</string>
<string name="notice_room_topic_removed">%1$s님이 방 주제를 삭제했습니다</string>
<string name="notice_event_redacted">메시지가 삭제되었습니다</string>
<string name="notice_event_redacted_by">메시지가 %1$s님에 의해 삭제되었습니다</string>
<string name="notice_event_redacted_with_reason">메시지가 삭제되었습니다 [이유: %1$s]</string>
<string name="notice_event_redacted_by_with_reason">메시지가 %1$s님에 의해 삭제되었습니다 [이유: %2$s]</string>
<string name="notice_profile_change_redacted">%1$s님이 프로필 %2$s을(를) 업데이트했습니다</string>
<string name="notice_room_third_party_invite">%1$s님이 %2$s님에게 방 초대를 보냈습니다</string>
<string name="notice_room_third_party_registered_invite">%1$s님이 %2$s의 초대를 수락했습니다</string>
<string name="notice_crypto_unable_to_decrypt">** 암호를 해독할 수 없음: %s **</string>
<string name="notice_crypto_error_unkwown_inbound_session_id">발신인의 기기에서 이 메시지의 키를 보내지 않았습니다.</string>
<string name="message_reply_to_prefix">이 답장의 질문</string>
<string name="could_not_redact">검열할 수 없습니다</string>
<string name="unable_to_send_message">메시지를 보낼 수 없습니다</string>
<string name="message_failed_to_upload">사진 업로드에 실패했습니다</string>
<string name="network_error">네트워크 오류</string>
<string name="matrix_error">Matrix 오류</string>
<string name="room_error_join_failed_empty_room">현재 빈 방에 다시 들어갈 수 없습니다.</string>
<string name="encrypted_message">암호화된 메시지</string>
<string name="medium_email">이메일 주소</string>
<string name="medium_phone_number">전화번호</string>
<string name="reply_to_an_image">사진을 보냈습니다.</string>
<string name="reply_to_a_video">동영상을 보냈습니다.</string>
<string name="reply_to_an_audio_file">오디오 파일을 보냈습니다.</string>
<string name="reply_to_a_file">파일을 보냈습니다.</string>
<string name="room_displayname_invite_from">%s에서 초대함</string>
<string name="room_displayname_room_invite">방 초대</string>
<string name="room_displayname_two_members">%1$s님과 %2$s님</string>
<plurals name="room_displayname_three_and_more_members">
<item quantity="other">%1$s님 외 %2$d명</item>
</plurals>
<string name="room_displayname_empty_room">빈 방</string>
<string name="verification_emoji_dog">개</string>
<string name="verification_emoji_cat">고양이</string>
<string name="verification_emoji_lion">사자</string>
<string name="verification_emoji_horse">말</string>
<string name="verification_emoji_unicorn">유니콘</string>
<string name="verification_emoji_pig">돼지</string>
<string name="verification_emoji_elephant">코끼리</string>
<string name="verification_emoji_rabbit">토끼</string>
<string name="verification_emoji_panda">판다</string>
<string name="verification_emoji_rooster">수탉</string>
<string name="verification_emoji_penguin">펭귄</string>
<string name="verification_emoji_turtle">거북</string>
<string name="verification_emoji_fish">물고기</string>
<string name="verification_emoji_octopus">문어</string>
<string name="verification_emoji_butterfly">나비</string>
<string name="verification_emoji_flower">꽃</string>
<string name="verification_emoji_tree">나무</string>
<string name="verification_emoji_cactus">선인장</string>
<string name="verification_emoji_mushroom">버섯</string>
<string name="verification_emoji_globe">지구본</string>
<string name="verification_emoji_moon">달</string>
<string name="verification_emoji_cloud">구름</string>
<string name="verification_emoji_fire">불</string>
<string name="verification_emoji_banana">바나나</string>
<string name="verification_emoji_apple">사과</string>
<string name="verification_emoji_strawberry">딸기</string>
<string name="verification_emoji_corn">옥수수</string>
<string name="verification_emoji_pizza">피자</string>
<string name="verification_emoji_cake">케이크</string>
<string name="verification_emoji_heart">하트</string>
<string name="verification_emoji_smiley">웃음</string>
<string name="verification_emoji_robot">로봇</string>
<string name="verification_emoji_hat">모자</string>
<string name="verification_emoji_glasses">안경</string>
<string name="verification_emoji_wrench">스패너</string>
<string name="verification_emoji_santa">산타클로스</string>
<string name="verification_emoji_thumbsup">좋아요</string>
<string name="verification_emoji_umbrella">우산</string>
<string name="verification_emoji_hourglass">모래시계</string>
<string name="verification_emoji_clock">시계</string>
<string name="verification_emoji_gift">선물</string>
<string name="verification_emoji_lightbulb">전구</string>
<string name="verification_emoji_book">책</string>
<string name="verification_emoji_pencil">연필</string>
<string name="verification_emoji_paperclip">클립</string>
<string name="verification_emoji_scissors">가위</string>
<string name="verification_emoji_lock">자물쇠</string>
<string name="verification_emoji_key">열쇠</string>
<string name="verification_emoji_hammer">망치</string>
<string name="verification_emoji_telephone">전화기</string>
<string name="verification_emoji_flag">깃발</string>
<string name="verification_emoji_train">기차</string>
<string name="verification_emoji_bicycle">자전거</string>
<string name="verification_emoji_airplane">비행기</string>
<string name="verification_emoji_rocket">로켓</string>
<string name="verification_emoji_trophy">트로피</string>
<string name="verification_emoji_ball">공</string>
<string name="verification_emoji_guitar">기타</string>
<string name="verification_emoji_trumpet">트럼펫</string>
<string name="verification_emoji_bell">종</string>
<string name="verification_emoji_anchor">닻</string>
<string name="verification_emoji_folder">폴더</string>
<string name="verification_emoji_pin">핀</string>
<string name="initial_sync_start_importing_account">초기 동기화:
\n계정 가져오는 중…</string>
<string name="initial_sync_start_importing_account_crypto">초기 동기화:
\n암호 가져오는 중</string>
<string name="initial_sync_start_importing_account_rooms">초기 동기화:
\n방 가져오는 중</string>
<string name="initial_sync_start_importing_account_joined_rooms">초기 동기화:
\n들어간 방 가져오는 중</string>
<string name="initial_sync_start_importing_account_invited_rooms">초기 동기화:
\n초대받은 방 가져오는 중</string>
<string name="initial_sync_start_importing_account_left_rooms">초기 동기화:
\n떠난 방 가져오는 중</string>
<string name="initial_sync_start_importing_account_groups">초기 동기화:
\n커뮤니티 가져오는 중</string>
<string name="initial_sync_start_importing_account_data">초기 동기화:
\n계정 데이터 가져오는 중</string>
<string name="event_status_sending_message">메시지 보내는 중…</string>
<string name="clear_timeline_send_queue">전송 대기 열 지우기</string>
</resources> </resources>

View File

@ -176,4 +176,9 @@
<string name="initial_sync_start_importing_account_data">Initiële synchronisatie: <string name="initial_sync_start_importing_account_data">Initiële synchronisatie:
\nAccountgegevens worden geïmporteerd</string> \nAccountgegevens worden geïmporteerd</string>
<string name="notice_room_update">%s heeft dit gesprek opgewaardeerd.</string>
<string name="event_status_sending_message">Bericht wordt verstuurd…</string>
<string name="clear_timeline_send_queue">Uitgaande wachtrij legen</string>
</resources> </resources>

View File

@ -7,7 +7,7 @@
<string name="notice_room_invite">%1$s zaprosił(a) %2$s</string> <string name="notice_room_invite">%1$s zaprosił(a) %2$s</string>
<string name="notice_room_invite_you">%1$s zaprosił(a) Cię</string> <string name="notice_room_invite_you">%1$s zaprosił(a) Cię</string>
<string name="notice_room_join">%1$s dołączył(a)</string> <string name="notice_room_join">%1$s dołączył(a)</string>
<string name="notice_room_leave">%1$s wyszedł(-ła)</string> <string name="notice_room_leave">%1$s opuścił(a)</string>
<string name="notice_room_reject">%1$s odrzucił(a) zaproszenie</string> <string name="notice_room_reject">%1$s odrzucił(a) zaproszenie</string>
<string name="notice_room_kick">%1$s wyrzucił(a) %2$s</string> <string name="notice_room_kick">%1$s wyrzucił(a) %2$s</string>
<string name="notice_room_unban">%1$s odblokował(a) %2$s</string> <string name="notice_room_unban">%1$s odblokował(a) %2$s</string>
@ -17,11 +17,11 @@
<string name="notice_display_name_changed_from">%1$s zmienił(a) wyświetlaną nazwę z %2$s na %3$s</string> <string name="notice_display_name_changed_from">%1$s zmienił(a) wyświetlaną nazwę z %2$s na %3$s</string>
<string name="notice_display_name_removed">%1$s usunął(-ęła) swoją wyświetlaną nazwę (%2$s)</string> <string name="notice_display_name_removed">%1$s usunął(-ęła) swoją wyświetlaną nazwę (%2$s)</string>
<string name="notice_room_topic_changed">%1$s zmienił(a) temat na: %2$s</string> <string name="notice_room_topic_changed">%1$s zmienił(a) temat na: %2$s</string>
<string name="unable_to_send_message">Nie udało się wysłać wiadomości</string> <string name="unable_to_send_message">Nie można wysłać wiadomości</string>
<string name="message_failed_to_upload">Nie udało się wysłać zdjęcia</string> <string name="message_failed_to_upload">Przesyłanie zdjęcia nie powiodło się</string>
<string name="network_error">ogólne błędy</string> <string name="network_error">Błąd sieci</string>
<string name="matrix_error">Błąd Matrixa</string> <string name="matrix_error">Błąd Matrixa</string>
<string name="encrypted_message">Wiadomość zaszyfrowana</string> <string name="encrypted_message">Wiadomość zaszyfrowana</string>
@ -31,7 +31,7 @@
<string name="notice_room_visibility_shared">wszyscy członkowie pokoju.</string> <string name="notice_room_visibility_shared">wszyscy członkowie pokoju.</string>
<string name="notice_room_visibility_world_readable">wszyscy.</string> <string name="notice_room_visibility_world_readable">wszyscy.</string>
<string name="notice_room_name_changed">%1$s zmienił(a) znawę pokoju na: %2$s</string> <string name="notice_room_name_changed">%1$s zmienił(a) nazwę pokoju na: %2$s</string>
<string name="notice_ended_call">%s zakończył(a) rozmowę.</string> <string name="notice_ended_call">%s zakończył(a) rozmowę.</string>
<string name="notice_room_name_removed">%1$s usunął(-ęła) nazwę pokoju</string> <string name="notice_room_name_removed">%1$s usunął(-ęła) nazwę pokoju</string>
<string name="notice_room_topic_removed">%1$s usunął(-ęła) temat pokoju</string> <string name="notice_room_topic_removed">%1$s usunął(-ęła) temat pokoju</string>
@ -57,9 +57,9 @@
</plurals> </plurals>
<string name="notice_crypto_unable_to_decrypt">** Nie można odszyfrować: %s **</string> <string name="notice_crypto_unable_to_decrypt">** Nie można odszyfrować: %s **</string>
<string name="notice_placed_video_call">%s umieścił wideo rozmowe.</string> <string name="notice_placed_video_call">%s wykonał(a) rozmowę wideo.</string>
<string name="notice_placed_voice_call">%s umieścił połączenie głosowe.</string> <string name="notice_placed_voice_call">%s wykonał(a) połączenie głosowe.</string>
<string name="notice_made_future_room_visibility">%1$s uczynił historię pokoju widoczną do %2$s</string> <string name="notice_made_future_room_visibility">%1$s uczynił(a) przyszłą historię pokoju widoczną dla %2$s</string>
<string name="notice_room_visibility_invited">wszyscy członkowie pokoju, od momentu w którym zostali zaproszeni.</string> <string name="notice_room_visibility_invited">wszyscy członkowie pokoju, od momentu w którym zostali zaproszeni.</string>
<string name="notice_room_visibility_joined">wszyscy członkowie pokoju, od momentu w którym dołączyli.</string> <string name="notice_room_visibility_joined">wszyscy członkowie pokoju, od momentu w którym dołączyli.</string>
<string name="notice_room_visibility_unknown">nieznane (%s).</string> <string name="notice_room_visibility_unknown">nieznane (%s).</string>
@ -147,4 +147,29 @@
<string name="verification_emoji_santa">Mikołaj</string> <string name="verification_emoji_santa">Mikołaj</string>
<string name="verification_emoji_gift">Prezent</string> <string name="verification_emoji_gift">Prezent</string>
<string name="verification_emoji_hammer">Młotek</string> <string name="verification_emoji_hammer">Młotek</string>
<string name="notice_room_update">%s zakutalizował(a) ten pokój.</string>
<string name="verification_emoji_thumbsup">Kciuk w górę</string>
<string name="verification_emoji_lock">Zamek</string>
<string name="verification_emoji_ball">Piłka</string>
<string name="initial_sync_start_importing_account">Synchronizacja początkowa:
\nImportowanie konta…</string>
<string name="initial_sync_start_importing_account_crypto">Synchronizacja początkowa:
\nImportowanie kryptografii</string>
<string name="initial_sync_start_importing_account_rooms">Synchronizacja początkowa:
\nImportowanie Pokoi</string>
<string name="initial_sync_start_importing_account_joined_rooms">Synchronizacja początkowa:
\nImportowanie dołączonych Pokoi</string>
<string name="initial_sync_start_importing_account_invited_rooms">Synchronizacja początkowa:
\nImportowanie zaproszonych Pokoi</string>
<string name="initial_sync_start_importing_account_left_rooms">Synchronizacja początkowa:
\nImportowanie opuszczonych Pokoi</string>
<string name="initial_sync_start_importing_account_groups">Synchronizacja początkowa:
\nImportowanie Społeczności</string>
<string name="initial_sync_start_importing_account_data">Synchronizacja początkowa:
\nImportowanie danych Konta</string>
<string name="event_status_sending_message">Wysyłanie wiadomości…</string>
<string name="clear_timeline_send_queue">Wyczyść kolejkę wysyłania</string>
</resources> </resources>

View File

@ -78,4 +78,10 @@
<string name="room_displayname_empty_room">Sala vazia</string> <string name="room_displayname_empty_room">Sala vazia</string>
<string name="summary_user_sent_sticker">%1$s enviou um sticker.</string>
<string name="notice_room_update">%s fez o upgrade da sala.</string>
<string name="notice_event_redacted">Mensagem removida</string>
<string name="notice_event_redacted_by">Mensagem removida por %1$s</string>
</resources> </resources>

View File

@ -169,9 +169,9 @@
\nИмпорт криптографии</string> \nИмпорт криптографии</string>
<string name="initial_sync_start_importing_account_rooms">Начальная синхронизация: <string name="initial_sync_start_importing_account_rooms">Начальная синхронизация:
\nИмпорт комнат</string> \nИмпорт комнат</string>
<string name="initial_sync_start_importing_account_joined_rooms">Начальная синхронизация: <string name="initial_sync_start_importing_account_joined_rooms">Синхронизация начата:
\nИмпорт присоединенных комнат</string> \nИмпорт присоединенных комнат</string>
<string name="initial_sync_start_importing_account_invited_rooms">Начальная синхронизация: <string name="initial_sync_start_importing_account_invited_rooms">Синхронизация начата:
\nИмпорт приглашенных комнат</string> \nИмпорт приглашенных комнат</string>
<string name="initial_sync_start_importing_account_left_rooms">Начальная синхронизация: <string name="initial_sync_start_importing_account_left_rooms">Начальная синхронизация:
\nИмпорт покинутых комнат</string> \nИмпорт покинутых комнат</string>
@ -180,4 +180,9 @@
<string name="initial_sync_start_importing_account_data">Начальная синхронизация: <string name="initial_sync_start_importing_account_data">Начальная синхронизация:
\nИмпорт данных учетной записи</string> \nИмпорт данных учетной записи</string>
<string name="notice_room_update">%s обновил эту комнату.</string>
<string name="event_status_sending_message">Отправка сообщения…</string>
<string name="clear_timeline_send_queue">Очистить очередь отправки</string>
</resources> </resources>

View File

@ -82,4 +82,95 @@
</plurals> </plurals>
<string name="notice_room_update">%s aktualizoval túto miestnosť.</string>
<string name="notice_event_redacted">Správa odstránená</string>
<string name="notice_event_redacted_by">Správa odstránená používateľom %1$s</string>
<string name="notice_event_redacted_with_reason">Správa odstránená [dôvod: %1$s]</string>
<string name="notice_event_redacted_by_with_reason">Správa odstránená používateľom %1$s [dôvod: %2$s]</string>
<string name="verification_emoji_dog">Pes</string>
<string name="verification_emoji_cat">Mačka</string>
<string name="verification_emoji_lion">Lev</string>
<string name="verification_emoji_horse">Kôň</string>
<string name="verification_emoji_unicorn">Jednorožec</string>
<string name="verification_emoji_pig">Prasa</string>
<string name="verification_emoji_elephant">Slon</string>
<string name="verification_emoji_rabbit">Zajac</string>
<string name="verification_emoji_panda">Panda</string>
<string name="verification_emoji_rooster">Kohút</string>
<string name="verification_emoji_penguin">Tučniak</string>
<string name="verification_emoji_turtle">Korytnačka</string>
<string name="verification_emoji_fish">Ryba</string>
<string name="verification_emoji_octopus">Chobotnica</string>
<string name="verification_emoji_butterfly">Motýľ</string>
<string name="verification_emoji_flower">Kvetina</string>
<string name="verification_emoji_tree">Strom</string>
<string name="verification_emoji_cactus">Kaktus</string>
<string name="verification_emoji_mushroom">Hríb</string>
<string name="verification_emoji_globe">Zemeguľa</string>
<string name="verification_emoji_moon">Mesiac</string>
<string name="verification_emoji_cloud">Oblak</string>
<string name="verification_emoji_fire">Oheň</string>
<string name="verification_emoji_banana">Banán</string>
<string name="verification_emoji_apple">Jablko</string>
<string name="verification_emoji_strawberry">Jahoda</string>
<string name="verification_emoji_corn">Kukurica</string>
<string name="verification_emoji_pizza">Pizza</string>
<string name="verification_emoji_cake">Koláč</string>
<string name="verification_emoji_heart">Srdce</string>
<string name="verification_emoji_smiley">Úsmev</string>
<string name="verification_emoji_robot">Robot</string>
<string name="verification_emoji_hat">Klobúk</string>
<string name="verification_emoji_glasses">Okuliare</string>
<string name="verification_emoji_wrench">Skrutkovač</string>
<string name="verification_emoji_santa">Mikuláš</string>
<string name="verification_emoji_thumbsup">Palec nahor</string>
<string name="verification_emoji_umbrella">Dáždnik</string>
<string name="verification_emoji_hourglass">Presýpacie hodiny</string>
<string name="verification_emoji_clock">Hodiny</string>
<string name="verification_emoji_gift">Darček</string>
<string name="verification_emoji_lightbulb">Žiarovka</string>
<string name="verification_emoji_book">Kniha</string>
<string name="verification_emoji_pencil">Ceruzka</string>
<string name="verification_emoji_paperclip">Kancelárska sponka</string>
<string name="verification_emoji_scissors">Nožnice</string>
<string name="verification_emoji_lock">Zámok</string>
<string name="verification_emoji_key">Kľúč</string>
<string name="verification_emoji_hammer">Kladivo</string>
<string name="verification_emoji_telephone">Telefón</string>
<string name="verification_emoji_flag">Vlajka</string>
<string name="verification_emoji_train">Vlak</string>
<string name="verification_emoji_bicycle">Bicykel</string>
<string name="verification_emoji_airplane">Lietadlo</string>
<string name="verification_emoji_rocket">Raketa</string>
<string name="verification_emoji_trophy">Trofej</string>
<string name="verification_emoji_ball">Lopta</string>
<string name="verification_emoji_guitar">Gitara</string>
<string name="verification_emoji_trumpet">Trúbka</string>
<string name="verification_emoji_bell">Zvonček</string>
<string name="verification_emoji_anchor">Kotva</string>
<string name="verification_emoji_headphone">Schlúchadlá</string>
<string name="verification_emoji_folder">Priečinok</string>
<string name="verification_emoji_pin">Pin</string>
<string name="initial_sync_start_importing_account">Úvodná synchronizácia:
\nPrebieha import účtu…</string>
<string name="initial_sync_start_importing_account_crypto">Úvodná synchronizácia:
\nPrebieha import šifrovacích kľúčov</string>
<string name="initial_sync_start_importing_account_rooms">Úvodná synchronizácia:
\nPrebieha import miestností</string>
<string name="initial_sync_start_importing_account_joined_rooms">Úvodná synchronizácia:
\nPrebieha import miestností, do ktorých ste vstúpili</string>
<string name="initial_sync_start_importing_account_invited_rooms">Úvodná synchronizácia:
\nPrebieha import pozvánok</string>
<string name="initial_sync_start_importing_account_left_rooms">Úvodná synchronizácia:
\nPrebieha import opustených miestností</string>
<string name="initial_sync_start_importing_account_groups">Úvodná synchronizácia:
\nPrebieha import komunít</string>
<string name="initial_sync_start_importing_account_data">Úvodná synchronizácia:
\nPrebieha import údajov účtu</string>
<string name="event_status_sending_message">Odosielanie správy…</string>
<string name="clear_timeline_send_queue">Vymazať správy na odoslanie</string>
</resources> </resources>

View File

@ -146,4 +146,26 @@
<string name="verification_emoji_anchor">Spirancë</string> <string name="verification_emoji_anchor">Spirancë</string>
<string name="verification_emoji_headphone">Kufje</string> <string name="verification_emoji_headphone">Kufje</string>
<string name="verification_emoji_folder">Dosje</string> <string name="verification_emoji_folder">Dosje</string>
<string name="notice_room_update">%s e përmirësoi këtë dhomë.</string>
<string name="initial_sync_start_importing_account">Njëkohësimi Fillestar:
\nPo importohet llogaria…</string>
<string name="initial_sync_start_importing_account_crypto">Njëkohësimi Fillestar:
\nPo importohet kriptografi</string>
<string name="initial_sync_start_importing_account_rooms">Njëkohësimi Fillestar:
\nPo importohen Dhoma</string>
<string name="initial_sync_start_importing_account_joined_rooms">Njëkohësimi Fillestar:
\nPo importohen Dhoma Ku Është Bërë Hyrje</string>
<string name="initial_sync_start_importing_account_invited_rooms">Njëkohësimi Fillestar:
\nPo importohen Dhoma Me Ftesë</string>
<string name="initial_sync_start_importing_account_left_rooms">Njëkohësimi Fillestar:
\nPo importohen Dhoma të Braktisura</string>
<string name="initial_sync_start_importing_account_groups">Njëkohësimi Fillestar:
\nPo importohen Bashkësi</string>
<string name="initial_sync_start_importing_account_data">Njëkohësimi Fillestar:
\nPo importohet të Dhëna Llogarie</string>
<string name="event_status_sending_message">Po dërgohet mesazh…</string>
<string name="clear_timeline_send_queue">Spastro radhë pritjeje</string>
</resources> </resources>

View File

@ -48,7 +48,7 @@
<string name="notice_room_third_party_registered_invite">%1$s èt duutnodigienge vo %2$s anveird</string> <string name="notice_room_third_party_registered_invite">%1$s èt duutnodigienge vo %2$s anveird</string>
<string name="notice_crypto_unable_to_decrypt">** Kun nie ountsleuteln: %s **</string> <string name="notice_crypto_unable_to_decrypt">** Kun nie ountsleuteln: %s **</string>
<string name="notice_crypto_error_unkwown_inbound_session_id">t Toestel van den afzender èt geen sleutels vo dit bericht gesteurd.</string> <string name="notice_crypto_error_unkwown_inbound_session_id">t Toestel van den afzender èt geen sleutels vo da bericht hier gesteurd.</string>
<string name="message_reply_to_prefix">Als antwoord ip</string> <string name="message_reply_to_prefix">Als antwoord ip</string>
@ -150,21 +150,26 @@
<string name="verification_emoji_folder">Mappe</string> <string name="verification_emoji_folder">Mappe</string>
<string name="verification_emoji_pin">Pinne</string> <string name="verification_emoji_pin">Pinne</string>
<string name="initial_sync_start_importing_account">Initiële synchronisoatie: <string name="initial_sync_start_importing_account">Initiële synchronisoasje:
\nAccount wor geïmporteerd…</string> \nAccount wor geïmporteerd…</string>
<string name="initial_sync_start_importing_account_crypto">Initiële synchronisoatie: <string name="initial_sync_start_importing_account_crypto">Initiële synchronisoasje:
\nCrypto wor geïmporteerd</string> \nCrypto wor geïmporteerd</string>
<string name="initial_sync_start_importing_account_rooms">Initiële synchronisoatie: <string name="initial_sync_start_importing_account_rooms">Initiële synchronisoasje:
\nGesprekkn wordn geïmporteerd</string> \nGesprekkn wordn geïmporteerd</string>
<string name="initial_sync_start_importing_account_joined_rooms">Initiële synchronisoatie: <string name="initial_sync_start_importing_account_joined_rooms">Initiële synchronisoasje:
\nDeelgenoomn gesprekken wordn geïmporteerd</string> \nDeelgenoomn gesprekken wordn geïmporteerd</string>
<string name="initial_sync_start_importing_account_invited_rooms">Initiële synchronisoatie: <string name="initial_sync_start_importing_account_invited_rooms">Initiële synchronisoasje:
\nUutgenodigde gesprekkn wordn geïmporteerd</string> \nUutgenodigde gesprekkn wordn geïmporteerd</string>
<string name="initial_sync_start_importing_account_left_rooms">Initiële synchronisoatie: <string name="initial_sync_start_importing_account_left_rooms">Initiële synchronisoasje:
\nVerloatn gesprekkn wordn geïmporteerd</string> \nVerloatn gesprekkn wordn geïmporteerd</string>
<string name="initial_sync_start_importing_account_groups">Initiële synchronisoatie: <string name="initial_sync_start_importing_account_groups">Initiële synchronisoasje:
\nGemeenschappn wordn geïmporteerd</string> \nGemeenschappn wordn geïmporteerd</string>
<string name="initial_sync_start_importing_account_data">Initiële synchronisoatie: <string name="initial_sync_start_importing_account_data">Initiële synchronisoasje:
\nAccountgegeevns wordn geïmporteerd</string> \nAccountgegeevns wordn geïmporteerd</string>
<string name="notice_room_update">%s èt da gesprek hier ipgewoardeerd.</string>
<string name="event_status_sending_message">Bericht wor verstuurd…</string>
<string name="clear_timeline_send_queue">Uutgoande wachtreeke leegn</string>
</resources> </resources>

View File

@ -162,4 +162,9 @@
<string name="initial_sync_start_importing_account_data">初始化同步: <string name="initial_sync_start_importing_account_data">初始化同步:
\n正在导入账号数据</string> \n正在导入账号数据</string>
<string name="notice_room_update">%s 升级了聊天室。</string>
<string name="event_status_sending_message">正在发送消息…</string>
<string name="clear_timeline_send_queue">清除正在发送队列</string>
</resources> </resources>

View File

@ -165,4 +165,9 @@
<string name="initial_sync_start_importing_account_data">初始化同步: <string name="initial_sync_start_importing_account_data">初始化同步:
\n正在匯入帳號資料</string> \n正在匯入帳號資料</string>
<string name="notice_room_update">%s 已升級此聊天室。</string>
<string name="event_status_sending_message">正在傳送訊息……</string>
<string name="clear_timeline_send_queue">清除傳送佇列</string>
</resources> </resources>

View File

@ -15,7 +15,7 @@ androidExtensions {
} }
ext.versionMajor = 0 ext.versionMajor = 0
ext.versionMinor = 3 ext.versionMinor = 4
ext.versionPatch = 0 ext.versionPatch = 0
static def getGitTimestamp() { static def getGitTimestamp() {
@ -318,6 +318,8 @@ dependencies {
implementation 'diff_match_patch:diff_match_patch:current' implementation 'diff_match_patch:diff_match_patch:current'
implementation "androidx.emoji:emoji-appcompat:1.0.0"
// TESTS // TESTS
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:runner:1.2.0'

View File

@ -0,0 +1,67 @@
/*
* 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.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
@Singleton
class EmojiCompatWrapper @Inject constructor(private val context: Context) {
private var initialized = false
fun init(fontRequest: FontRequest) {
//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
}
}
}

View File

@ -66,6 +66,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
@Inject lateinit var authenticator: Authenticator @Inject lateinit var authenticator: Authenticator
@Inject lateinit var vectorConfiguration: VectorConfiguration @Inject lateinit var vectorConfiguration: VectorConfiguration
@Inject lateinit var emojiCompatFontProvider: EmojiCompatFontProvider @Inject lateinit var emojiCompatFontProvider: EmojiCompatFontProvider
@Inject lateinit var emojiCompatWrapper: EmojiCompatWrapper
@Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler @Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler
@Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var activeSessionHolder: ActiveSessionHolder
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager @Inject lateinit var notificationDrawerManager: NotificationDrawerManager
@ -105,6 +106,9 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
) )
FontsContractCompat.requestFont(this, fontRequest, emojiCompatFontProvider, getFontThreadHandler()) FontsContractCompat.requestFont(this, fontRequest, emojiCompatFontProvider, getFontThreadHandler())
vectorConfiguration.initConfiguration() vectorConfiguration.initConfiguration()
emojiCompatWrapper.init(fontRequest)
NotificationUtils.createNotificationChannels(applicationContext) NotificationUtils.createNotificationChannels(applicationContext)
if (authenticator.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) { if (authenticator.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) {
val lastAuthenticatedSession = authenticator.getLastAuthenticatedSession()!! val lastAuthenticatedSession = authenticator.getLastAuthenticatedSession()!!

View File

@ -58,6 +58,7 @@ import im.vector.riotx.features.rageshake.BugReportActivity
import im.vector.riotx.features.rageshake.BugReporter import im.vector.riotx.features.rageshake.BugReporter
import im.vector.riotx.features.rageshake.RageShake import im.vector.riotx.features.rageshake.RageShake
import im.vector.riotx.features.reactions.EmojiReactionPickerActivity 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.PublicRoomsFragment
import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
@ -181,6 +182,8 @@ interface ScreenComponent {
fun inject(displayReadReceiptsBottomSheet: DisplayReadReceiptsBottomSheet) fun inject(displayReadReceiptsBottomSheet: DisplayReadReceiptsBottomSheet)
fun inject(reactionButton: ReactionButton)
@Component.Factory @Component.Factory
interface Factory { interface Factory {
fun create(vectorComponent: VectorComponent, fun create(vectorComponent: VectorComponent,

View File

@ -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.auth.Authenticator
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.riotx.EmojiCompatFontProvider import im.vector.riotx.EmojiCompatFontProvider
import im.vector.riotx.EmojiCompatWrapper
import im.vector.riotx.VectorApplication import im.vector.riotx.VectorApplication
import im.vector.riotx.core.pushers.PushersManager import im.vector.riotx.core.pushers.PushersManager
import im.vector.riotx.features.configuration.VectorConfiguration import im.vector.riotx.features.configuration.VectorConfiguration
@ -70,6 +71,8 @@ interface VectorComponent {
fun emojiCompatFontProvider(): EmojiCompatFontProvider fun emojiCompatFontProvider(): EmojiCompatFontProvider
fun emojiCompatWrapper() : EmojiCompatWrapper
fun eventHtmlRenderer(): EventHtmlRenderer fun eventHtmlRenderer(): EventHtmlRenderer
fun navigator(): Navigator fun navigator(): Navigator

View File

@ -1,75 +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.core.ui.views
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.LinearLayout
import android.widget.RelativeLayout
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import butterknife.ButterKnife
import im.vector.riotx.R
import im.vector.riotx.features.themes.ThemeUtils
import kotlinx.android.synthetic.main.view_jump_to_read_marker.view.*
import me.gujun.android.span.span
import me.saket.bettermovementmethod.BetterLinkMovementMethod
class JumpToReadMarkerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : RelativeLayout(context, attrs, defStyleAttr) {
interface Callback {
fun onJumpToReadMarkerClicked(readMarkerId: String)
fun onClearReadMarkerClicked()
}
var callback: Callback? = null
init {
setupView()
}
private fun setupView() {
LinearLayout.inflate(context, R.layout.view_jump_to_read_marker, this)
setBackgroundColor(ContextCompat.getColor(context, R.color.notification_accent_color))
jumpToReadMarkerLabelView.movementMethod = BetterLinkMovementMethod.getInstance()
isClickable = true
closeJumpToReadMarkerView.setOnClickListener {
visibility = View.GONE
callback?.onClearReadMarkerClicked()
}
}
fun render(show: Boolean, readMarkerId: String?) {
isVisible = show
if (readMarkerId != null) {
jumpToReadMarkerLabelView.text = span(resources.getString(R.string.room_jump_to_first_unread)) {
textDecorationLine = "underline"
onClick = { callback?.onJumpToReadMarkerClicked(readMarkerId) }
}
}
}
}

View File

@ -1,86 +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.core.ui.views
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import im.vector.riotx.R
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import kotlinx.coroutines.*
private const val DELAY_IN_MS = 1_500L
class ReadMarkerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
interface Callback {
fun onReadMarkerDisplayed()
}
private var callback: Callback? = null
private var callbackDispatcherJob: Job? = null
fun bindView(informationData: MessageInformationData, readMarkerCallback: Callback) {
this.callback = readMarkerCallback
if (informationData.displayReadMarker) {
visibility = VISIBLE
callbackDispatcherJob = GlobalScope.launch(Dispatchers.Main) {
delay(DELAY_IN_MS)
callback?.onReadMarkerDisplayed()
}
startAnimation()
} else {
visibility = INVISIBLE
}
}
fun unbind() {
this.callbackDispatcherJob?.cancel()
this.callback = null
this.animation?.cancel()
this.visibility = INVISIBLE
}
private fun startAnimation() {
if (animation == null) {
animation = AnimationUtils.loadAnimation(context, R.anim.unread_marker_anim)
animation.startOffset = DELAY_IN_MS / 2
animation.duration = DELAY_IN_MS / 2
animation.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(animation: Animation) {
}
override fun onAnimationEnd(animation: Animation) {
visibility = INVISIBLE
}
override fun onAnimationRepeat(animation: Animation) {}
})
}
animation.start()
}
}

View File

@ -21,11 +21,8 @@ import android.util.AttributeSet
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import butterknife.ButterKnife
import im.vector.riotx.R 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.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
import kotlinx.android.synthetic.main.view_read_receipts.view.* import kotlinx.android.synthetic.main.view_read_receipts.view.*
@ -48,7 +45,6 @@ class ReadReceiptsView @JvmOverloads constructor(
private fun setupView() { private fun setupView() {
inflate(context, R.layout.view_read_receipts, this) inflate(context, R.layout.view_read_receipts, this)
ButterKnife.bind(this)
} }
fun render(readReceipts: List<ReadReceiptData>, avatarRenderer: AvatarRenderer, clickListener: OnClickListener) { fun render(readReceipts: List<ReadReceiptData>, avatarRenderer: AvatarRenderer, clickListener: OnClickListener) {

View File

@ -1,25 +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
import java.io.File
data class DownloadFileState(
val mimeType: String,
val file: File?,
val throwable: Throwable?
)

View File

@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.detail
import com.jaiselrahman.filepicker.model.MediaFile import com.jaiselrahman.filepicker.model.MediaFile
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
import im.vector.matrix.android.api.session.room.timeline.Timeline 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.TimelineEvent
@ -26,16 +27,13 @@ sealed class RoomDetailActions {
data class SendMessage(val text: String, val autoMarkdown: Boolean) : RoomDetailActions() data class SendMessage(val text: String, val autoMarkdown: Boolean) : RoomDetailActions()
data class SendMedia(val mediaFiles: List<MediaFile>) : RoomDetailActions() data class SendMedia(val mediaFiles: List<MediaFile>) : RoomDetailActions()
data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailActions() data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions()
data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailActions()
data class LoadMoreTimelineEvents(val direction: Timeline.Direction) : RoomDetailActions() data class LoadMoreTimelineEvents(val direction: Timeline.Direction) : RoomDetailActions()
data class SendReaction(val reaction: String, val targetEventId: String) : RoomDetailActions() data class SendReaction(val reaction: String, val targetEventId: String) : RoomDetailActions()
data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailActions() data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailActions()
data class UndoReaction(val targetEventId: String, val key: String, val reason: String? = "") : RoomDetailActions() data class UndoReaction(val targetEventId: String, val key: String, val reason: String? = "") : RoomDetailActions()
data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailActions() data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailActions()
data class NavigateToEvent(val eventId: String, val highlight: Boolean) : RoomDetailActions() data class NavigateToEvent(val eventId: String, val position: Int?) : RoomDetailActions()
data class SetReadMarkerAction(val eventId: String) : RoomDetailActions()
object MarkAllAsRead : RoomDetailActions()
data class DownloadFile(val eventId: String, val messageFileContent: MessageFileContent) : RoomDetailActions() data class DownloadFile(val eventId: String, val messageFileContent: MessageFileContent) : RoomDetailActions()
data class HandleTombstoneEvent(val event: Event): RoomDetailActions() data class HandleTombstoneEvent(val event: Event): RoomDetailActions()
object AcceptInvite : RoomDetailActions() object AcceptInvite : RoomDetailActions()
@ -49,4 +47,5 @@ sealed class RoomDetailActions {
object ClearSendQueue : RoomDetailActions() object ClearSendQueue : RoomDetailActions()
object ResendAll : RoomDetailActions() object ResendAll : RoomDetailActions()
} }

View File

@ -28,12 +28,7 @@ import android.os.Parcelable
import android.text.Editable import android.text.Editable
import android.text.Spannable import android.text.Spannable
import android.text.TextUtils import android.text.TextUtils
import android.view.HapticFeedbackConstants import android.view.*
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.Window
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
@ -51,12 +46,7 @@ import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView import butterknife.BindView
import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.EpoxyVisibilityTracker import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.mvrx.Async import com.airbnb.mvrx.*
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.github.piasy.biv.BigImageViewer import com.github.piasy.biv.BigImageViewer
import com.github.piasy.biv.loader.ImageLoader import com.github.piasy.biv.loader.ImageLoader
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
@ -70,13 +60,7 @@ import im.vector.matrix.android.api.permalinks.PermalinkFactory
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
@ -93,21 +77,9 @@ import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.core.files.addEntryToDownloadManager import im.vector.riotx.core.files.addEntryToDownloadManager
import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.ui.views.JumpToReadMarkerView
import im.vector.riotx.core.ui.views.NotificationAreaView import im.vector.riotx.core.ui.views.NotificationAreaView
import im.vector.riotx.core.utils.PERMISSIONS_FOR_TAKING_PHOTO import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES import im.vector.riotx.core.utils.*
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_DOWNLOAD_FILE
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_CAMERA
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CAMERA
import im.vector.riotx.core.utils.allGranted
import im.vector.riotx.core.utils.checkPermissions
import im.vector.riotx.core.utils.copyToClipboard
import im.vector.riotx.core.utils.openCamera
import im.vector.riotx.core.utils.shareMedia
import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter
import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy
import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter
@ -122,18 +94,9 @@ import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel
import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState
import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet 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.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.action.ActionsHandler 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.SimpleAction
import im.vector.riotx.features.home.room.detail.timeline.action.ViewEditHistoryBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener import im.vector.riotx.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener
import im.vector.riotx.features.home.room.detail.timeline.item.AbsMessageItem import im.vector.riotx.features.home.room.detail.timeline.item.*
import im.vector.riotx.features.home.room.detail.timeline.item.MessageFileItem
import im.vector.riotx.features.home.room.detail.timeline.item.MessageImageVideoItem
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.html.EventHtmlRenderer
import im.vector.riotx.features.html.PillImageSpan import im.vector.riotx.features.html.PillImageSpan
import im.vector.riotx.features.invite.VectorInviteView import im.vector.riotx.features.invite.VectorInviteView
@ -171,8 +134,7 @@ class RoomDetailFragment :
VectorBaseFragment(), VectorBaseFragment(),
TimelineEventController.Callback, TimelineEventController.Callback,
AutocompleteUserPresenter.Callback, AutocompleteUserPresenter.Callback,
VectorInviteView.Callback, VectorInviteView.Callback {
JumpToReadMarkerView.Callback {
companion object { companion object {
@ -232,7 +194,6 @@ class RoomDetailFragment :
override fun getMenuRes() = R.menu.menu_timeline override fun getMenuRes() = R.menu.menu_timeline
private lateinit var actionViewModel: ActionsHandler private lateinit var actionViewModel: ActionsHandler
private lateinit var layoutManager: LinearLayoutManager
@BindView(R.id.composerLayout) @BindView(R.id.composerLayout)
lateinit var composerLayout: TextComposerView lateinit var composerLayout: TextComposerView
@ -250,7 +211,6 @@ class RoomDetailFragment :
setupAttachmentButton() setupAttachmentButton()
setupInviteView() setupInviteView()
setupNotificationView() setupNotificationView()
setupJumpToReadMarkerView()
roomDetailViewModel.subscribe { renderState(it) } roomDetailViewModel.subscribe { renderState(it) }
textComposerViewModel.subscribe { renderTextComposerState(it) } textComposerViewModel.subscribe { renderTextComposerState(it) }
roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) } roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) }
@ -264,12 +224,8 @@ class RoomDetailFragment :
} }
roomDetailViewModel.navigateToEvent.observeEvent(this) { roomDetailViewModel.navigateToEvent.observeEvent(this) {
val scrollPosition = timelineEventController.searchPositionOfEvent(it) //
if (scrollPosition == null) {
scrollOnHighlightedEventCallback.scheduleScrollTo(it) scrollOnHighlightedEventCallback.scheduleScrollTo(it)
} else {
layoutManager.scrollToPosition(scrollPosition)
}
} }
roomDetailViewModel.selectSubscribe(this, RoomDetailViewState::tombstoneEventHandling, uniqueOnly("tombstoneEventHandling")) { roomDetailViewModel.selectSubscribe(this, RoomDetailViewState::tombstoneEventHandling, uniqueOnly("tombstoneEventHandling")) {
@ -303,10 +259,6 @@ class RoomDetailFragment :
} }
} }
private fun setupJumpToReadMarkerView() {
jumpToReadMarkerView.callback = this
}
private fun setupNotificationView() { private fun setupNotificationView() {
notificationAreaView.delegate = object : NotificationAreaView.Delegate { notificationAreaView.delegate = object : NotificationAreaView.Delegate {
@ -428,7 +380,7 @@ class RoomDetailFragment :
private fun setupRecyclerView() { private fun setupRecyclerView() {
val epoxyVisibilityTracker = EpoxyVisibilityTracker() val epoxyVisibilityTracker = EpoxyVisibilityTracker()
epoxyVisibilityTracker.attach(recyclerView) epoxyVisibilityTracker.attach(recyclerView)
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true) val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register() val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager) scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager)
scrollOnHighlightedEventCallback = ScrollOnHighlightedEventCallback(layoutManager, timelineEventController) scrollOnHighlightedEventCallback = ScrollOnHighlightedEventCallback(layoutManager, timelineEventController)
@ -453,7 +405,7 @@ class RoomDetailFragment :
R.drawable.ic_reply, R.drawable.ic_reply,
object : RoomMessageTouchHelperCallback.QuickReplayHandler { object : RoomMessageTouchHelperCallback.QuickReplayHandler {
override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
(model as? AbsMessageItem)?.attributes?.informationData?.let { (model as? AbsMessageItem)?.informationData?.let {
val eventId = it.eventId val eventId = it.eventId
roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId)) roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId))
} }
@ -464,7 +416,7 @@ class RoomDetailFragment :
is MessageFileItem, is MessageFileItem,
is MessageImageVideoItem, is MessageImageVideoItem,
is MessageTextItem -> { is MessageTextItem -> {
return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED return (model as AbsMessageItem).informationData.sendState == SendState.SYNCED
} }
else -> false else -> false
} }
@ -633,7 +585,7 @@ class RoomDetailFragment :
val summary = state.asyncRoomSummary() val summary = state.asyncRoomSummary()
val inviter = state.asyncInviter() val inviter = state.asyncInviter()
if (summary?.membership == Membership.JOIN) { if (summary?.membership == Membership.JOIN) {
timelineEventController.setTimeline(state.timeline, state.highlightedEventId) timelineEventController.setTimeline(state.timeline, state.eventId)
inviteView.visibility = View.GONE inviteView.visibility = View.GONE
val uid = session.myUserId val uid = session.myUserId
val meMember = session.getRoom(state.roomId)?.getRoomMember(uid) val meMember = session.getRoom(state.roomId)?.getRoomMember(uid)
@ -656,12 +608,10 @@ class RoomDetailFragment :
composerLayout.visibility = View.GONE composerLayout.visibility = View.GONE
notificationAreaView.render(NotificationAreaView.State.Tombstone(state.tombstoneEvent)) notificationAreaView.render(NotificationAreaView.State.Tombstone(state.tombstoneEvent))
} }
jumpToReadMarkerView.render(state.showJumpToReadMarker, summary?.readMarkerId)
} }
private fun renderRoomSummary(state: RoomDetailViewState) { private fun renderRoomSummary(state: RoomDetailViewState) {
state.asyncRoomSummary()?.let { state.asyncRoomSummary()?.let {
if (it.membership.isLeft()) { if (it.membership.isLeft()) {
Timber.w("The room has been left") Timber.w("The room has been left")
activity?.finish() activity?.finish()
@ -746,7 +696,7 @@ class RoomDetailFragment :
showSnackWithMessage(getString(R.string.navigate_to_room_when_already_in_the_room)) showSnackWithMessage(getString(R.string.navigate_to_room_when_already_in_the_room))
} else { } else {
// Highlight and scroll to this event // Highlight and scroll to this event
roomDetailViewModel.process(RoomDetailActions.NavigateToEvent(eventId, true)) roomDetailViewModel.process(RoomDetailActions.NavigateToEvent(eventId, timelineEventController.searchPositionOfEvent(eventId)))
} }
return true return true
} }
@ -766,11 +716,7 @@ class RoomDetailFragment :
} }
override fun onEventVisible(event: TimelineEvent) { override fun onEventVisible(event: TimelineEvent) {
roomDetailViewModel.process(RoomDetailActions.TimelineEventTurnsVisible(event)) roomDetailViewModel.process(RoomDetailActions.EventDisplayed(event))
}
override fun onEventInvisible(event: TimelineEvent) {
roomDetailViewModel.process(RoomDetailActions.TimelineEventTurnsInvisible(event))
} }
override fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View) { override fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View) {
@ -890,14 +836,6 @@ class RoomDetailFragment :
.show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS") .show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS")
} }
override fun onReadMarkerLongDisplayed(informationData: MessageInformationData) {
val firstVisibleItem = layoutManager.findFirstVisibleItemPosition()
val eventId = timelineEventController.searchEventIdAtPosition(firstVisibleItem)
if (eventId != null) {
roomDetailViewModel.process(RoomDetailActions.SetReadMarkerAction(eventId))
}
}
// AutocompleteUserPresenter.Callback // AutocompleteUserPresenter.Callback
override fun onQueryUsers(query: CharSequence?) { override fun onQueryUsers(query: CharSequence?) {
@ -1074,16 +1012,4 @@ class RoomDetailFragment :
notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId) notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId)
roomDetailViewModel.process(RoomDetailActions.RejectInvite) roomDetailViewModel.process(RoomDetailActions.RejectInvite)
} }
// JumpToReadMarkerView.Callback
override fun onJumpToReadMarkerClicked(readMarkerId: String) {
roomDetailViewModel.process(RoomDetailActions.NavigateToEvent(readMarkerId, false))
}
override fun onClearReadMarkerClicked() {
roomDetailViewModel.process(RoomDetailActions.MarkAllAsRead)
}
} }

View File

@ -38,7 +38,6 @@ 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.events.model.toModel
import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.file.FileService
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageType 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.message.getFileUrl
@ -59,8 +58,6 @@ import im.vector.riotx.features.command.CommandParser
import im.vector.riotx.features.command.ParsedCommand import im.vector.riotx.features.command.ParsedCommand
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.settings.VectorPreferences
import io.reactivex.Observable
import io.reactivex.functions.Function3
import io.reactivex.rxkotlin.subscribeBy import io.reactivex.rxkotlin.subscribeBy
import org.commonmark.parser.Parser import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer import org.commonmark.renderer.html.HtmlRenderer
@ -78,8 +75,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private val room = session.getRoom(initialState.roomId)!! private val room = session.getRoom(initialState.roomId)!!
private val roomId = initialState.roomId private val roomId = initialState.roomId
private val eventId = initialState.eventId private val eventId = initialState.eventId
private val invisibleEventsObservable = BehaviorRelay.create<RoomDetailActions.TimelineEventTurnsInvisible>() private val displayedEventsObservable = BehaviorRelay.create<RoomDetailActions.EventDisplayed>()
private val visibleEventsObservable = BehaviorRelay.create<RoomDetailActions.TimelineEventTurnsVisible>()
private val timelineSettings = if (userPreferencesProvider.shouldShowHiddenEvents()) { private val timelineSettings = if (userPreferencesProvider.shouldShowHiddenEvents()) {
TimelineSettings(30, false, true, TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES, userPreferencesProvider.shouldShowReadReceipts()) TimelineSettings(30, false, true, TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES, userPreferencesProvider.shouldShowReadReceipts())
} else { } else {
@ -113,7 +109,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
observeRoomSummary() observeRoomSummary()
observeEventDisplayedActions() observeEventDisplayedActions()
observeSummaryState() observeSummaryState()
observeJumpToReadMarkerViewVisibility()
room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear() room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear()
timeline.start() timeline.start()
setState { copy(timeline = this@RoomDetailViewModel.timeline) } setState { copy(timeline = this@RoomDetailViewModel.timeline) }
@ -123,8 +118,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
when (action) { when (action) {
is RoomDetailActions.SendMessage -> handleSendMessage(action) is RoomDetailActions.SendMessage -> handleSendMessage(action)
is RoomDetailActions.SendMedia -> handleSendMedia(action) is RoomDetailActions.SendMedia -> handleSendMedia(action)
is RoomDetailActions.TimelineEventTurnsVisible -> handleEventVisible(action) is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action)
is RoomDetailActions.TimelineEventTurnsInvisible -> handleEventInvisible(action)
is RoomDetailActions.LoadMoreTimelineEvents -> handleLoadMore(action) is RoomDetailActions.LoadMoreTimelineEvents -> handleLoadMore(action)
is RoomDetailActions.SendReaction -> handleSendReaction(action) is RoomDetailActions.SendReaction -> handleSendReaction(action)
is RoomDetailActions.AcceptInvite -> handleAcceptInvite() is RoomDetailActions.AcceptInvite -> handleAcceptInvite()
@ -142,16 +136,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
is RoomDetailActions.RemoveFailedEcho -> handleRemove(action) is RoomDetailActions.RemoveFailedEcho -> handleRemove(action)
is RoomDetailActions.ClearSendQueue -> handleClearSendQueue() is RoomDetailActions.ClearSendQueue -> handleClearSendQueue()
is RoomDetailActions.ResendAll -> handleResendAll() is RoomDetailActions.ResendAll -> handleResendAll()
is RoomDetailActions.SetReadMarkerAction -> handleSetReadMarkerAction(action)
is RoomDetailActions.MarkAllAsRead -> handleMarkAllAsRead()
else -> Timber.e("Unhandled Action: $action") else -> Timber.e("Unhandled Action: $action")
} }
} }
private fun handleEventInvisible(action: RoomDetailActions.TimelineEventTurnsInvisible) {
invisibleEventsObservable.accept(action)
}
private fun handleTombstoneEvent(action: RoomDetailActions.HandleTombstoneEvent) { private fun handleTombstoneEvent(action: RoomDetailActions.HandleTombstoneEvent) {
val tombstoneContent = action.event.getClearContent().toModel<RoomTombstoneContent>() val tombstoneContent = action.event.getClearContent().toModel<RoomTombstoneContent>()
?: return ?: return
@ -456,14 +444,14 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
room.sendMedias(attachments) room.sendMedias(attachments)
} }
private fun handleEventVisible(action: RoomDetailActions.TimelineEventTurnsVisible) { private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) {
if (action.event.root.sendState.isSent()) { //ignore pending/local events if (action.event.root.sendState.isSent()) { //ignore pending/local events
visibleEventsObservable.accept(action) displayedEventsObservable.accept(action)
} }
//We need to update this with the related m.replace also (to move read receipt) //We need to update this with the related m.replace also (to move read receipt)
action.event.annotations?.editSummary?.sourceEvents?.forEach { action.event.annotations?.editSummary?.sourceEvents?.forEach {
room.getTimeLineEvent(it)?.let { event -> room.getTimeLineEvent(it)?.let { event ->
visibleEventsObservable.accept(RoomDetailActions.TimelineEventTurnsVisible(event)) displayedEventsObservable.accept(RoomDetailActions.EventDisplayed(event))
} }
} }
} }
@ -506,6 +494,11 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} }
} }
data class DownloadFileState(
val mimeType: String,
val file: File?,
val throwable: Throwable?
)
private fun handleDownloadFile(action: RoomDetailActions.DownloadFile) { private fun handleDownloadFile(action: RoomDetailActions.DownloadFile) {
session.downloadFile( session.downloadFile(
@ -537,15 +530,53 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private fun handleNavigateToEvent(action: RoomDetailActions.NavigateToEvent) { private fun handleNavigateToEvent(action: RoomDetailActions.NavigateToEvent) {
val targetEventId = action.eventId val targetEventId = action.eventId
val indexOfEvent = timeline.getIndexOfEvent(targetEventId)
if (indexOfEvent == null) { if (action.position != null) {
// Event is not already in RAM // Event is already in RAM
timeline.restartWithEventId(targetEventId) withState {
if (it.eventId == targetEventId) {
// ensure another click on the same permalink will also do a scroll
setState {
copy(
eventId = null
)
} }
if (action.highlight) {
setState { copy(highlightedEventId = targetEventId) }
} }
setState {
copy(
eventId = targetEventId
)
}
}
_navigateToEvent.postLiveEvent(targetEventId) _navigateToEvent.postLiveEvent(targetEventId)
} else {
// change timeline
timeline.dispose()
timeline = room.createTimeline(targetEventId, timelineSettings)
timeline.start()
withState {
if (it.eventId == targetEventId) {
// ensure another click on the same permalink will also do a scroll
setState {
copy(
eventId = null
)
}
}
setState {
copy(
eventId = targetEventId,
timeline = this@RoomDetailViewModel.timeline
)
}
}
_navigateToEvent.postLiveEvent(targetEventId)
}
} }
private fun handleResendEvent(action: RoomDetailActions.ResendMessage) { private fun handleResendEvent(action: RoomDetailActions.ResendMessage) {
@ -591,7 +622,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private fun observeEventDisplayedActions() { private fun observeEventDisplayedActions() {
// We are buffering scroll events for one second // We are buffering scroll events for one second
// and keep the most recent one to set the read receipt on. // and keep the most recent one to set the read receipt on.
visibleEventsObservable displayedEventsObservable
.buffer(1, TimeUnit.SECONDS) .buffer(1, TimeUnit.SECONDS)
.filter { it.isNotEmpty() } .filter { it.isNotEmpty() }
.subscribeBy(onNext = { actions -> .subscribeBy(onNext = { actions ->
@ -603,24 +634,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
.disposeOnClear() .disposeOnClear()
} }
private fun handleSetReadMarkerAction(action: RoomDetailActions.SetReadMarkerAction) = withState { state ->
var readMarkerId = action.eventId
if (readMarkerId == state.asyncRoomSummary()?.readMarkerId) {
val indexOfEvent = timeline.getIndexOfEvent(action.eventId)
// force to set the read marker on the next event
if (indexOfEvent != null) {
timeline.getTimelineEventAtIndex(indexOfEvent - 1)?.root?.eventId?.also { eventIdOfNext ->
readMarkerId = eventIdOfNext
}
}
}
room.setReadMarker(readMarkerId, callback = object : MatrixCallback<Unit> {})
}
private fun handleMarkAllAsRead() {
room.markAllAsRead(object : MatrixCallback<Any> {})
}
private fun observeSyncState() { private fun observeSyncState() {
session.rx() session.rx()
.liveSyncState() .liveSyncState()
@ -632,39 +645,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
.disposeOnClear() .disposeOnClear()
} }
private fun observeJumpToReadMarkerViewVisibility() {
Observable
.combineLatest(
room.rx().liveRoomSummary(),
visibleEventsObservable.distinctUntilChanged(),
isEventVisibleObservable { it.hasReadMarker }.startWith(false),
Function3<RoomSummary, RoomDetailActions.TimelineEventTurnsVisible, Boolean, Boolean> { roomSummary, currentVisibleEvent, isReadMarkerViewVisible ->
val readMarkerId = roomSummary.readMarkerId
if (readMarkerId == null || isReadMarkerViewVisible || !timeline.isLive) {
false
} else {
val readMarkerPosition = timeline.getIndexOfEvent(readMarkerId)
?: Int.MAX_VALUE
val currentVisibleEventPosition = timeline.getIndexOfEvent(currentVisibleEvent.event.root.eventId)
?: Int.MIN_VALUE
readMarkerPosition > currentVisibleEventPosition
}
}
)
.distinctUntilChanged()
.subscribe {
setState { copy(showJumpToReadMarker = it) }
}
.disposeOnClear()
}
private fun isEventVisibleObservable(filterEvent: (TimelineEvent) -> Boolean): Observable<Boolean> {
return Observable.merge(
visibleEventsObservable.filter { filterEvent(it.event) }.map { true },
invisibleEventsObservable.filter { filterEvent(it.event) }.map { false }
)
}
private fun observeRoomSummary() { private fun observeRoomSummary() {
room.rx().liveRoomSummary() room.rx().liveRoomSummary()
.execute { async -> .execute { async ->

View File

@ -51,9 +51,7 @@ data class RoomDetailViewState(
val isEncrypted: Boolean = false, val isEncrypted: Boolean = false,
val tombstoneEvent: Event? = null, val tombstoneEvent: Event? = null,
val tombstoneEventHandling: Async<String> = Uninitialized, val tombstoneEventHandling: Async<String> = Uninitialized,
val syncState: SyncState = SyncState.IDLE, val syncState: SyncState = SyncState.IDLE
val showJumpToReadMarker: Boolean = false,
val highlightedEventId: String? = null
) : MvRxState { ) : MvRxState {
constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId) constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId)

View File

@ -191,12 +191,13 @@ class RoomMessageTouchHelperCallback(private val context: Context,
} }
val y = (itemView.top + itemView.measuredHeight / 2).toFloat() val y = (itemView.top + itemView.measuredHeight / 2).toFloat()
//magic numbers? val hw = imageDrawable.intrinsicWidth / 2f
val hh = imageDrawable.intrinsicHeight / 2f
imageDrawable.setBounds( imageDrawable.setBounds(
(x - convertToPx(12) * scale).toInt(), (x - hw * scale).toInt(),
(y - convertToPx(11) * scale).toInt(), (y - hh * scale).toInt(),
(x + convertToPx(12) * scale).toInt(), (x + hw * scale).toInt(),
(y + convertToPx(10) * scale).toInt() (y + hh * scale).toInt()
) )
imageDrawable.draw(canvas) imageDrawable.draw(canvas)
imageDrawable.alpha = 255 imageDrawable.alpha = 255

View File

@ -38,7 +38,7 @@ class ScrollOnHighlightedEventCallback(private val layoutManager: LinearLayoutMa
// Do not scroll it item is already visible // Do not scroll it item is already visible
if (positionToScroll !in firstVisibleItem..lastVisibleItem) { if (positionToScroll !in firstVisibleItem..lastVisibleItem) {
// Note: Offset will be from the bottom, since the layoutManager is reversed // Note: Offset will be from the bottom, since the layoutManager is reversed
layoutManager.scrollToPosition(position) layoutManager.scrollToPositionWithOffset(positionToScroll, 120)
} }
scheduledEventId.set(null) scheduledEventId.set(null)
} }

View File

@ -69,7 +69,7 @@ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() {
val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context, val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context,
LinearLayout.VERTICAL) LinearLayout.VERTICAL)
epoxyRecyclerView.addItemDecoration(dividerItemDecoration) epoxyRecyclerView.addItemDecoration(dividerItemDecoration)
bottomSheetTitle.text = getString(R.string.read_receipts_list) bottomSheetTitle.text = getString(R.string.read_at)
epoxyController.setData(displayReadReceiptArgs.readReceipts) epoxyController.setData(displayReadReceiptArgs.readReceipts)
} }

View File

@ -49,11 +49,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
private val timelineMediaSizeProvider: TimelineMediaSizeProvider, private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
@TimelineEventControllerHandler @TimelineEventControllerHandler
private val backgroundHandler: Handler private val backgroundHandler: Handler,
userPreferencesProvider: UserPreferencesProvider
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener { ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener {
interface Callback : BaseCallback, ReactionPillCallback, AvatarCallback, UrlClickCallback, ReadReceiptsCallback { interface Callback : BaseCallback, ReactionPillCallback, AvatarCallback, UrlClickCallback, ReadReceiptsCallback {
fun onEventInvisible(event: TimelineEvent)
fun onEventVisible(event: TimelineEvent) fun onEventVisible(event: TimelineEvent)
fun onRoomCreateLinkClicked(url: String) fun onRoomCreateLinkClicked(url: String)
fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View) fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View)
@ -81,7 +81,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
interface ReadReceiptsCallback { interface ReadReceiptsCallback {
fun onReadReceiptsClicked(readReceipts: List<ReadReceiptData>) fun onReadReceiptsClicked(readReceipts: List<ReadReceiptData>)
fun onReadMarkerLongDisplayed(informationData: MessageInformationData)
} }
interface UrlClickCallback { interface UrlClickCallback {
@ -141,6 +140,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
} }
} }
private val showHiddenEvents = userPreferencesProvider.shouldShowHiddenEvents()
init { init {
requestModelBuild() requestModelBuild()
} }
@ -246,7 +247,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
private fun buildItemModels(currentPosition: Int, items: List<TimelineEvent>): CacheItemData { private fun buildItemModels(currentPosition: Int, items: List<TimelineEvent>): CacheItemData {
val event = items[currentPosition] val event = items[currentPosition]
val nextEvent = items.nextOrNull(currentPosition) val nextEvent = items.nextDisplayableEvent(currentPosition, showHiddenEvents)
val date = event.root.localDateTime() val date = event.root.localDateTime()
val nextDate = nextEvent?.root?.localDateTime() val nextDate = nextEvent?.root?.localDateTime()
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate() val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
@ -326,42 +327,18 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
return shouldAdd return shouldAdd
} }
fun searchPositionOfEvent(eventId: String): Int? = synchronized(modelCache) { fun searchPositionOfEvent(eventId: String): Int? {
synchronized(modelCache) {
// Search in the cache // Search in the cache
var realPosition = 0 modelCache.forEachIndexed { idx, cacheItemData ->
for (i in 0 until modelCache.size) { if (cacheItemData?.eventId == eventId) {
val itemCache = modelCache[i] return idx
if (itemCache?.eventId == eventId) {
return realPosition
} }
if (itemCache?.eventModel != null) {
realPosition++
}
if (itemCache?.mergedHeaderModel != null) {
realPosition++
}
if (itemCache?.formattedDayModel != null) {
realPosition++
}
}
return null
} }
fun searchEventIdAtPosition(position: Int): String? = synchronized(modelCache) { return null
var offsetValue = 0
for (i in 0 until position) {
val itemCache = modelCache[i]
if (itemCache?.eventModel == null) {
offsetValue--
}
if (itemCache?.mergedHeaderModel != null) {
offsetValue++
}
if (itemCache?.formattedDayModel != null) {
offsetValue++
} }
} }
return modelCache.getOrNull(position - offsetValue)?.eventId
} }
private data class CacheItemData( private data class CacheItemData(
@ -371,5 +348,3 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
val mergedHeaderModel: MergedHeaderItem? = null, val mergedHeaderModel: MergedHeaderItem? = null,
val formattedDayModel: DaySeparatorItem? = null val formattedDayModel: DaySeparatorItem? = null
) )
}

View File

@ -35,7 +35,6 @@ import im.vector.riotx.R
import im.vector.riotx.core.extensions.canReact import im.vector.riotx.core.extensions.canReact
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.resources.StringProvider 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 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 //Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.getClearType() != EventType.MESSAGE) return false if (event.root.getClearType() != EventType.MESSAGE) return false
//TODO if user is admin or moderator //TODO if user is admin or moderator
return event.annotations?.reactionsSummary?.any { isSingleEmoji(it.key) } ?: false return event.annotations?.reactionsSummary?.isNotEmpty() ?: false
} }

View File

@ -38,12 +38,8 @@ abstract class ReactionInfoSimpleItem : EpoxyModelWithHolder<ReactionInfoSimpleI
@EpoxyAttribute @EpoxyAttribute
var timeStamp: CharSequence? = null var timeStamp: CharSequence? = null
@EpoxyAttribute
var emojiTypeFace: Typeface? = null
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
holder.emojiReactionView.text = reactionKey holder.emojiReactionView.text = reactionKey
holder.emojiReactionView.typeface = emojiTypeFace ?: Typeface.DEFAULT
holder.displayNameView.text = authorDisplayName holder.displayNameView.text = authorDisplayName
timeStamp?.let { timeStamp?.let {
holder.timeStampView.text = it holder.timeStampView.text = it

View File

@ -113,7 +113,7 @@ class ViewEditHistoryEpoxyController(private val context: Context,
when (it.operation) { when (it.operation) {
diff_match_patch.Operation.DELETE -> { diff_match_patch.Operation.DELETE -> {
span { span {
text = it.text text = it.text.replace("\n"," ")
textColor = ContextCompat.getColor(context, R.color.vector_error_color) textColor = ContextCompat.getColor(context, R.color.vector_error_color)
textDecorationLine = "line-through" textDecorationLine = "line-through"
} }

View File

@ -28,7 +28,6 @@ import com.airbnb.epoxy.EpoxyRecyclerView
import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.riotx.EmojiCompatFontProvider
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
@ -43,14 +42,11 @@ class ViewReactionBottomSheet : VectorBaseBottomSheetDialogFragment() {
private val viewModel: ViewReactionViewModel by fragmentViewModel(ViewReactionViewModel::class) private val viewModel: ViewReactionViewModel by fragmentViewModel(ViewReactionViewModel::class)
@Inject lateinit var viewReactionViewModelFactory: ViewReactionViewModel.Factory @Inject lateinit var viewReactionViewModelFactory: ViewReactionViewModel.Factory
@Inject lateinit var emojiCompatFontProvider: EmojiCompatFontProvider
@BindView(R.id.bottom_sheet_display_reactions_list) @BindView(R.id.bottom_sheet_display_reactions_list)
lateinit var epoxyRecyclerView: EpoxyRecyclerView lateinit var epoxyRecyclerView: EpoxyRecyclerView
private val epoxyController by lazy { @Inject lateinit var epoxyController: ViewReactionsEpoxyController
ViewReactionsEpoxyController(requireContext(), emojiCompatFontProvider.typeface)
}
override fun injectWith(screenComponent: ScreenComponent) { override fun injectWith(screenComponent: ScreenComponent) {
screenComponent.inject(this) screenComponent.inject(this)

View File

@ -90,7 +90,7 @@ class ViewReactionViewModel @AssistedInject constructor(@Assisted
.flatMapSingle { summaries -> .flatMapSingle { summaries ->
Observable Observable
.fromIterable(summaries.reactionsSummary) .fromIterable(summaries.reactionsSummary)
.filter { reactionAggregatedSummary -> isSingleEmoji(reactionAggregatedSummary.key) } //.filter { reactionAggregatedSummary -> isSingleEmoji(reactionAggregatedSummary.key) }
.toReactionInfoList() .toReactionInfoList()
} }
.execute { .execute {

View File

@ -17,20 +17,23 @@
package im.vector.riotx.features.home.room.detail.timeline.action package im.vector.riotx.features.home.room.detail.timeline.action
import android.content.Context import android.content.Context
import android.graphics.Typeface
import android.text.format.DateUtils
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import im.vector.riotx.EmojiCompatWrapper
import im.vector.riotx.R 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.genericFooterItem
import im.vector.riotx.core.ui.list.genericLoaderItem import im.vector.riotx.core.ui.list.genericLoaderItem
import javax.inject.Inject
/** /**
* Epoxy controller for reaction event list * Epoxy controller for reaction event list
*/ */
class ViewReactionsEpoxyController(private val context: Context, private val emojiCompatTypeface: Typeface?) class ViewReactionsEpoxyController @Inject constructor(
private val stringProvider: StringProvider,
private val emojiCompatWrapper: EmojiCompatWrapper )
: TypedEpoxyController<DisplayReactionsViewState>() { : TypedEpoxyController<DisplayReactionsViewState>() {
override fun buildModels(state: DisplayReactionsViewState) { override fun buildModels(state: DisplayReactionsViewState) {
@ -43,16 +46,15 @@ class ViewReactionsEpoxyController(private val context: Context, private val emo
is Fail -> { is Fail -> {
genericFooterItem { genericFooterItem {
id("failure") id("failure")
text(context.getString(R.string.unknown_error)) text(stringProvider.getString(R.string.unknown_error))
} }
} }
is Success -> { is Success -> {
state.mapReactionKeyToMemberList()?.forEach { state.mapReactionKeyToMemberList()?.forEach {
reactionInfoSimpleItem { reactionInfoSimpleItem {
id(it.eventId) id(it.eventId)
emojiTypeFace(emojiCompatTypeface)
timeStamp(it.timestamp) timeStamp(it.timestamp)
reactionKey(it.reactionKey) reactionKey(emojiCompatWrapper.safeEmojiSpanify(it.reactionKey))
authorDisplayName(it.authorName ?: it.authorId) authorDisplayName(it.authorName ?: it.authorId)
} }
} }

View File

@ -17,21 +17,35 @@
package im.vector.riotx.features.home.room.detail.timeline.factory package im.vector.riotx.features.home.room.detail.timeline.factory
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent 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.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 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) { val text = if (exception == null) {
"${event.root.getClearType()} events are not yet handled" "${event.root.getClearType()} events are not yet handled"
} else { } else {
"an exception occurred when rendering the event ${event.root.eventId}" "an exception occurred when rendering the event ${event.root.eventId}"
} }
val informationData = informationDataFactory.create(event, null)
return DefaultItem_() return DefaultItem_()
.text(text) .text(text)
.avatarRenderer(avatarRenderer)
.highlighted(highlight) .highlighted(highlight)
.informationData(informationData)
.baseCallback(callback)
.readReceiptsCallback(callback)
} }
} }

View File

@ -16,6 +16,7 @@
package im.vector.riotx.features.home.room.detail.timeline.factory package im.vector.riotx.features.home.room.detail.timeline.factory
import android.view.View
import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.events.model.EventType 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.TimelineEvent
@ -23,11 +24,11 @@ import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.utils.DebouncedClickListener
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem_ import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem_
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory import im.vector.riotx.features.home.room.detail.timeline.util.MessageInformationDataFactory
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
import me.gujun.android.span.span import me.gujun.android.span.span
import javax.inject.Inject import javax.inject.Inject
@ -35,7 +36,7 @@ import javax.inject.Inject
class EncryptedItemFactory @Inject constructor(private val messageInformationDataFactory: MessageInformationDataFactory, class EncryptedItemFactory @Inject constructor(private val messageInformationDataFactory: MessageInformationDataFactory,
private val colorProvider: ColorProvider, private val colorProvider: ColorProvider,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val attributesFactory: MessageItemAttributesFactory) { private val avatarRenderer: AvatarRenderer) {
fun create(event: TimelineEvent, fun create(event: TimelineEvent,
nextEvent: TimelineEvent?, nextEvent: TimelineEvent?,
@ -64,13 +65,22 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
// TODO This is not correct format for error, change it // TODO This is not correct format for error, change it
val informationData = messageInformationDataFactory.create(event, nextEvent) val informationData = messageInformationDataFactory.create(event, nextEvent)
val attributes = attributesFactory.create(null, informationData, callback)
return MessageTextItem_() return MessageTextItem_()
.attributes(attributes)
.message(spannableStr) .message(spannableStr)
.avatarRenderer(avatarRenderer)
.colorProvider(colorProvider)
.informationData(informationData)
.highlighted(highlight) .highlighted(highlight)
.avatarCallback(callback)
.urlClickCallback(callback) .urlClickCallback(callback)
.cellClickListener(
DebouncedClickListener(View.OnClickListener { view ->
callback?.onEncryptedMessageClicked(informationData, view)
}))
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, null, view)
?: false
}
} }
else -> null else -> null
} }

View File

@ -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.timeline.factory
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent
import im.vector.riotx.R
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.helper.senderAvatar
import im.vector.riotx.features.home.room.detail.timeline.helper.senderName
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem
import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem_
import javax.inject.Inject
class EncryptionItemFactory @Inject constructor(private val stringProvider: StringProvider,
private val avatarRenderer: AvatarRenderer) {
fun create(event: TimelineEvent,
highlight: Boolean,
callback: TimelineEventController.BaseCallback?): NoticeItem? {
val text = buildNoticeText(event.root, event.senderName) ?: return null
val informationData = MessageInformationData(
eventId = event.root.eventId ?: "?",
senderId = event.root.senderId ?: "",
sendState = event.root.sendState,
avatarUrl = event.senderAvatar(),
memberName = event.senderName(),
showInformation = false
)
return NoticeItem_()
.avatarRenderer(avatarRenderer)
.noticeText(text)
.informationData(informationData)
.highlighted(highlight)
.baseCallback(callback)
}
private fun buildNoticeText(event: Event, senderName: String?): CharSequence? {
return when {
EventType.ENCRYPTION == event.getClearType() -> {
val content = event.content.toModel<EncryptionEventContent>() ?: return null
stringProvider.getString(R.string.notice_end_to_end, senderName, content.algorithm)
}
else -> null
}
}
}

View File

@ -47,14 +47,27 @@ import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.linkify.VectorLinkify import im.vector.riotx.core.linkify.VectorLinkify
import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.resources.UserPreferencesProvider
import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.core.utils.DebouncedClickListener
import im.vector.riotx.features.home.AvatarRenderer 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.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder 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.TimelineMediaSizeProvider
import im.vector.riotx.features.home.room.detail.timeline.item.* import im.vector.riotx.features.home.room.detail.timeline.helper.senderAvatar
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory import im.vector.riotx.features.home.room.detail.timeline.item.BlankItem_
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory 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.html.EventHtmlRenderer
import im.vector.riotx.features.media.ImageContentRenderer import im.vector.riotx.features.media.ImageContentRenderer
import im.vector.riotx.features.media.VideoContentRenderer import im.vector.riotx.features.media.VideoContentRenderer
@ -62,13 +75,14 @@ import me.gujun.android.span.span
import javax.inject.Inject import javax.inject.Inject
class MessageItemFactory @Inject constructor( class MessageItemFactory @Inject constructor(
private val avatarRenderer: AvatarRenderer,
private val colorProvider: ColorProvider, private val colorProvider: ColorProvider,
private val timelineMediaSizeProvider: TimelineMediaSizeProvider, private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
private val htmlRenderer: Lazy<EventHtmlRenderer>, private val htmlRenderer: Lazy<EventHtmlRenderer>,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val emojiCompatFontProvider: EmojiCompatFontProvider,
private val imageContentRenderer: ImageContentRenderer, private val imageContentRenderer: ImageContentRenderer,
private val messageInformationDataFactory: MessageInformationDataFactory, private val messageInformationDataFactory: MessageInformationDataFactory,
private val messageItemAttributesFactory: MessageItemAttributesFactory,
private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder, private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder,
private val noticeItemFactory: NoticeItemFactory) { private val noticeItemFactory: NoticeItemFactory) {
@ -84,8 +98,7 @@ class MessageItemFactory @Inject constructor(
if (event.root.isRedacted()) { if (event.root.isRedacted()) {
//message is redacted //message is redacted
val attributes = messageItemAttributesFactory.create(null, informationData, callback) return buildRedactedItem(informationData, highlight, callback)
return buildRedactedItem(attributes, highlight)
} }
val messageContent: MessageContent = val messageContent: MessageContent =
@ -99,26 +112,22 @@ class MessageItemFactory @Inject constructor(
// This is an edit event, we should it when debugging as a notice event // This is an edit event, we should it when debugging as a notice event
return noticeItemFactory.create(event, highlight, callback) return noticeItemFactory.create(event, highlight, callback)
} }
val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback)
// val all = event.root.toContent() // val all = event.root.toContent()
// val ev = all.toModel<Event>() // val ev = all.toModel<Event>()
return when (messageContent) { return when (messageContent) {
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, is MessageEmoteContent -> buildEmoteMessageItem(messageContent,
informationData, informationData,
highlight, highlight,
callback, callback)
attributes)
is MessageTextContent -> buildTextMessageItem(messageContent, is MessageTextContent -> buildTextMessageItem(messageContent,
informationData, informationData,
highlight, highlight,
callback, callback)
attributes) is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback)
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback)
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback)
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageFileContent -> buildFileMessageItem(messageContent, informationData, highlight, callback)
is MessageFileContent -> buildFileMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, callback)
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, callback, attributes)
else -> buildNotHandledMessageItem(messageContent, highlight) else -> buildNotHandledMessageItem(messageContent, highlight)
} }
} }
@ -126,29 +135,55 @@ class MessageItemFactory @Inject constructor(
private fun buildAudioMessageItem(messageContent: MessageAudioContent, private fun buildAudioMessageItem(messageContent: MessageAudioContent,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?): MessageFileItem? {
attributes: AbsMessageItem.Attributes): MessageFileItem? {
return MessageFileItem_() return MessageFileItem_()
.attributes(attributes) .avatarRenderer(avatarRenderer)
.colorProvider(colorProvider)
.informationData(informationData)
.highlighted(highlight) .highlighted(highlight)
.avatarCallback(callback)
.readReceiptsCallback(callback)
.filename(messageContent.body) .filename(messageContent.body)
.iconRes(R.drawable.filetype_audio) .iconRes(R.drawable.filetype_audio)
.reactionPillCallback(callback)
.emojiTypeFace(emojiCompatFontProvider.typeface)
.cellClickListener(
DebouncedClickListener(View.OnClickListener { view: View ->
callback?.onEventCellClicked(informationData, messageContent, view)
}))
.clickListener( .clickListener(
DebouncedClickListener(View.OnClickListener { DebouncedClickListener(View.OnClickListener {
callback?.onAudioMessageClicked(messageContent) callback?.onAudioMessageClicked(messageContent)
})) }))
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false
}
} }
private fun buildFileMessageItem(messageContent: MessageFileContent, private fun buildFileMessageItem(messageContent: MessageFileContent,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?): MessageFileItem? {
attributes: AbsMessageItem.Attributes): MessageFileItem? {
return MessageFileItem_() return MessageFileItem_()
.attributes(attributes) .avatarRenderer(avatarRenderer)
.colorProvider(colorProvider)
.informationData(informationData)
.highlighted(highlight) .highlighted(highlight)
.avatarCallback(callback)
.filename(messageContent.body) .filename(messageContent.body)
.reactionPillCallback(callback)
.readReceiptsCallback(callback)
.emojiTypeFace(emojiCompatFontProvider.typeface)
.iconRes(R.drawable.filetype_attachment) .iconRes(R.drawable.filetype_attachment)
.cellClickListener(
DebouncedClickListener(View.OnClickListener { view ->
callback?.onEventCellClicked(informationData, messageContent, view)
}))
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false
}
.clickListener( .clickListener(
DebouncedClickListener(View.OnClickListener { _ -> DebouncedClickListener(View.OnClickListener { _ ->
callback?.onFileMessageClicked(informationData.eventId, messageContent) callback?.onFileMessageClicked(informationData.eventId, messageContent)
@ -165,8 +200,7 @@ class MessageItemFactory @Inject constructor(
private fun buildImageMessageItem(messageContent: MessageImageContent, private fun buildImageMessageItem(messageContent: MessageImageContent,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?): MessageImageVideoItem? {
attributes: AbsMessageItem.Attributes): MessageImageVideoItem? {
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
val data = ImageContentRenderer.Data( val data = ImageContentRenderer.Data(
@ -181,23 +215,36 @@ class MessageItemFactory @Inject constructor(
rotation = messageContent.info?.rotation rotation = messageContent.info?.rotation
) )
return MessageImageVideoItem_() return MessageImageVideoItem_()
.attributes(attributes) .avatarRenderer(avatarRenderer)
.colorProvider(colorProvider)
.imageContentRenderer(imageContentRenderer) .imageContentRenderer(imageContentRenderer)
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.playable(messageContent.info?.mimeType == "image/gif") .playable(messageContent.info?.mimeType == "image/gif")
.informationData(informationData)
.highlighted(highlight) .highlighted(highlight)
.avatarCallback(callback)
.mediaData(data) .mediaData(data)
.reactionPillCallback(callback)
.readReceiptsCallback(callback)
.emojiTypeFace(emojiCompatFontProvider.typeface)
.clickListener( .clickListener(
DebouncedClickListener(View.OnClickListener { view -> DebouncedClickListener(View.OnClickListener { view ->
callback?.onImageMessageClicked(messageContent, data, view) callback?.onImageMessageClicked(messageContent, data, view)
})) }))
.cellClickListener(
DebouncedClickListener(View.OnClickListener { view ->
callback?.onEventCellClicked(informationData, messageContent, view)
}))
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false
}
} }
private fun buildVideoMessageItem(messageContent: MessageVideoContent, private fun buildVideoMessageItem(messageContent: MessageVideoContent,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?): MessageImageVideoItem? {
attributes: AbsMessageItem.Attributes): MessageImageVideoItem? {
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
val thumbnailData = ImageContentRenderer.Data( val thumbnailData = ImageContentRenderer.Data(
@ -220,20 +267,33 @@ class MessageItemFactory @Inject constructor(
) )
return MessageImageVideoItem_() return MessageImageVideoItem_()
.attributes(attributes)
.imageContentRenderer(imageContentRenderer) .imageContentRenderer(imageContentRenderer)
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.avatarRenderer(avatarRenderer)
.colorProvider(colorProvider)
.playable(true) .playable(true)
.informationData(informationData)
.highlighted(highlight) .highlighted(highlight)
.avatarCallback(callback)
.mediaData(thumbnailData) .mediaData(thumbnailData)
.reactionPillCallback(callback)
.readReceiptsCallback(callback)
.emojiTypeFace(emojiCompatFontProvider.typeface)
.cellClickListener(
DebouncedClickListener(View.OnClickListener { view ->
callback?.onEventCellClicked(informationData, messageContent, view)
}))
.clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) } .clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) }
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false
}
} }
private fun buildTextMessageItem(messageContent: MessageTextContent, private fun buildTextMessageItem(messageContent: MessageTextContent,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?): MessageTextItem? {
attributes: AbsMessageItem.Attributes): MessageTextItem? {
val bodyToUse = messageContent.formattedBody?.let { val bodyToUse = messageContent.formattedBody?.let {
htmlRenderer.get().render(it.trim()) htmlRenderer.get().render(it.trim())
@ -250,10 +310,24 @@ class MessageItemFactory @Inject constructor(
message(linkifiedBody) message(linkifiedBody)
} }
} }
.attributes(attributes) .avatarRenderer(avatarRenderer)
.informationData(informationData)
.colorProvider(colorProvider)
.highlighted(highlight) .highlighted(highlight)
.avatarCallback(callback)
.urlClickCallback(callback) .urlClickCallback(callback)
.reactionPillCallback(callback)
.readReceiptsCallback(callback)
.emojiTypeFace(emojiCompatFontProvider.typeface)
//click on the text //click on the text
.cellClickListener(
DebouncedClickListener(View.OnClickListener { view ->
callback?.onEventCellClicked(informationData, messageContent, view)
}))
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false
}
} }
private fun annotateWithEdited(linkifiedBody: CharSequence, private fun annotateWithEdited(linkifiedBody: CharSequence,
@ -291,8 +365,7 @@ class MessageItemFactory @Inject constructor(
private fun buildNoticeMessageItem(messageContent: MessageNoticeContent, private fun buildNoticeMessageItem(messageContent: MessageNoticeContent,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?): MessageTextItem? {
attributes: AbsMessageItem.Attributes): MessageTextItem? {
val message = messageContent.body.let { val message = messageContent.body.let {
val formattedBody = span { val formattedBody = span {
@ -303,17 +376,34 @@ class MessageItemFactory @Inject constructor(
linkifyBody(formattedBody, callback) linkifyBody(formattedBody, callback)
} }
return MessageTextItem_() return MessageTextItem_()
.attributes(attributes) .avatarRenderer(avatarRenderer)
.message(message) .message(message)
.colorProvider(colorProvider)
.informationData(informationData)
.highlighted(highlight) .highlighted(highlight)
.avatarCallback(callback)
.reactionPillCallback(callback)
.urlClickCallback(callback) .urlClickCallback(callback)
.readReceiptsCallback(callback)
.emojiTypeFace(emojiCompatFontProvider.typeface)
.memberClickListener(
DebouncedClickListener(View.OnClickListener { view ->
callback?.onMemberNameClicked(informationData)
}))
.cellClickListener(
DebouncedClickListener(View.OnClickListener { view ->
callback?.onEventCellClicked(informationData, messageContent, view)
}))
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false
}
} }
private fun buildEmoteMessageItem(messageContent: MessageEmoteContent, private fun buildEmoteMessageItem(messageContent: MessageEmoteContent,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?): MessageTextItem? {
attributes: AbsMessageItem.Attributes): MessageTextItem? {
val message = messageContent.body.let { val message = messageContent.body.let {
val formattedBody = "* ${informationData.memberName} $it" val formattedBody = "* ${informationData.memberName} $it"
@ -328,16 +418,43 @@ class MessageItemFactory @Inject constructor(
message(message) message(message)
} }
} }
.attributes(attributes) .avatarRenderer(avatarRenderer)
.colorProvider(colorProvider)
.informationData(informationData)
.highlighted(highlight) .highlighted(highlight)
.avatarCallback(callback)
.reactionPillCallback(callback)
.readReceiptsCallback(callback)
.urlClickCallback(callback) .urlClickCallback(callback)
.emojiTypeFace(emojiCompatFontProvider.typeface)
.cellClickListener(
DebouncedClickListener(View.OnClickListener { view ->
callback?.onEventCellClicked(informationData, messageContent, view)
}))
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false
}
} }
private fun buildRedactedItem(attributes: AbsMessageItem.Attributes, private fun buildRedactedItem(informationData: MessageInformationData,
highlight: Boolean): RedactedMessageItem? { highlight: Boolean,
callback: TimelineEventController.Callback?): RedactedMessageItem? {
return RedactedMessageItem_() return RedactedMessageItem_()
.attributes(attributes) .avatarRenderer(avatarRenderer)
.colorProvider(colorProvider)
.informationData(informationData)
.highlighted(highlight) .highlighted(highlight)
.avatarCallback(callback)
.readReceiptsCallback(callback)
.cellClickListener(
DebouncedClickListener(View.OnClickListener { view ->
callback?.onEventCellClicked(informationData, null, view)
}))
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, null, view)
?: false
}
} }
private fun linkifyBody(body: CharSequence, callback: TimelineEventController.Callback?): CharSequence { private fun linkifyBody(body: CharSequence, callback: TimelineEventController.Callback?): CharSequence {

View File

@ -20,9 +20,12 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotx.features.home.AvatarRenderer 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.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory import im.vector.riotx.features.home.room.detail.timeline.helper.senderAvatar
import im.vector.riotx.features.home.room.detail.timeline.helper.senderName
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem
import 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 import javax.inject.Inject
class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEventFormatter, class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEventFormatter,

View File

@ -25,6 +25,7 @@ import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class TimelineItemFactory @Inject constructor(private val messageItemFactory: MessageItemFactory, class TimelineItemFactory @Inject constructor(private val messageItemFactory: MessageItemFactory,
private val encryptionItemFactory: EncryptionItemFactory,
private val encryptedItemFactory: EncryptedItemFactory, private val encryptedItemFactory: EncryptedItemFactory,
private val noticeItemFactory: NoticeItemFactory, private val noticeItemFactory: NoticeItemFactory,
private val defaultItemFactory: DefaultItemFactory, private val defaultItemFactory: DefaultItemFactory,
@ -34,7 +35,6 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
nextEvent: TimelineEvent?, nextEvent: TimelineEvent?,
eventIdToHighlight: String?, eventIdToHighlight: String?,
callback: TimelineEventController.Callback?): VectorEpoxyModel<*> { callback: TimelineEventController.Callback?): VectorEpoxyModel<*> {
val highlight = event.root.eventId == eventIdToHighlight val highlight = event.root.eventId == eventIdToHighlight
val computedModel = try { val computedModel = try {
@ -50,11 +50,11 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
EventType.CALL_HANGUP, EventType.CALL_HANGUP,
EventType.CALL_ANSWER, EventType.CALL_ANSWER,
EventType.REACTION, EventType.REACTION,
EventType.REDACTION, EventType.REDACTION -> noticeItemFactory.create(event, highlight, callback)
EventType.ENCRYPTION -> noticeItemFactory.create(event, highlight, callback)
// State room create // State room create
EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback) EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback)
// Crypto // Crypto
EventType.ENCRYPTION -> encryptionItemFactory.create(event, highlight, callback)
EventType.ENCRYPTED -> { EventType.ENCRYPTED -> {
if (event.root.isRedacted()) { if (event.root.isRedacted()) {
// Redacted event, let the MessageItemFactory handle it // Redacted event, let the MessageItemFactory handle it
@ -66,7 +66,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
// Unhandled event types (yet) // Unhandled event types (yet)
EventType.STATE_ROOM_THIRD_PARTY_INVITE, EventType.STATE_ROOM_THIRD_PARTY_INVITE,
EventType.STICKER -> defaultItemFactory.create(event, highlight) EventType.STICKER -> defaultItemFactory.create(event, highlight, callback)
else -> { else -> {
Timber.v("Type ${event.root.getClearType()} not handled") Timber.v("Type ${event.root.getClearType()} not handled")
null null
@ -74,7 +74,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "failed to create message item") Timber.e(e, "failed to create message item")
defaultItemFactory.create(event, highlight, e) defaultItemFactory.create(event, highlight, callback, e)
} }
return (computedModel ?: EmptyItem_()) return (computedModel ?: EmptyItem_())
} }

View File

@ -23,7 +23,6 @@ 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.*
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.home.room.detail.timeline.helper.senderName import im.vector.riotx.features.home.room.detail.timeline.helper.senderName
@ -42,7 +41,6 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
EventType.CALL_INVITE, EventType.CALL_INVITE,
EventType.CALL_HANGUP, EventType.CALL_HANGUP,
EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
EventType.ENCRYPTION -> formatEncryptionEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
EventType.MESSAGE, EventType.MESSAGE,
EventType.REACTION, EventType.REACTION,
EventType.REDACTION -> formatDebug(timelineEvent.root) EventType.REDACTION -> formatDebug(timelineEvent.root)
@ -62,7 +60,6 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
EventType.CALL_INVITE, EventType.CALL_INVITE,
EventType.CALL_HANGUP, EventType.CALL_HANGUP,
EventType.CALL_ANSWER -> formatCallEvent(event, senderName) EventType.CALL_ANSWER -> formatCallEvent(event, senderName)
EventType.ENCRYPTION -> formatEncryptionEvent(event, senderName)
EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(event, senderName) EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(event, senderName)
else -> { else -> {
Timber.v("Type $type not handled by this formatter") Timber.v("Type $type not handled by this formatter")
@ -212,9 +209,4 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
} }
} }
private fun formatEncryptionEvent(event: Event, senderName: String?): CharSequence? {
val eventContent: EncryptionEventContent = event.getClearContent().toModel() ?: return null
return stringProvider.getString(R.string.notice_end_to_end, senderName, eventContent.algorithm)
}
} }

View File

@ -1,58 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.home.room.detail.timeline.helper
import android.view.View
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.riotx.EmojiCompatFontProvider
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.utils.DebouncedClickListener
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.item.AbsMessageItem
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import javax.inject.Inject
class MessageItemAttributesFactory @Inject constructor(
private val avatarRenderer: AvatarRenderer,
private val colorProvider: ColorProvider,
private val emojiCompatFontProvider: EmojiCompatFontProvider) {
fun create(messageContent: MessageContent?, informationData: MessageInformationData, callback: TimelineEventController.Callback?): AbsMessageItem.Attributes {
return AbsMessageItem.Attributes(
informationData = informationData,
avatarRenderer = avatarRenderer,
colorProvider = colorProvider,
itemLongClickListener = View.OnLongClickListener { view ->
callback?.onEventLongClicked(informationData, messageContent, view) ?: false
},
itemClickListener = DebouncedClickListener(View.OnClickListener { view ->
callback?.onEventCellClicked(informationData, messageContent, view)
}),
memberClickListener = DebouncedClickListener(View.OnClickListener { view ->
callback?.onMemberNameClicked(informationData)
}),
reactionPillCallback = callback,
avatarCallback = callback,
readReceiptsCallback = callback,
emojiTypeFace = emojiCompatFontProvider.typeface
)
}
}

View File

@ -49,6 +49,30 @@ object TimelineDisplayableEvents {
) )
} }
fun TimelineEvent.isDisplayable(showHiddenEvent: Boolean): Boolean {
val allowed = TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES.takeIf { showHiddenEvent }
?: TimelineDisplayableEvents.DISPLAYABLE_TYPES
if (!allowed.contains(root.type)) {
return false
}
if (root.content.isNullOrEmpty()) {
//redacted events have empty content but are displayable
return root.unsignedData?.redactedEvent != null
}
//Edits should be filtered out!
if (EventType.MESSAGE == root.type
&& root.content.toModel<MessageContent>()?.relatesTo?.type == RelationType.REPLACE) {
return false
}
return true
}
//
//fun List<TimelineEvent>.filterDisplayableEvents(): List<TimelineEvent> {
// return this.filter {
// it.isDisplayable()
// }
//}
fun TimelineEvent.senderAvatar(): String? { fun TimelineEvent.senderAvatar(): String? {
// We might have no avatar when user leave, so we try to get it from prevContent // We might have no avatar when user leave, so we try to get it from prevContent
return senderAvatar return senderAvatar
@ -108,10 +132,10 @@ fun List<TimelineEvent>.prevSameTypeEvents(index: Int, minSize: Int): List<Timel
.reversed() .reversed()
} }
fun List<TimelineEvent>.nextOrNull(index: Int): TimelineEvent? { fun List<TimelineEvent>.nextDisplayableEvent(index: Int, showHiddenEvent: Boolean): TimelineEvent? {
return if (index >= size - 1) { return if (index >= size - 1) {
null null
} else { } else {
subList(index + 1, this.size).firstOrNull() subList(index + 1, this.size).firstOrNull { it.isDisplayable(showHiddenEvent) }
} }
} }

View File

@ -28,10 +28,9 @@ class TimelineEventVisibilityStateChangedListener(private val callback: Timeline
override fun onVisibilityStateChanged(visibilityState: Int) { override fun onVisibilityStateChanged(visibilityState: Int) {
if (visibilityState == VisibilityState.VISIBLE) { if (visibilityState == VisibilityState.VISIBLE) {
callback?.onEventVisible(event) callback?.onEventVisible(event)
} else if (visibilityState == VisibilityState.INVISIBLE) {
callback?.onEventInvisible(event)
} }
} }
} }
@ -41,9 +40,9 @@ class MergedTimelineEventVisibilityStateChangedListener(private val callback: Ti
override fun onVisibilityStateChanged(visibilityState: Int) { override fun onVisibilityStateChanged(visibilityState: Int) {
if (visibilityState == VisibilityState.VISIBLE) { if (visibilityState == VisibilityState.VISIBLE) {
events.forEach { callback?.onEventVisible(it) } events.forEach {
} else if (visibilityState == VisibilityState.INVISIBLE) { callback?.onEventVisible(it)
events.forEach { callback?.onEventInvisible(it) } }
} }
} }

View File

@ -32,8 +32,6 @@ import com.airbnb.epoxy.EpoxyAttribute
import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.ui.views.ReadMarkerView
import im.vector.riotx.core.ui.views.ReadReceiptsView
import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.core.utils.DebouncedClickListener
import im.vector.riotx.core.utils.DimensionUtils.dpToPx import im.vector.riotx.core.utils.DimensionUtils.dpToPx
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
@ -44,42 +42,63 @@ import im.vector.riotx.features.ui.getMessageTextColor
abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() { abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
@EpoxyAttribute @EpoxyAttribute
lateinit var attributes: Attributes lateinit var informationData: MessageInformationData
@EpoxyAttribute
lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute
lateinit var colorProvider: ColorProvider
@EpoxyAttribute
var longClickListener: View.OnLongClickListener? = null
@EpoxyAttribute
var cellClickListener: View.OnClickListener? = null
@EpoxyAttribute
var memberClickListener: View.OnClickListener? = null
@EpoxyAttribute
var emojiTypeFace: Typeface? = null
@EpoxyAttribute
var reactionPillCallback: TimelineEventController.ReactionPillCallback? = null
@EpoxyAttribute
var avatarCallback: TimelineEventController.AvatarCallback? = null
@EpoxyAttribute
var readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null
private val _avatarClickListener = DebouncedClickListener(View.OnClickListener { private val _avatarClickListener = DebouncedClickListener(View.OnClickListener {
attributes.avatarCallback?.onAvatarClicked(attributes.informationData) avatarCallback?.onAvatarClicked(informationData)
}) })
private val _memberNameClickListener = DebouncedClickListener(View.OnClickListener { private val _memberNameClickListener = DebouncedClickListener(View.OnClickListener {
attributes.avatarCallback?.onMemberNameClicked(attributes.informationData) avatarCallback?.onMemberNameClicked(informationData)
}) })
private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener { private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener {
attributes.readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts) readReceiptsCallback?.onReadReceiptsClicked(informationData.readReceipts)
}) })
private val _readMarkerCallback = object : ReadMarkerView.Callback {
override fun onReadMarkerDisplayed() {
attributes.readReceiptsCallback?.onReadMarkerLongDisplayed(attributes.informationData)
}
}
var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener { var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener {
override fun onReacted(reactionButton: ReactionButton) { override fun onReacted(reactionButton: ReactionButton) {
attributes.reactionPillCallback?.onClickOnReactionPill(attributes.informationData, reactionButton.reactionString, true) reactionPillCallback?.onClickOnReactionPill(informationData, reactionButton.reactionString, true)
} }
override fun onUnReacted(reactionButton: ReactionButton) { override fun onUnReacted(reactionButton: ReactionButton) {
attributes.reactionPillCallback?.onClickOnReactionPill(attributes.informationData, reactionButton.reactionString, false) reactionPillCallback?.onClickOnReactionPill(informationData, reactionButton.reactionString, false)
} }
override fun onLongClick(reactionButton: ReactionButton) { override fun onLongClick(reactionButton: ReactionButton) {
attributes.reactionPillCallback?.onLongClickOnReactionPill(attributes.informationData, reactionButton.reactionString) reactionPillCallback?.onLongClickOnReactionPill(informationData, reactionButton.reactionString)
} }
} }
override fun bind(holder: H) { override fun bind(holder: H) {
super.bind(holder) super.bind(holder)
if (attributes.informationData.showInformation) { if (informationData.showInformation) {
holder.avatarImageView.layoutParams = holder.avatarImageView.layoutParams?.apply { holder.avatarImageView.layoutParams = holder.avatarImageView.layoutParams?.apply {
val size = dpToPx(avatarStyle.avatarSizeDP, holder.view.context) val size = dpToPx(avatarStyle.avatarSizeDP, holder.view.context)
height = size height = size
@ -90,13 +109,13 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
holder.memberNameView.visibility = View.VISIBLE holder.memberNameView.visibility = View.VISIBLE
holder.memberNameView.setOnClickListener(_memberNameClickListener) holder.memberNameView.setOnClickListener(_memberNameClickListener)
holder.timeView.visibility = View.VISIBLE holder.timeView.visibility = View.VISIBLE
holder.timeView.text = attributes.informationData.time holder.timeView.text = informationData.time
holder.memberNameView.text = attributes.informationData.memberName holder.memberNameView.text = informationData.memberName
attributes.avatarRenderer.render(attributes.informationData.avatarUrl, attributes.informationData.senderId, attributes.informationData.memberName?.toString(), holder.avatarImageView) avatarRenderer.render(informationData.avatarUrl, informationData.senderId, informationData.memberName?.toString(), holder.avatarImageView)
holder.view.setOnClickListener(attributes.itemClickListener) holder.view.setOnClickListener(cellClickListener)
holder.view.setOnLongClickListener(attributes.itemLongClickListener) holder.view.setOnLongClickListener(longClickListener)
holder.avatarImageView.setOnLongClickListener(attributes.itemLongClickListener) holder.avatarImageView.setOnLongClickListener(longClickListener)
holder.memberNameView.setOnLongClickListener(attributes.itemLongClickListener) holder.memberNameView.setOnLongClickListener(longClickListener)
} else { } else {
holder.avatarImageView.setOnClickListener(null) holder.avatarImageView.setOnClickListener(null)
holder.memberNameView.setOnClickListener(null) holder.memberNameView.setOnClickListener(null)
@ -108,10 +127,10 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
holder.avatarImageView.setOnLongClickListener(null) holder.avatarImageView.setOnLongClickListener(null)
holder.memberNameView.setOnLongClickListener(null) holder.memberNameView.setOnLongClickListener(null)
} }
holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener)
holder.readMarkerView.bindView(attributes.informationData, _readMarkerCallback)
if (!shouldShowReactionAtBottom() || attributes.informationData.orderedReactionList.isNullOrEmpty()) { holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener)
if (!shouldShowReactionAtBottom() || informationData.orderedReactionList.isNullOrEmpty()) {
holder.reactionWrapper?.isVisible = false holder.reactionWrapper?.isVisible = false
} else { } else {
//inflate if needed //inflate if needed
@ -123,7 +142,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
//clear all reaction buttons (but not the Flow helper!) //clear all reaction buttons (but not the Flow helper!)
holder.reactionWrapper?.children?.forEach { (it as? ReactionButton)?.isGone = true } holder.reactionWrapper?.children?.forEach { (it as? ReactionButton)?.isGone = true }
val idToRefInFlow = ArrayList<Int>() val idToRefInFlow = ArrayList<Int>()
attributes.informationData.orderedReactionList?.chunked(8)?.firstOrNull()?.forEachIndexed { index, reaction -> informationData.orderedReactionList?.chunked(8)?.firstOrNull()?.forEachIndexed { index, reaction ->
(holder.reactionWrapper?.children?.elementAtOrNull(index) as? ReactionButton)?.let { reactionButton -> (holder.reactionWrapper?.children?.elementAtOrNull(index) as? ReactionButton)?.let { reactionButton ->
reactionButton.isVisible = true reactionButton.isVisible = true
reactionButton.reactedListener = reactionClickListener reactionButton.reactedListener = reactionClickListener
@ -131,7 +150,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
idToRefInFlow.add(reactionButton.id) idToRefInFlow.add(reactionButton.id)
reactionButton.reactionString = reaction.key reactionButton.reactionString = reaction.key
reactionButton.reactionCount = reaction.count reactionButton.reactionCount = reaction.count
reactionButton.emojiTypeFace = attributes.emojiTypeFace //reactionButton.emojiTypeFace = emojiTypeFace
reactionButton.setChecked(reaction.addedByMe) reactionButton.setChecked(reaction.addedByMe)
reactionButton.isEnabled = reaction.synced reactionButton.isEnabled = reaction.synced
} }
@ -142,48 +161,25 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && !holder.view.isInLayout) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && !holder.view.isInLayout) {
holder.reactionFlowHelper?.requestLayout() holder.reactionFlowHelper?.requestLayout()
} }
holder.reactionWrapper?.setOnLongClickListener(attributes.itemLongClickListener) holder.reactionWrapper?.setOnLongClickListener(longClickListener)
} }
} }
override fun unbind(holder: H) {
holder.readMarkerView.unbind()
super.unbind(holder)
}
open fun shouldShowReactionAtBottom(): Boolean { open fun shouldShowReactionAtBottom(): Boolean {
return true return true
} }
protected open fun renderSendState(root: View, textView: TextView?, failureIndicator: ImageView? = null) { protected open fun renderSendState(root: View, textView: TextView?, failureIndicator: ImageView? = null) {
root.isClickable = attributes.informationData.sendState.isSent() root.isClickable = informationData.sendState.isSent()
val state = if (attributes.informationData.hasPendingEdits) SendState.UNSENT else attributes.informationData.sendState val state = if (informationData.hasPendingEdits) SendState.UNSENT else informationData.sendState
textView?.setTextColor(attributes.colorProvider.getMessageTextColor(state)) textView?.setTextColor(colorProvider.getMessageTextColor(state))
failureIndicator?.isVisible = attributes.informationData.sendState.hasFailed() failureIndicator?.isVisible = informationData.sendState.hasFailed()
} }
/**
* This class holds all the common attributes for message items.
*/
data class Attributes(
val informationData: MessageInformationData,
val avatarRenderer: AvatarRenderer,
val colorProvider: ColorProvider,
val itemLongClickListener: View.OnLongClickListener? = null,
val itemClickListener: View.OnClickListener? = null,
val memberClickListener: View.OnClickListener? = null,
val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null,
val avatarCallback: TimelineEventController.AvatarCallback? = null,
val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
val emojiTypeFace: Typeface? = null
)
abstract class Holder(@IdRes stubId: Int) : BaseHolder(stubId) { abstract class Holder(@IdRes stubId: Int) : BaseHolder(stubId) {
val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView) val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
val memberNameView by bind<TextView>(R.id.messageMemberNameView) val memberNameView by bind<TextView>(R.id.messageMemberNameView)
val timeView by bind<TextView>(R.id.messageTimeView) val timeView by bind<TextView>(R.id.messageTimeView)
val readReceiptsView by bind<ReadReceiptsView>(R.id.readReceiptsView)
val readMarkerView by bind<ReadMarkerView>(R.id.readMarkerView)
var reactionWrapper: ViewGroup? = null var reactionWrapper: ViewGroup? = null
var reactionFlowHelper: Flow? = null var reactionFlowHelper: Flow? = null
} }

View File

@ -24,10 +24,8 @@ import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.platform.CheckableView import im.vector.riotx.core.platform.CheckableView
import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.ui.views.ReadReceiptsView
import im.vector.riotx.core.utils.DimensionUtils.dpToPx import im.vector.riotx.core.utils.DimensionUtils.dpToPx
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
/** /**
* Children must override getViewType() * Children must override getViewType()
@ -52,6 +50,7 @@ abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>
abstract class BaseHolder(@IdRes val stubId: Int) : VectorEpoxyHolder() { abstract class BaseHolder(@IdRes val stubId: Int) : VectorEpoxyHolder() {
val leftGuideline by bind<Guideline>(R.id.messageStartGuideline) val leftGuideline by bind<Guideline>(R.id.messageStartGuideline)
val checkableBackground by bind<CheckableView>(R.id.messageSelectedBackground) val checkableBackground by bind<CheckableView>(R.id.messageSelectedBackground)
val readReceiptsView by bind<ReadReceiptsView>(R.id.readReceiptsView)
override fun bindView(itemView: View) { override fun bindView(itemView: View) {
super.bindView(itemView) super.bindView(itemView)

View File

@ -16,19 +16,46 @@
package im.vector.riotx.features.home.room.detail.timeline.item package im.vector.riotx.features.home.room.detail.timeline.item
import android.view.View
import android.widget.TextView import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R 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) @EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
abstract class DefaultItem : BaseEventItem<DefaultItem.Holder>() { abstract class DefaultItem : BaseEventItem<DefaultItem.Holder>() {
@EpoxyAttribute
lateinit var informationData: MessageInformationData
@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
private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener {
readReceiptsCallback?.onReadReceiptsClicked(informationData.readReceipts)
})
@EpoxyAttribute @EpoxyAttribute
var text: CharSequence? = null var text: CharSequence? = null
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
holder.messageView.text = text holder.messageView.text = text
holder.view.setOnLongClickListener(longClickListener)
holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener)
} }
override fun getViewType() = STUB_ID override fun getViewType() = STUB_ID

View File

@ -21,6 +21,7 @@ import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.isVisible
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
@ -75,6 +76,9 @@ data class MergedHeaderItem(private val isCollapsed: Boolean,
holder.separatorView.visibility = View.VISIBLE holder.separatorView.visibility = View.VISIBLE
holder.expandView.setText(R.string.merged_events_collapse) holder.expandView.setText(R.string.merged_events_collapse)
} }
// No read receipt for this item
holder.readReceiptsView.isVisible = false
} }
data class Data( data class Data(

View File

@ -43,21 +43,21 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
imageContentRenderer.render(mediaData, ImageContentRenderer.Mode.THUMBNAIL, holder.imageView) imageContentRenderer.render(mediaData, ImageContentRenderer.Mode.THUMBNAIL, holder.imageView)
if (!attributes.informationData.sendState.hasFailed()) { if (!informationData.sendState.hasFailed()) {
contentUploadStateTrackerBinder.bind(attributes.informationData.eventId, mediaData, holder.progressLayout) contentUploadStateTrackerBinder.bind(informationData.eventId, mediaData, holder.progressLayout)
} }
holder.imageView.setOnClickListener(clickListener) holder.imageView.setOnClickListener(clickListener)
holder.imageView.setOnLongClickListener(attributes.itemLongClickListener) holder.imageView.setOnLongClickListener(longClickListener)
ViewCompat.setTransitionName(holder.imageView,"imagePreview_${id()}") ViewCompat.setTransitionName(holder.imageView,"imagePreview_${id()}")
holder.mediaContentView.setOnClickListener(attributes.itemClickListener) holder.mediaContentView.setOnClickListener(cellClickListener)
holder.mediaContentView.setOnLongClickListener(attributes.itemLongClickListener) holder.mediaContentView.setOnLongClickListener(longClickListener)
// The sending state color will be apply to the progress text // The sending state color will be apply to the progress text
renderSendState(holder.imageView, null, holder.failedToSendIndicator) renderSendState(holder.imageView, null, holder.failedToSendIndicator)
holder.playContentView.visibility = if (playable) View.VISIBLE else View.GONE holder.playContentView.visibility = if (playable) View.VISIBLE else View.GONE
} }
override fun unbind(holder: Holder) { override fun unbind(holder: Holder) {
contentUploadStateTrackerBinder.unbind(attributes.informationData.eventId) contentUploadStateTrackerBinder.unbind(informationData.eventId)
super.unbind(holder) super.unbind(holder)
} }

View File

@ -33,8 +33,7 @@ data class MessageInformationData(
val orderedReactionList: List<ReactionInfoData>? = null, val orderedReactionList: List<ReactionInfoData>? = null,
val hasBeenEdited: Boolean = false, val hasBeenEdited: Boolean = false,
val hasPendingEdits: Boolean = false, val hasPendingEdits: Boolean = false,
val readReceipts: List<ReadReceiptData> = emptyList(), val readReceipts: List<ReadReceiptData> = emptyList()
val displayReadMarker: Boolean = false
) : Parcelable ) : Parcelable

View File

@ -79,8 +79,8 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
holder.messageView.setTextFuture(textFuture) holder.messageView.setTextFuture(textFuture)
renderSendState(holder.messageView, holder.messageView) renderSendState(holder.messageView, holder.messageView)
holder.messageView.setOnClickListener(attributes.itemClickListener) holder.messageView.setOnClickListener(cellClickListener)
holder.messageView.setOnLongClickListener(attributes.itemLongClickListener) holder.messageView.setOnLongClickListener(longClickListener)
findPillsAndProcess { it.bind(holder.messageView) } findPillsAndProcess { it.bind(holder.messageView) }
} }

View File

@ -22,8 +22,6 @@ import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.ui.views.ReadMarkerView
import im.vector.riotx.core.ui.views.ReadReceiptsView
import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.core.utils.DebouncedClickListener
import im.vector.riotx.features.home.AvatarRenderer 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.TimelineEventController
@ -54,12 +52,6 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
readReceiptsCallback?.onReadReceiptsClicked(informationData.readReceipts) readReceiptsCallback?.onReadReceiptsClicked(informationData.readReceipts)
}) })
private val _readMarkerCallback = object : ReadMarkerView.Callback {
override fun onReadMarkerDisplayed() {
readReceiptsCallback?.onReadMarkerLongDisplayed(informationData)
}
}
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
holder.noticeTextView.text = noticeText holder.noticeTextView.text = noticeText
@ -72,12 +64,6 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
) )
holder.view.setOnLongClickListener(longClickListener) holder.view.setOnLongClickListener(longClickListener)
holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener) holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener)
holder.readMarkerView.bindView(informationData, _readMarkerCallback)
}
override fun unbind(holder: Holder) {
holder.readMarkerView.unbind()
super.unbind(holder)
} }
override fun getViewType() = STUB_ID override fun getViewType() = STUB_ID
@ -85,8 +71,6 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
class Holder : BaseHolder(STUB_ID) { class Holder : BaseHolder(STUB_ID) {
val avatarImageView by bind<ImageView>(R.id.itemNoticeAvatarView) val avatarImageView by bind<ImageView>(R.id.itemNoticeAvatarView)
val noticeTextView by bind<TextView>(R.id.itemNoticeTextView) val noticeTextView by bind<TextView>(R.id.itemNoticeTextView)
val readReceiptsView by bind<ReadReceiptsView>(R.id.readReceiptsView)
val readMarkerView by bind<ReadMarkerView>(R.id.readMarkerView)
} }
companion object { companion object {

View File

@ -1,5 +1,4 @@
/* /*
* Copyright 2019 New Vector Ltd * Copyright 2019 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -13,10 +12,9 @@
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package im.vector.riotx.features.home.room.detail.timeline.helper 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.Session
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
@ -64,9 +62,6 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId ?: "")) textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId ?: ""))
} }
val displayReadMarker = event.hasReadMarker
&& event.readReceipts.find { it.user.userId == session.myUserId } == null
return MessageInformationData( return MessageInformationData(
eventId = eventId, eventId = eventId,
senderId = event.root.senderId ?: "", senderId = event.root.senderId ?: "",
@ -76,7 +71,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
memberName = formattedMemberName, memberName = formattedMemberName,
showInformation = showInformation, showInformation = showInformation,
orderedReactionList = event.annotations?.reactionsSummary orderedReactionList = event.annotations?.reactionsSummary
?.filter { isSingleEmoji(it.key) } //?.filter { isSingleEmoji(it.key) }
?.map { ?.map {
ReactionInfoData(it.key, it.count, it.addedByMe, it.localEchoEvents.isEmpty()) ReactionInfoData(it.key, it.count, it.addedByMe, it.localEchoEvents.isEmpty())
}, },
@ -90,8 +85,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
.map { .map {
ReadReceiptData(it.user.userId, it.user.avatarUrl, it.user.displayName, it.originServerTs) ReadReceiptData(it.user.userId, it.user.avatarUrl, it.user.displayName, it.originServerTs)
} }
.toList(), .toList()
displayReadMarker = displayReadMarker
) )
} }
} }

View File

@ -89,19 +89,45 @@ class VideoContentRenderer @Inject constructor(private val activeSessionHolder:
}) })
} }
} else { } else {
thumbnailView.isVisible = false
loadingView.isVisible = false
val resolvedUrl = contentUrlResolver.resolveFullSize(data.url) val resolvedUrl = contentUrlResolver.resolveFullSize(data.url)
if (resolvedUrl == null) { if (resolvedUrl == null) {
thumbnailView.isVisible = false
loadingView.isVisible = false
errorView.isVisible = true errorView.isVisible = true
errorView.setText(R.string.unknown_error) errorView.setText(R.string.unknown_error)
} else { } else {
//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<File> {
override fun onSuccess(data: File) {
thumbnailView.isVisible = false
loadingView.isVisible = false
videoView.isVisible = true videoView.isVisible = true
videoView.setVideoPath(resolvedUrl)
videoView.setVideoPath(data.path)
videoView.start() videoView.start()
} }
override fun onFailure(failure: Throwable) {
loadingView.isVisible = false
errorView.isVisible = true
errorView.text = errorFormatter.toHumanReadable(failure)
}
})
}
} }
} }

View File

@ -48,7 +48,7 @@ class PushRuleTriggerListener @Inject constructor(
//TODO //TODO
} else { } else {
notifiableEvent.noisy = !notificationAction.soundName.isNullOrBlank() notifiableEvent.noisy = !notificationAction.soundName.isNullOrBlank()
Timber.v("New event to notify $notifiableEvent tweaks:$notificationAction") Timber.v("New event to notify")
notificationDrawerManager.onNotifiableEventReceived(notifiableEvent) notificationDrawerManager.onNotifiableEventReceived(notifiableEvent)
} }
} else { } else {

View File

@ -21,7 +21,6 @@ import android.animation.AnimatorSet
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.content.Context import android.content.Context
import android.content.res.TypedArray import android.content.res.TypedArray
import android.graphics.Typeface
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
@ -35,8 +34,11 @@ import android.widget.TextView
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import im.vector.riotx.EmojiCompatWrapper
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.HasScreenInjector
import im.vector.riotx.core.utils.TextUtils import im.vector.riotx.core.utils.TextUtils
import javax.inject.Inject
/** /**
* An animated reaction button. * An animated reaction button.
@ -46,6 +48,12 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut
defStyleAttr: Int = 0) defStyleAttr: Int = 0)
: FrameLayout(context, attrs, defStyleAttr), View.OnClickListener, View.OnLongClickListener { : FrameLayout(context, attrs, defStyleAttr), View.OnClickListener, View.OnLongClickListener {
init {
if (context is HasScreenInjector) {
context.injector().inject(this)
}
}
companion object { companion object {
private val DECCELERATE_INTERPOLATOR = DecelerateInterpolator() private val DECCELERATE_INTERPOLATOR = DecelerateInterpolator()
private val ACCELERATE_DECELERATE_INTERPOLATOR = AccelerateDecelerateInterpolator() private val ACCELERATE_DECELERATE_INTERPOLATOR = AccelerateDecelerateInterpolator()
@ -53,17 +61,13 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut
} }
@Inject lateinit var emojiCompatWrapper: EmojiCompatWrapper
private var emojiView: TextView? = null private var emojiView: TextView? = null
private var countTextView: TextView? = null private var countTextView: TextView? = null
private var reactionSelector: View? = null private var reactionSelector: View? = null
var emojiTypeFace: Typeface? = null
set(value) {
field = value
emojiView?.typeface = value ?: Typeface.DEFAULT
}
private var dotsView: DotsView private var dotsView: DotsView
private var circleView: CircleView private var circleView: CircleView
var reactedListener: ReactedListener? = null var reactedListener: ReactedListener? = null
@ -82,7 +86,9 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut
var reactionString = "😀" var reactionString = "😀"
set(value) { set(value) {
field = value field = value
emojiView?.text = field //maybe cache this for performances?
val emojiSpanned = emojiCompatWrapper.safeEmojiSpanify(value)
emojiView?.text = emojiSpanned
} }
private var animationScaleFactor: Float = 0.toFloat() private var animationScaleFactor: Float = 0.toFloat()
@ -104,7 +110,7 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut
countTextView?.text = TextUtils.formatCountToShortDecimal(reactionCount) 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) val array = context.obtainStyledAttributes(attrs, R.styleable.ReactionButton, defStyleAttr, 0)

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"> <set xmlns:android="http://schemas.android.com/apk/res/android">
<scale <scale
android:duration="1500"
android:fromXScale="1" android:fromXScale="1"
android:fromYScale="1" android:fromYScale="1"
android:pivotX="50%p" android:pivotX="50%p"
@ -9,6 +10,7 @@
android:toYScale="0" /> android:toYScale="0" />
<alpha <alpha
android:duration="1500"
android:fromAlpha="1" android:fromAlpha="1"
android:toAlpha="0" /> android:toAlpha="0" />
</set> </set>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="10dp" />
<solid android:color="@color/riotx_header_panel_border_mobile_black" />
</shape>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="10dp" />
<solid android:color="@color/riotx_header_panel_border_mobile_dark" />
</shape>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="10dp" />
<solid android:color="@color/riotx_header_panel_border_mobile_light" />
</shape>

View File

@ -34,7 +34,9 @@
android:id="@+id/videoMediaViewerThumbnailView" android:id="@+id/videoMediaViewerThumbnailView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="center"
android:visibility="gone" android:visibility="gone"
android:scaleType="centerInside"
tools:visibility="visible" /> tools:visibility="visible" />
<ProgressBar <ProgressBar
@ -49,6 +51,7 @@
android:id="@+id/videoMediaViewerVideoView" android:id="@+id/videoMediaViewerVideoView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="center"
android:visibility="gone" /> android:visibility="gone" />
<TextView <TextView

View File

@ -6,29 +6,6 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<FrameLayout
android:id="@+id/syncProgressBarWrap"
android:layout_width="match_parent"
android:layout_height="3dp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/roomToolbar"
tools:visibility="visible">
<ProgressBar
android:id="@+id/syncProgressBar"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="14dp"
android:layout_gravity="center"
android:background="?riotx_header_panel_background"
android:indeterminate="true"
android:visibility="gone"
tools:visibility="visible" />
</FrameLayout>
<!-- Trick to remove surrounding padding (clip frome wrapping frame) -->
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/roomToolbar" android:id="@+id/roomToolbar"
style="@style/VectorToolbarStyle" style="@style/VectorToolbarStyle"
@ -94,12 +71,28 @@
</androidx.appcompat.widget.Toolbar> </androidx.appcompat.widget.Toolbar>
<androidx.constraintlayout.widget.Barrier <!-- Trick to remove surrounding padding (clip frome wrapping frame) -->
android:id="@+id/recyclerViewBarrier" <FrameLayout
android:layout_width="wrap_content" android:id="@+id/syncProgressBarWrap"
android:layout_height="wrap_content" android:layout_width="match_parent"
app:barrierDirection="top" android:layout_height="3dp"
app:constraint_referenced_ids="composerLayout,notificationAreaView" /> android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/roomToolbar"
tools:visibility="visible">
<ProgressBar
android:id="@+id/syncProgressBar"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="14dp"
android:layout_gravity="center"
android:background="?riotx_header_panel_background"
android:indeterminate="true"
android:visibility="gone"
tools:visibility="visible" />
</FrameLayout>
<com.airbnb.epoxy.EpoxyRecyclerView <com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/recyclerView" android:id="@+id/recyclerView"
@ -112,24 +105,12 @@
app:layout_constraintTop_toBottomOf="@id/syncProgressBarWrap" app:layout_constraintTop_toBottomOf="@id/syncProgressBarWrap"
tools:listitem="@layout/item_timeline_event_base" /> tools:listitem="@layout/item_timeline_event_base" />
<im.vector.riotx.core.ui.views.JumpToReadMarkerView <androidx.constraintlayout.widget.Barrier
android:id="@+id/jumpToReadMarkerView" android:id="@+id/recyclerViewBarrier"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:visibility="gone" app:barrierDirection="top"
app:layout_constraintEnd_toEndOf="parent" app:constraint_referenced_ids="composerLayout,notificationAreaView" />
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/syncProgressBarWrap" />
<im.vector.riotx.core.ui.views.NotificationAreaView
android:id="@+id/notificationAreaView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:paddingBottom="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<im.vector.riotx.features.home.room.detail.composer.TextComposerView <im.vector.riotx.features.home.room.detail.composer.TextComposerView
android:id="@+id/composerLayout" android:id="@+id/composerLayout"
@ -141,6 +122,16 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" /> app:layout_constraintStart_toStartOf="parent" />
<im.vector.riotx.core.ui.views.NotificationAreaView
android:id="@+id/notificationAreaView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:paddingBottom="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<im.vector.riotx.features.invite.VectorInviteView <im.vector.riotx.features.invite.VectorInviteView
android:id="@+id/inviteView" android:id="@+id/inviteView"
android:layout_width="0dp" android:layout_width="0dp"
@ -153,4 +144,5 @@
app:layout_constraintTop_toBottomOf="@+id/roomToolbar" app:layout_constraintTop_toBottomOf="@+id/roomToolbar"
tools:visibility="visible" /> tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -5,38 +5,26 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_vertical" android:gravity="center_vertical"
android:minHeight="40dp"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingStart="8dp" android:paddingStart="8dp"
android:paddingEnd="8dp"> android:paddingEnd="8dp">
<ImageView <ImageView
android:id="@+id/readReceiptAvatar" android:id="@+id/readReceiptAvatar"
android:layout_width="24dp" android:layout_width="32dp"
android:layout_height="24dp" android:layout_height="32dp"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
tools:src="@tools:sample/avatars" /> tools:src="@tools:sample/avatars" />
<TextView <TextView
android:id="@+id/readReceiptName" android:id="@+id/readReceiptName"
android:layout_width="0dp" style="@style/BottomSheetItemTextMain"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_weight="1"
android:ellipsize="end"
android:lines="1"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:textColor="?riotx_text_primary"
android:textSize="16sp"
tools:text="@sample/matrix.json/data/displayName" /> tools:text="@sample/matrix.json/data/displayName" />
<TextView <TextView
android:id="@+id/readReceiptDate" android:id="@+id/readReceiptDate"
android:layout_width="wrap_content" style="@style/BottomSheetItemTime"
android:layout_height="wrap_content"
android:lines="1"
android:textColor="?riotx_text_secondary"
android:textSize="12sp"
tools:text="10:44" /> tools:text="10:44" />
</LinearLayout> </LinearLayout>

View File

@ -6,6 +6,7 @@
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingStart="8dp" android:paddingStart="8dp"
android:minHeight="40dp"
android:paddingEnd="8dp"> android:paddingEnd="8dp">
<TextView <TextView
@ -22,25 +23,12 @@
<TextView <TextView
android:id="@+id/itemSimpleReactionInfoMemberName" android:id="@+id/itemSimpleReactionInfoMemberName"
android:layout_width="0dp" style="@style/BottomSheetItemTextMain"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:layout_weight="1"
android:ellipsize="end"
android:lines="1"
android:textColor="?android:textColorPrimary"
android:textSize="16sp"
tools:text="@sample/matrix.json/data/displayName" /> tools:text="@sample/matrix.json/data/displayName" />
<TextView <TextView
android:id="@+id/itemSimpleReactionInfoTime" android:id="@+id/itemSimpleReactionInfoTime"
android:layout_width="wrap_content" style="@style/BottomSheetItemTime"
android:layout_height="wrap_content"
android:lines="1"
android:textColor="?android:textColorSecondary"
android:textSize="12sp"
tools:text="10:44" /> tools:text="10:44" />

View File

@ -122,26 +122,17 @@
</ViewStub> </ViewStub>
<im.vector.riotx.core.ui.views.ReadReceiptsView <im.vector.riotx.core.ui.views.ReadReceiptsView
android:id="@+id/readReceiptsView" android:id="@+id/readReceiptsView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp" android:layout_marginBottom="4dp"
app:layout_constraintBottom_toTopOf="@+id/readMarkerView" android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent" />
<im.vector.riotx.core.ui.views.ReadMarkerView
android:id="@+id/readMarkerView"
android:layout_width="0dp"
android:layout_height="2dp"
android:background="?attr/vctr_unread_marker_line_color"
android:layout_marginBottom="2dp"
android:visibility="invisible"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" /> tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -58,21 +58,10 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp" android:layout_marginBottom="4dp"
app:layout_constraintBottom_toTopOf="@+id/readMarkerView" android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent" />
<im.vector.riotx.core.ui.views.ReadMarkerView
android:id="@+id/readMarkerView"
android:layout_width="0dp"
android:layout_height="2dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="2dp"
android:background="?attr/vctr_unread_marker_line_color"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" /> tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

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