forked from GitHub-Mirror/riotX-android
Compare commits
34 Commits
feature/re
...
master
Author | SHA1 | Date | |
---|---|---|---|
a89f0ddd1d | |||
58f878fca9 | |||
88095e4bd9 | |||
47d22a3d5e | |||
28e82cb8ea | |||
35817245cb | |||
75266f42bb | |||
95c4c9ce56 | |||
ce5570105d | |||
188a9aebfa | |||
c95223f5d2 | |||
ef0362ba9c | |||
ea242f6737 | |||
cbc08d834b | |||
0ab6b33fb6 | |||
1b394527b6 | |||
a8f1388721 | |||
166be4e289 | |||
b49ccefe63 | |||
825760d17e | |||
b5af62c3ea | |||
a51d96bf00 | |||
7e142d201d | |||
2be6058971 | |||
49d73f360e | |||
bd88d85a21 | |||
be4fc5cce6 | |||
704da1be55 | |||
5d002532d3 | |||
d4161e9a1a | |||
7966ebef03 | |||
ed5faca5d2 | |||
9cd69d1e33 | |||
df6080b1da |
19
CHANGES.md
19
CHANGES.md
@ -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)
|
||||||
===================================================
|
===================================================
|
||||||
|
@ -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
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
)
|
|
@ -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>>
|
||||||
}
|
}
|
@ -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.
|
||||||
@ -63,16 +56,9 @@ interface Timeline {
|
|||||||
*/
|
*/
|
||||||
fun paginate(direction: Direction, count: Int)
|
fun paginate(direction: Direction, count: Int)
|
||||||
|
|
||||||
fun pendingEventCount(): Int
|
fun pendingEventCount() : Int
|
||||||
|
|
||||||
fun failedToDeliverEventCount(): Int
|
|
||||||
|
|
||||||
fun getIndexOfEvent(eventId: String?): Int?
|
|
||||||
|
|
||||||
fun getTimelineEventAtIndex(index: Int): TimelineEvent?
|
|
||||||
|
|
||||||
fun getTimelineEventWithId(eventId: String?): TimelineEvent?
|
|
||||||
|
|
||||||
|
fun failedToDeliverEventCount() : Int
|
||||||
|
|
||||||
interface Listener {
|
interface Listener {
|
||||||
/**
|
/**
|
||||||
|
@ -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>()
|
||||||
|
@ -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)
|
||||||
|
@ -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 {
|
||||||
@ -43,12 +43,12 @@ internal class RoomSummaryMapper @Inject constructor(
|
|||||||
//for now decrypt sync
|
//for now decrypt sync
|
||||||
try {
|
try {
|
||||||
val result = cryptoService.decryptEvent(latestEvent.root, latestEvent.root.roomId + UUID.randomUUID().toString())
|
val result = cryptoService.decryptEvent(latestEvent.root, latestEvent.root.roomId + UUID.randomUUID().toString())
|
||||||
latestEvent.root.mxDecryptionResult = OlmDecryptionResult(
|
latestEvent.root.mxDecryptionResult = OlmDecryptionResult(
|
||||||
payload = result.clearEvent,
|
payload = result.clearEvent,
|
||||||
senderKey = result.senderCurve25519Key,
|
senderKey = result.senderCurve25519Key,
|
||||||
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
||||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||||
)
|
)
|
||||||
} catch (e: MXCryptoError) {
|
} catch (e: MXCryptoError) {
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -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
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
|
||||||
|
|
||||||
}
|
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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")
|
||||||
|
@ -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)
|
|
||||||
}
|
|
@ -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"
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
@ -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
|
||||||
|
@ -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
|
@ -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)
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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> {
|
||||||
@ -220,7 +216,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
|
|||||||
}
|
}
|
||||||
return Transformations.map(liveEntity) { realmResults ->
|
return Transformations.map(liveEntity) { realmResults ->
|
||||||
realmResults.firstOrNull()?.asDomain()
|
realmResults.firstOrNull()?.asDomain()
|
||||||
?: EventAnnotationsSummary(eventId, emptyList(), null)
|
?: EventAnnotationsSummary(eventId, emptyList(), null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -233,7 +229,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
|
|||||||
private fun saveLocalEcho(event: Event) {
|
private fun saveLocalEcho(event: Event) {
|
||||||
monarchy.writeAsync { realm ->
|
monarchy.writeAsync { realm ->
|
||||||
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst()
|
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst()
|
||||||
?: return@writeAsync
|
?: return@writeAsync
|
||||||
roomEntity.addSendingEvent(event)
|
roomEntity.addSendingEvent(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,21 +163,27 @@ 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Public methods ******************************************************************************
|
// Public methods ******************************************************************************
|
||||||
|
|
||||||
override fun paginate(direction: Timeline.Direction, count: Int) {
|
override fun paginate(direction: Timeline.Direction, count: Int) {
|
||||||
BACKGROUND_HANDLER.post {
|
BACKGROUND_HANDLER.post {
|
||||||
@ -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,38 +272,20 @@ 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
|
// Private methods *****************************************************************************
|
||||||
|
|
||||||
override fun rebuildEvent(eventId: String, hasReadMarker: Boolean): Boolean {
|
|
||||||
return rebuildEvent(eventId) { te ->
|
|
||||||
te.copy(hasReadMarker = hasReadMarker)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onReadMarkerUpdated() {
|
|
||||||
postSnapshot()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Private methods *****************************************************************************
|
|
||||||
|
|
||||||
private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean {
|
|
||||||
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 ->
|
||||||
@ -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,24 +566,12 @@ 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() {
|
// Extension methods ***************************************************************************
|
||||||
prevDisplayIndex = DISPLAY_INDEX_UNKNOWN
|
|
||||||
nextDisplayIndex = DISPLAY_INDEX_UNKNOWN
|
|
||||||
builtEvents.clear()
|
|
||||||
builtEventsIdMap.clear()
|
|
||||||
backwardsPaginationState.set(PaginationState())
|
|
||||||
forwardsPaginationState.set(PaginationState())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Extension methods ***************************************************************************
|
|
||||||
|
|
||||||
private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {
|
private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {
|
||||||
return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS
|
return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS
|
||||||
|
@ -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)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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 d’envoi</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -48,7 +48,7 @@
|
|||||||
<string name="notice_room_third_party_registered_invite">%1$s èt d’uutnodigienge vo %2$s anveird</string>
|
<string name="notice_room_third_party_registered_invite">%1$s èt d’uutnodigienge 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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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'
|
||||||
|
67
vector/src/main/java/im/vector/riotx/EmojiCompatWrapper.kt
Normal file
67
vector/src/main/java/im/vector/riotx/EmojiCompatWrapper.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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()!!
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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) {
|
||||||
|
@ -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?
|
|
||||||
)
|
|
@ -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,18 +27,15 @@ 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()
|
||||||
object RejectInvite : RoomDetailActions()
|
object RejectInvite : RoomDetailActions()
|
||||||
|
|
||||||
@ -49,4 +47,5 @@ sealed class RoomDetailActions {
|
|||||||
object ClearSendQueue : RoomDetailActions()
|
object ClearSendQueue : RoomDetailActions()
|
||||||
object ResendAll : RoomDetailActions()
|
object ResendAll : RoomDetailActions()
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -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()
|
||||||
@ -734,7 +684,7 @@ class RoomDetailFragment :
|
|||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TimelineEventController.Callback ************************************************************
|
// TimelineEventController.Callback ************************************************************
|
||||||
|
|
||||||
override fun onUrlClicked(url: String): Boolean {
|
override fun onUrlClicked(url: String): Boolean {
|
||||||
return permalinkHandler.launch(requireActivity(), url, object : NavigateToRoomInterceptor {
|
return permalinkHandler.launch(requireActivity(), url, object : NavigateToRoomInterceptor {
|
||||||
@ -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,15 +836,7 @@ class RoomDetailFragment :
|
|||||||
.show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS")
|
.show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReadMarkerLongDisplayed(informationData: MessageInformationData) {
|
// AutocompleteUserPresenter.Callback
|
||||||
val firstVisibleItem = layoutManager.findFirstVisibleItemPosition()
|
|
||||||
val eventId = timelineEventController.searchEventIdAtPosition(firstVisibleItem)
|
|
||||||
if (eventId != null) {
|
|
||||||
roomDetailViewModel.process(RoomDetailActions.SetReadMarkerAction(eventId))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AutocompleteUserPresenter.Callback
|
|
||||||
|
|
||||||
override fun onQueryUsers(query: CharSequence?) {
|
override fun onQueryUsers(query: CharSequence?) {
|
||||||
textComposerViewModel.process(TextComposerActions.QueryUsers(query))
|
textComposerViewModel.process(TextComposerActions.QueryUsers(query))
|
||||||
@ -1063,7 +1001,7 @@ class RoomDetailFragment :
|
|||||||
snack.show()
|
snack.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
// VectorInviteView.Callback
|
// VectorInviteView.Callback
|
||||||
|
|
||||||
override fun onAcceptInvite() {
|
override fun onAcceptInvite() {
|
||||||
notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId)
|
notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId)
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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) }
|
||||||
@ -121,37 +116,30 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||||||
|
|
||||||
fun process(action: RoomDetailActions) {
|
fun process(action: RoomDetailActions) {
|
||||||
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()
|
is RoomDetailActions.RejectInvite -> handleRejectInvite()
|
||||||
is RoomDetailActions.RejectInvite -> handleRejectInvite()
|
is RoomDetailActions.RedactAction -> handleRedactEvent(action)
|
||||||
is RoomDetailActions.RedactAction -> handleRedactEvent(action)
|
is RoomDetailActions.UndoReaction -> handleUndoReact(action)
|
||||||
is RoomDetailActions.UndoReaction -> handleUndoReact(action)
|
is RoomDetailActions.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
|
||||||
is RoomDetailActions.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
|
is RoomDetailActions.EnterEditMode -> handleEditAction(action)
|
||||||
is RoomDetailActions.EnterEditMode -> handleEditAction(action)
|
is RoomDetailActions.EnterQuoteMode -> handleQuoteAction(action)
|
||||||
is RoomDetailActions.EnterQuoteMode -> handleQuoteAction(action)
|
is RoomDetailActions.EnterReplyMode -> handleReplyAction(action)
|
||||||
is RoomDetailActions.EnterReplyMode -> handleReplyAction(action)
|
is RoomDetailActions.DownloadFile -> handleDownloadFile(action)
|
||||||
is RoomDetailActions.DownloadFile -> handleDownloadFile(action)
|
is RoomDetailActions.NavigateToEvent -> handleNavigateToEvent(action)
|
||||||
is RoomDetailActions.NavigateToEvent -> handleNavigateToEvent(action)
|
is RoomDetailActions.HandleTombstoneEvent -> handleTombstoneEvent(action)
|
||||||
is RoomDetailActions.HandleTombstoneEvent -> handleTombstoneEvent(action)
|
is RoomDetailActions.ResendMessage -> handleResendEvent(action)
|
||||||
is RoomDetailActions.ResendMessage -> handleResendEvent(action)
|
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()
|
else -> Timber.e("Unhandled Action: $action")
|
||||||
is RoomDetailActions.SetReadMarkerAction -> handleSetReadMarkerAction(action)
|
|
||||||
is RoomDetailActions.MarkAllAsRead -> handleMarkAllAsRead()
|
|
||||||
else -> Timber.e("Unhandled Action: $action")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleEventInvisible(action: RoomDetailActions.TimelineEventTurnsInvisible) {
|
|
||||||
invisibleEventsObservable.accept(action)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleTombstoneEvent(action: RoomDetailActions.HandleTombstoneEvent) {
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
eventId = targetEventId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_navigateToEvent.postLiveEvent(targetEventId)
|
||||||
|
} else {
|
||||||
|
// change timeline
|
||||||
|
timeline.dispose()
|
||||||
|
timeline = room.createTimeline(targetEventId, timelineSettings)
|
||||||
|
timeline.start()
|
||||||
|
|
||||||
|
withState {
|
||||||
|
if (it.eventId == targetEventId) {
|
||||||
|
// ensure another click on the same permalink will also do a scroll
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
eventId = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
eventId = targetEventId,
|
||||||
|
timeline = this@RoomDetailViewModel.timeline
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_navigateToEvent.postLiveEvent(targetEventId)
|
||||||
}
|
}
|
||||||
if (action.highlight) {
|
|
||||||
setState { copy(highlightedEventId = targetEventId) }
|
|
||||||
}
|
|
||||||
_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 ->
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,50 +327,24 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||||||
return shouldAdd
|
return shouldAdd
|
||||||
}
|
}
|
||||||
|
|
||||||
fun searchPositionOfEvent(eventId: String): Int? = synchronized(modelCache) {
|
fun searchPositionOfEvent(eventId: String): Int? {
|
||||||
// Search in the cache
|
synchronized(modelCache) {
|
||||||
var realPosition = 0
|
// Search in the cache
|
||||||
for (i in 0 until modelCache.size) {
|
modelCache.forEachIndexed { idx, cacheItemData ->
|
||||||
val itemCache = modelCache[i]
|
if (cacheItemData?.eventId == eventId) {
|
||||||
if (itemCache?.eventId == eventId) {
|
return idx
|
||||||
return realPosition
|
}
|
||||||
}
|
|
||||||
if (itemCache?.eventModel != null) {
|
|
||||||
realPosition++
|
|
||||||
}
|
|
||||||
if (itemCache?.mergedHeaderModel != null) {
|
|
||||||
realPosition++
|
|
||||||
}
|
|
||||||
if (itemCache?.formattedDayModel != null) {
|
|
||||||
realPosition++
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun searchEventIdAtPosition(position: Int): String? = synchronized(modelCache) {
|
private data class CacheItemData(
|
||||||
var offsetValue = 0
|
val localId: Long,
|
||||||
for (i in 0 until position) {
|
val eventId: String?,
|
||||||
val itemCache = modelCache[i]
|
val eventModel: EpoxyModel<*>? = null,
|
||||||
if (itemCache?.eventModel == null) {
|
val mergedHeaderModel: MergedHeaderItem? = null,
|
||||||
offsetValue--
|
val formattedDayModel: DaySeparatorItem? = null
|
||||||
}
|
)
|
||||||
if (itemCache?.mergedHeaderModel != null) {
|
|
||||||
offsetValue++
|
|
||||||
}
|
|
||||||
if (itemCache?.formattedDayModel != null) {
|
|
||||||
offsetValue++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return modelCache.getOrNull(position - offsetValue)?.eventId
|
|
||||||
}
|
|
||||||
|
|
||||||
private data class CacheItemData(
|
|
||||||
val localId: Long,
|
|
||||||
val eventId: String?,
|
|
||||||
val eventModel: EpoxyModel<*>? = null,
|
|
||||||
val mergedHeaderModel: MergedHeaderItem? = null,
|
|
||||||
val formattedDayModel: DaySeparatorItem? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -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,41 +98,36 @@ 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 =
|
||||||
event.getLastMessageContent()
|
event.getLastMessageContent()
|
||||||
?: //Malformed content, we should echo something on screen
|
?: //Malformed content, we should echo something on screen
|
||||||
return DefaultItem_().text(stringProvider.getString(R.string.malformed_message))
|
return DefaultItem_().text(stringProvider.getString(R.string.malformed_message))
|
||||||
|
|
||||||
if (messageContent.relatesTo?.type == RelationType.REPLACE
|
if (messageContent.relatesTo?.type == RelationType.REPLACE
|
||||||
|| event.isEncrypted() && event.root.content.toModel<EncryptedEventContent>()?.relatesTo?.type == RelationType.REPLACE
|
|| event.isEncrypted() && event.root.content.toModel<EncryptedEventContent>()?.relatesTo?.type == RelationType.REPLACE
|
||||||
) {
|
) {
|
||||||
// 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,29 +215,42 @@ 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(
|
||||||
filename = messageContent.body,
|
filename = messageContent.body,
|
||||||
url = messageContent.videoInfo?.thumbnailFile?.url
|
url = messageContent.videoInfo?.thumbnailFile?.url
|
||||||
?: messageContent.videoInfo?.thumbnailUrl,
|
?: messageContent.videoInfo?.thumbnailUrl,
|
||||||
elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(),
|
elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(),
|
||||||
height = messageContent.videoInfo?.height,
|
height = messageContent.videoInfo?.height,
|
||||||
maxHeight = maxHeight,
|
maxHeight = maxHeight,
|
||||||
@ -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)
|
||||||
//click on the text
|
.reactionPillCallback(callback)
|
||||||
|
.readReceiptsCallback(callback)
|
||||||
|
.emojiTypeFace(emojiCompatFontProvider.typeface)
|
||||||
|
//click on the text
|
||||||
|
.cellClickListener(
|
||||||
|
DebouncedClickListener(View.OnClickListener { view ->
|
||||||
|
callback?.onEventCellClicked(informationData, messageContent, view)
|
||||||
|
}))
|
||||||
|
.longClickListener { view ->
|
||||||
|
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
||||||
|
?: false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun annotateWithEdited(linkifiedBody: CharSequence,
|
private fun annotateWithEdited(linkifiedBody: CharSequence,
|
||||||
@ -282,17 +356,16 @@ class MessageItemFactory @Inject constructor(
|
|||||||
//nop
|
//nop
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
editStart,
|
editStart,
|
||||||
editEnd,
|
editEnd,
|
||||||
Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
|
Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
|
||||||
return spannable
|
return spannable
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
@ -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,
|
||||||
|
@ -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_())
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
@ -99,7 +96,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
|
|||||||
|
|
||||||
private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?): CharSequence? {
|
private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?): CharSequence? {
|
||||||
val historyVisibility = event.getClearContent().toModel<RoomHistoryVisibilityContent>()?.historyVisibility
|
val historyVisibility = event.getClearContent().toModel<RoomHistoryVisibilityContent>()?.historyVisibility
|
||||||
?: return null
|
?: return null
|
||||||
|
|
||||||
val formattedVisibility = when (historyVisibility) {
|
val formattedVisibility = when (historyVisibility) {
|
||||||
RoomHistoryVisibility.SHARED -> stringProvider.getString(R.string.notice_room_visibility_shared)
|
RoomHistoryVisibility.SHARED -> stringProvider.getString(R.string.notice_room_visibility_shared)
|
||||||
@ -149,7 +146,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
|
|||||||
stringProvider.getString(R.string.notice_display_name_removed, event.senderId, prevEventContent?.displayName)
|
stringProvider.getString(R.string.notice_display_name_removed, event.senderId, prevEventContent?.displayName)
|
||||||
else ->
|
else ->
|
||||||
stringProvider.getString(R.string.notice_display_name_changed_from,
|
stringProvider.getString(R.string.notice_display_name_changed_from,
|
||||||
event.senderId, prevEventContent?.displayName, eventContent?.displayName)
|
event.senderId, prevEventContent?.displayName, eventContent?.displayName)
|
||||||
}
|
}
|
||||||
displayText.append(displayNameText)
|
displayText.append(displayNameText)
|
||||||
}
|
}
|
||||||
@ -176,7 +173,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
|
|||||||
when {
|
when {
|
||||||
eventContent.thirdPartyInvite != null ->
|
eventContent.thirdPartyInvite != null ->
|
||||||
stringProvider.getString(R.string.notice_room_third_party_registered_invite,
|
stringProvider.getString(R.string.notice_room_third_party_registered_invite,
|
||||||
targetDisplayName, eventContent.thirdPartyInvite?.displayName)
|
targetDisplayName, eventContent.thirdPartyInvite?.displayName)
|
||||||
TextUtils.equals(event.stateKey, selfUserId) ->
|
TextUtils.equals(event.stateKey, selfUserId) ->
|
||||||
stringProvider.getString(R.string.notice_room_invite_you, senderDisplayName)
|
stringProvider.getString(R.string.notice_room_invite_you, senderDisplayName)
|
||||||
event.stateKey.isNullOrEmpty() ->
|
event.stateKey.isNullOrEmpty() ->
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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) }
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
@ -67,17 +59,11 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
|
|||||||
informationData.avatarUrl,
|
informationData.avatarUrl,
|
||||||
informationData.senderId,
|
informationData.senderId,
|
||||||
informationData.memberName?.toString()
|
informationData.memberName?.toString()
|
||||||
?: informationData.senderId,
|
?: informationData.senderId,
|
||||||
holder.avatarImageView
|
holder.avatarImageView
|
||||||
)
|
)
|
||||||
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 {
|
||||||
|
@ -1,22 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
|
* 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");
|
* you may not use this file except in compliance with the License.
|
||||||
* you may not use this file except in compliance with the License.
|
* You may obtain a copy of the License at
|
||||||
* You may obtain a copy of the License at
|
*
|
||||||
*
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
*
|
||||||
*
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* 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
|
||||||
@ -52,10 +50,10 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
|||||||
|
|
||||||
val showInformation =
|
val showInformation =
|
||||||
addDaySeparator
|
addDaySeparator
|
||||||
|| event.senderAvatar != nextEvent?.senderAvatar
|
|| event.senderAvatar != nextEvent?.senderAvatar
|
||||||
|| event.getDisambiguatedDisplayName() != nextEvent?.getDisambiguatedDisplayName()
|
|| event.getDisambiguatedDisplayName() != nextEvent?.getDisambiguatedDisplayName()
|
||||||
|| (nextEvent.root.getClearType() != EventType.MESSAGE && nextEvent.root.getClearType() != EventType.ENCRYPTED)
|
|| (nextEvent.root.getClearType() != EventType.MESSAGE && nextEvent.root.getClearType() != EventType.ENCRYPTED)
|
||||||
|| isNextMessageReceivedMoreThanOneHourAgo
|
|| isNextMessageReceivedMoreThanOneHourAgo
|
||||||
|
|
||||||
val time = dateFormatter.formatMessageHour(date)
|
val time = dateFormatter.formatMessageHour(date)
|
||||||
val avatarUrl = event.senderAvatar
|
val avatarUrl = event.senderAvatar
|
||||||
@ -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
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -89,18 +89,44 @@ 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 {
|
||||||
videoView.isVisible = true
|
|
||||||
videoView.setVideoPath(resolvedUrl)
|
//Temporary code, some remote videos are not played by videoview setVideoUri
|
||||||
videoView.start()
|
//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.setVideoPath(data.path)
|
||||||
|
videoView.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(failure: Throwable) {
|
||||||
|
loadingView.isVisible = false
|
||||||
|
errorView.isVisible = true
|
||||||
|
errorView.text = errorFormatter.toHumanReadable(failure)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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>
|
6
vector/src/main/res/drawable/pill_receipt_black.xml
Normal file
6
vector/src/main/res/drawable/pill_receipt_black.xml
Normal 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>
|
6
vector/src/main/res/drawable/pill_receipt_dark.xml
Normal file
6
vector/src/main/res/drawable/pill_receipt_dark.xml
Normal 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>
|
6
vector/src/main/res/drawable/pill_receipt_light.xml
Normal file
6
vector/src/main/res/drawable/pill_receipt_light.xml
Normal 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>
|
@ -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
|
||||||
|
@ -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>
|
@ -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>
|
@ -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" />
|
||||||
|
|
||||||
|
|
||||||
|
@ -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>
|
@ -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
Reference in New Issue
Block a user