Merge branch 'develop' into feature/sync_issues
This commit is contained in:
commit
990a266d83
@ -7,6 +7,8 @@ Features ✨:
|
|||||||
|
|
||||||
Improvements 🙌:
|
Improvements 🙌:
|
||||||
- Send mention Pills from composer
|
- Send mention Pills from composer
|
||||||
|
- Links in message preview in the bottom sheet are now active.
|
||||||
|
- Rework the read marker to make it more usable
|
||||||
|
|
||||||
Other changes:
|
Other changes:
|
||||||
- Fix a small grammatical error when an empty room list is shown.
|
- Fix a small grammatical error when an empty room list is shown.
|
||||||
|
|||||||
@ -30,10 +30,16 @@ package im.vector.matrix.android.api.session.room.timeline
|
|||||||
*/
|
*/
|
||||||
interface Timeline {
|
interface Timeline {
|
||||||
|
|
||||||
var listener: Listener?
|
val timelineID: String
|
||||||
|
|
||||||
val isLive: Boolean
|
val isLive: Boolean
|
||||||
|
|
||||||
|
fun addListener(listener: Listener): Boolean
|
||||||
|
|
||||||
|
fun removeListener(listener: Listener): Boolean
|
||||||
|
|
||||||
|
fun removeAllListeners()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
@ -98,7 +104,7 @@ interface Timeline {
|
|||||||
interface Listener {
|
interface Listener {
|
||||||
/**
|
/**
|
||||||
* Call when the timeline has been updated through pagination or sync.
|
* Call when the timeline has been updated through pagination or sync.
|
||||||
* @param snapshot the most uptodate snapshot
|
* @param snapshot the most up to date snapshot
|
||||||
*/
|
*/
|
||||||
fun onUpdated(snapshot: List<TimelineEvent>)
|
fun onUpdated(snapshot: List<TimelineEvent>)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,8 +41,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
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun getDisambiguatedDisplayName(): String {
|
fun getDisambiguatedDisplayName(): String {
|
||||||
|
|||||||
@ -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
|
||||||
@ -113,7 +112,6 @@ internal fun ChunkEntity.add(localRealm: Realm,
|
|||||||
it.roomId = roomId
|
it.roomId = roomId
|
||||||
it.annotations = EventAnnotationsSummaryEntity.where(localRealm, eventId).findFirst()
|
it.annotations = EventAnnotationsSummaryEntity.where(localRealm, eventId).findFirst()
|
||||||
it.readReceipts = readReceiptsSummaryEntity
|
it.readReceipts = readReceiptsSummaryEntity
|
||||||
it.readMarker = ReadMarkerEntity.where(localRealm, roomId = roomId, eventId = eventId).findFirst()
|
|
||||||
}
|
}
|
||||||
eventEntity.updateSenderData(localRealm, this)
|
eventEntity.updateSenderData(localRealm, this)
|
||||||
val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size
|
val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size
|
||||||
@ -122,14 +120,14 @@ internal fun ChunkEntity.add(localRealm: Realm,
|
|||||||
|
|
||||||
internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
|
internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
|
||||||
return when (direction) {
|
return when (direction) {
|
||||||
PaginationDirection.FORWARDS -> forwardsDisplayIndex
|
PaginationDirection.FORWARDS -> forwardsDisplayIndex
|
||||||
PaginationDirection.BACKWARDS -> backwardsDisplayIndex
|
PaginationDirection.BACKWARDS -> backwardsDisplayIndex
|
||||||
} ?: defaultValue
|
} ?: defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
|
internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
|
||||||
return when (direction) {
|
return when (direction) {
|
||||||
PaginationDirection.FORWARDS -> forwardsStateIndex
|
PaginationDirection.FORWARDS -> forwardsStateIndex
|
||||||
PaginationDirection.BACKWARDS -> backwardsStateIndex
|
PaginationDirection.BACKWARDS -> backwardsStateIndex
|
||||||
} ?: defaultValue
|
} ?: defaultValue
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,8 +46,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?.isNotEmpty() == true
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,8 +17,6 @@
|
|||||||
package im.vector.matrix.android.internal.database.model
|
package im.vector.matrix.android.internal.database.model
|
||||||
|
|
||||||
import io.realm.RealmObject
|
import io.realm.RealmObject
|
||||||
import io.realm.RealmResults
|
|
||||||
import io.realm.annotations.LinkingObjects
|
|
||||||
import io.realm.annotations.PrimaryKey
|
import io.realm.annotations.PrimaryKey
|
||||||
|
|
||||||
internal open class ReadMarkerEntity(
|
internal open class ReadMarkerEntity(
|
||||||
@ -27,8 +25,5 @@ internal open class ReadMarkerEntity(
|
|||||||
var eventId: String = ""
|
var eventId: String = ""
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
|
||||||
@LinkingObjects("readMarker")
|
|
||||||
val timelineEvent: RealmResults<TimelineEventEntity>? = null
|
|
||||||
|
|
||||||
companion object
|
companion object
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,8 +29,7 @@ internal open class TimelineEventEntity(var localId: Long = 0,
|
|||||||
var senderName: String? = null,
|
var senderName: String? = null,
|
||||||
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")
|
||||||
|
|||||||
@ -22,13 +22,9 @@ import io.realm.Realm
|
|||||||
import io.realm.RealmQuery
|
import io.realm.RealmQuery
|
||||||
import io.realm.kotlin.where
|
import io.realm.kotlin.where
|
||||||
|
|
||||||
internal fun ReadMarkerEntity.Companion.where(realm: Realm, roomId: String, eventId: String? = null): RealmQuery<ReadMarkerEntity> {
|
internal fun ReadMarkerEntity.Companion.where(realm: Realm, roomId: String): RealmQuery<ReadMarkerEntity> {
|
||||||
val query = realm.where<ReadMarkerEntity>()
|
return realm.where<ReadMarkerEntity>()
|
||||||
.equalTo(ReadMarkerEntityFields.ROOM_ID, roomId)
|
.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 {
|
internal fun ReadMarkerEntity.Companion.getOrCreate(realm: Realm, roomId: String): ReadMarkerEntity {
|
||||||
|
|||||||
@ -18,7 +18,9 @@ package im.vector.matrix.android.internal.database.query
|
|||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
import im.vector.matrix.android.internal.database.model.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 io.realm.Realm
|
||||||
|
|
||||||
internal fun isEventRead(monarchy: Monarchy,
|
internal fun isEventRead(monarchy: Monarchy,
|
||||||
userId: String?,
|
userId: String?,
|
||||||
@ -39,8 +41,10 @@ internal fun isEventRead(monarchy: Monarchy,
|
|||||||
isEventRead = if (eventToCheck?.sender == userId) {
|
isEventRead = if (eventToCheck?.sender == userId) {
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst() ?: return@doWithRealm
|
val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst()
|
||||||
val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex ?: Int.MIN_VALUE
|
?: return@doWithRealm
|
||||||
|
val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex
|
||||||
|
?: Int.MIN_VALUE
|
||||||
val eventToCheckIndex = eventToCheck?.displayIndex ?: Int.MAX_VALUE
|
val eventToCheckIndex = eventToCheck?.displayIndex ?: Int.MAX_VALUE
|
||||||
|
|
||||||
eventToCheckIndex <= readReceiptIndex
|
eventToCheckIndex <= readReceiptIndex
|
||||||
@ -49,3 +53,21 @@ internal fun isEventRead(monarchy: Monarchy,
|
|||||||
|
|
||||||
return isEventRead
|
return isEventRead
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun isReadMarkerMoreRecent(monarchy: Monarchy,
|
||||||
|
roomId: String?,
|
||||||
|
eventId: String?): Boolean {
|
||||||
|
if (roomId.isNullOrBlank() || eventId.isNullOrBlank()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
|
||||||
|
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) ?: return false
|
||||||
|
val eventToCheck = liveChunk.timelineEvents.find(eventId)?.root
|
||||||
|
|
||||||
|
val readMarker = ReadMarkerEntity.where(realm, roomId).findFirst() ?: return false
|
||||||
|
val readMarkerIndex = liveChunk.timelineEvents.find(readMarker.eventId)?.root?.displayIndex
|
||||||
|
?: Int.MIN_VALUE
|
||||||
|
val eventToCheckIndex = eventToCheck?.displayIndex ?: Int.MAX_VALUE
|
||||||
|
eventToCheckIndex <= readMarkerIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -18,7 +18,6 @@ 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.session.events.model.LocalEcho
|
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||||
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
|
|
||||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
import im.vector.matrix.android.internal.database.model.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.*
|
import im.vector.matrix.android.internal.database.query.*
|
||||||
@ -57,22 +56,18 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
|
|||||||
|
|
||||||
override suspend fun execute(params: SetReadMarkersTask.Params) {
|
override suspend fun execute(params: SetReadMarkersTask.Params) {
|
||||||
val markers = HashMap<String, String>()
|
val markers = HashMap<String, String>()
|
||||||
val fullyReadEventId: String?
|
|
||||||
val readReceiptEventId: String?
|
|
||||||
|
|
||||||
Timber.v("Execute set read marker with params: $params")
|
Timber.v("Execute set read marker with params: $params")
|
||||||
if (params.markAllAsRead) {
|
val (fullyReadEventId, readReceiptEventId) = 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
|
||||||
}
|
}
|
||||||
fullyReadEventId = latestSyncedEventId
|
Pair(latestSyncedEventId, latestSyncedEventId)
|
||||||
readReceiptEventId = latestSyncedEventId
|
|
||||||
} else {
|
} else {
|
||||||
fullyReadEventId = params.fullyReadEventId
|
Pair(params.fullyReadEventId, params.readReceiptEventId)
|
||||||
readReceiptEventId = params.readReceiptEventId
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fullyReadEventId != null && isReadMarkerMoreRecent(params.roomId, fullyReadEventId)) {
|
if (fullyReadEventId != null && !isReadMarkerMoreRecent(monarchy, params.roomId, fullyReadEventId)) {
|
||||||
if (LocalEcho.isLocalEchoId(fullyReadEventId)) {
|
if (LocalEcho.isLocalEchoId(fullyReadEventId)) {
|
||||||
Timber.w("Can't set read marker for local event $fullyReadEventId")
|
Timber.w("Can't set read marker for local event $fullyReadEventId")
|
||||||
} else {
|
} else {
|
||||||
@ -118,16 +113,4 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isReadMarkerMoreRecent(roomId: String, newReadMarkerId: String): Boolean {
|
|
||||||
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
|
|
||||||
val currentReadMarkerId = ReadMarkerEntity.where(realm, roomId = roomId).findFirst()?.eventId
|
|
||||||
?: return true
|
|
||||||
val readMarkerEvent = TimelineEventEntity.where(realm, roomId = roomId, eventId = currentReadMarkerId).findFirst()
|
|
||||||
val newReadMarkerEvent = TimelineEventEntity.where(realm, roomId = roomId, eventId = newReadMarkerId).findFirst()
|
|
||||||
val currentReadMarkerIndex = readMarkerEvent?.root?.displayIndex ?: Int.MAX_VALUE
|
|
||||||
val newReadMarkerIndex = newReadMarkerEvent?.root?.displayIndex ?: Int.MIN_VALUE
|
|
||||||
newReadMarkerIndex > currentReadMarkerIndex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -62,10 +62,14 @@ internal class TextPillsUtils @Inject constructor(
|
|||||||
var currIndex = 0
|
var currIndex = 0
|
||||||
pills.forEachIndexed { _, (urlSpan, start, end) ->
|
pills.forEachIndexed { _, (urlSpan, start, end) ->
|
||||||
// We want to replace with the pill with a html link
|
// We want to replace with the pill with a html link
|
||||||
|
// append text before pill
|
||||||
append(text, currIndex, start)
|
append(text, currIndex, start)
|
||||||
|
// append the pill
|
||||||
append(String.format(template, urlSpan.userId, urlSpan.displayName))
|
append(String.format(template, urlSpan.userId, urlSpan.displayName))
|
||||||
currIndex = end
|
currIndex = end
|
||||||
}
|
}
|
||||||
|
// append text after the last pill
|
||||||
|
append(text, currIndex, text.length)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -26,7 +26,8 @@ internal interface GetContextOfEventTask : Task<GetContextOfEventTask.Params, To
|
|||||||
|
|
||||||
data class Params(
|
data class Params(
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
val eventId: String
|
val eventId: String,
|
||||||
|
val limit: Int
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,7 +39,7 @@ internal class DefaultGetContextOfEventTask @Inject constructor(private val room
|
|||||||
override suspend fun execute(params: GetContextOfEventTask.Params): TokenChunkEventPersistor.Result {
|
override suspend fun execute(params: GetContextOfEventTask.Params): TokenChunkEventPersistor.Result {
|
||||||
val filter = filterRepository.getRoomFilter()
|
val filter = filterRepository.getRoomFilter()
|
||||||
val response = executeRequest<EventContextResponse> {
|
val response = executeRequest<EventContextResponse> {
|
||||||
apiCall = roomAPI.getContextOfEvent(params.roomId, params.eventId, 0, filter)
|
apiCall = roomAPI.getContextOfEvent(params.roomId, params.eventId, params.limit, filter)
|
||||||
}
|
}
|
||||||
return tokenChunkEventPersistor.insertInDb(response, params.roomId, PaginationDirection.BACKWARDS)
|
return tokenChunkEventPersistor.insertInDb(response, params.roomId, PaginationDirection.BACKWARDS)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -74,22 +74,14 @@ 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")
|
||||||
}
|
}
|
||||||
|
|
||||||
override var listener: Timeline.Listener? = null
|
private val listeners = ArrayList<Timeline.Listener>()
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
BACKGROUND_HANDLER.post {
|
|
||||||
postSnapshot()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val isStarted = AtomicBoolean(false)
|
private val isStarted = AtomicBoolean(false)
|
||||||
private val isReady = AtomicBoolean(false)
|
private val isReady = AtomicBoolean(false)
|
||||||
private val mainHandler = createUIHandler()
|
private val mainHandler = createUIHandler()
|
||||||
@ -110,7 +102,7 @@ internal class DefaultTimeline(
|
|||||||
private val backwardsState = AtomicReference(State())
|
private val backwardsState = AtomicReference(State())
|
||||||
private val forwardsState = AtomicReference(State())
|
private val forwardsState = AtomicReference(State())
|
||||||
|
|
||||||
private val timelineID = UUID.randomUUID().toString()
|
override val timelineID = UUID.randomUUID().toString()
|
||||||
|
|
||||||
override val isLive
|
override val isLive
|
||||||
get() = !hasMoreToLoad(Timeline.Direction.FORWARDS)
|
get() = !hasMoreToLoad(Timeline.Direction.FORWARDS)
|
||||||
@ -197,7 +189,6 @@ internal class DefaultTimeline(
|
|||||||
if (settings.buildReadReceipts) {
|
if (settings.buildReadReceipts) {
|
||||||
hiddenReadReceipts.start(realm, filteredEvents, nonFilteredEvents, this)
|
hiddenReadReceipts.start(realm, filteredEvents, nonFilteredEvents, this)
|
||||||
}
|
}
|
||||||
hiddenReadMarker.start(realm, filteredEvents, nonFilteredEvents, this)
|
|
||||||
isReady.set(true)
|
isReady.set(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -217,7 +208,6 @@ internal class DefaultTimeline(
|
|||||||
if (this::filteredEvents.isInitialized) {
|
if (this::filteredEvents.isInitialized) {
|
||||||
filteredEvents.removeAllChangeListeners()
|
filteredEvents.removeAllChangeListeners()
|
||||||
}
|
}
|
||||||
hiddenReadMarker.dispose()
|
|
||||||
if (settings.buildReadReceipts) {
|
if (settings.buildReadReceipts) {
|
||||||
hiddenReadReceipts.dispose()
|
hiddenReadReceipts.dispose()
|
||||||
}
|
}
|
||||||
@ -298,7 +288,21 @@ internal class DefaultTimeline(
|
|||||||
return hasMoreInCache(direction) || !hasReachedEnd(direction)
|
return hasMoreInCache(direction) || !hasReachedEnd(direction)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TimelineHiddenReadReceipts.Delegate
|
override fun addListener(listener: Timeline.Listener) = synchronized(listeners) {
|
||||||
|
listeners.add(listener).also {
|
||||||
|
postSnapshot()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeListener(listener: Timeline.Listener) = synchronized(listeners) {
|
||||||
|
listeners.remove(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeAllListeners() = synchronized(listeners) {
|
||||||
|
listeners.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 rebuildEvent(eventId) { te ->
|
||||||
@ -310,19 +314,7 @@ internal class DefaultTimeline(
|
|||||||
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 {
|
private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean {
|
||||||
return builtEventsIdMap[eventId]?.let { builtIndex ->
|
return builtEventsIdMap[eventId]?.let { builtIndex ->
|
||||||
@ -502,9 +494,9 @@ internal class DefaultTimeline(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
val params = PaginationTask.Params(roomId = roomId,
|
val params = PaginationTask.Params(roomId = roomId,
|
||||||
from = token,
|
from = token,
|
||||||
direction = direction.toPaginationDirection(),
|
direction = direction.toPaginationDirection(),
|
||||||
limit = limit)
|
limit = limit)
|
||||||
|
|
||||||
Timber.v("Should fetch $limit items $direction")
|
Timber.v("Should fetch $limit items $direction")
|
||||||
cancelableBag += paginationTask
|
cancelableBag += paginationTask
|
||||||
@ -579,7 +571,7 @@ internal class DefaultTimeline(
|
|||||||
val timelineEvent = buildTimelineEvent(eventEntity)
|
val timelineEvent = buildTimelineEvent(eventEntity)
|
||||||
|
|
||||||
if (timelineEvent.isEncrypted()
|
if (timelineEvent.isEncrypted()
|
||||||
&& timelineEvent.root.mxDecryptionResult == null) {
|
&& timelineEvent.root.mxDecryptionResult == null) {
|
||||||
timelineEvent.root.eventId?.let { eventDecryptor.requestDecryption(it) }
|
timelineEvent.root.eventId?.let { eventDecryptor.requestDecryption(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -641,7 +633,7 @@ internal class DefaultTimeline(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchEvent(eventId: String) {
|
private fun fetchEvent(eventId: String) {
|
||||||
val params = GetContextOfEventTask.Params(roomId, eventId)
|
val params = GetContextOfEventTask.Params(roomId, eventId, settings.initialSize)
|
||||||
cancelableBag += contextOfEventTask.configureWith(params).executeBy(taskExecutor)
|
cancelableBag += contextOfEventTask.configureWith(params).executeBy(taskExecutor)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -652,7 +644,13 @@ internal class DefaultTimeline(
|
|||||||
}
|
}
|
||||||
updateLoadingStates(filteredEvents)
|
updateLoadingStates(filteredEvents)
|
||||||
val snapshot = createSnapshot()
|
val snapshot = createSnapshot()
|
||||||
val runnable = Runnable { listener?.onUpdated(snapshot) }
|
val runnable = Runnable {
|
||||||
|
synchronized(listeners) {
|
||||||
|
listeners.forEach {
|
||||||
|
it.onUpdated(snapshot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
debouncer.debounce("post_snapshot", runnable, 50)
|
debouncer.debounce("post_snapshot", runnable, 50)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,17 +53,16 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
|
|||||||
|
|
||||||
override fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline {
|
override fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline {
|
||||||
return DefaultTimeline(roomId,
|
return DefaultTimeline(roomId,
|
||||||
eventId,
|
eventId,
|
||||||
monarchy.realmConfiguration,
|
monarchy.realmConfiguration,
|
||||||
taskExecutor,
|
taskExecutor,
|
||||||
contextOfEventTask,
|
contextOfEventTask,
|
||||||
clearUnlinkedEventsTask,
|
clearUnlinkedEventsTask,
|
||||||
paginationTask,
|
paginationTask,
|
||||||
cryptoService,
|
cryptoService,
|
||||||
timelineEventMapper,
|
timelineEventMapper,
|
||||||
settings,
|
settings,
|
||||||
TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings),
|
TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings)
|
||||||
TimelineHiddenReadMarker(roomId, settings)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -30,6 +30,7 @@ data class EventContextResponse(
|
|||||||
@Json(name = "state") override val stateEvents: List<Event> = emptyList()
|
@Json(name = "state") override val stateEvents: List<Event> = emptyList()
|
||||||
) : TokenChunkEvent {
|
) : TokenChunkEvent {
|
||||||
|
|
||||||
override val events: List<Event>
|
override val events: List<Event> by lazy {
|
||||||
get() = listOf(event)
|
eventsAfter.reversed() + listOf(event) + eventsBefore
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,133 +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.api.session.room.timeline.TimelineSettings
|
|
||||||
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
|
|
||||||
import im.vector.matrix.android.internal.database.model.ReadMarkerEntityFields
|
|
||||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
|
||||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
|
|
||||||
import im.vector.matrix.android.internal.database.query.FilterContent
|
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
|
||||||
import io.realm.OrderedRealmCollectionChangeListener
|
|
||||||
import io.realm.Realm
|
|
||||||
import io.realm.RealmQuery
|
|
||||||
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,
|
|
||||||
private val settings: TimelineSettings) {
|
|
||||||
|
|
||||||
interface Delegate {
|
|
||||||
fun rebuildEvent(eventId: String, hasReadMarker: Boolean): Boolean
|
|
||||||
fun onReadMarkerUpdated()
|
|
||||||
}
|
|
||||||
|
|
||||||
private var previousDisplayedEventId: String? = null
|
|
||||||
private var hiddenReadMarker: RealmResults<ReadMarkerEntity>? = null
|
|
||||||
|
|
||||||
private lateinit var filteredEvents: RealmResults<TimelineEventEntity>
|
|
||||||
private lateinit var nonFilteredEvents: RealmResults<TimelineEventEntity>
|
|
||||||
private lateinit var delegate: Delegate
|
|
||||||
|
|
||||||
private val readMarkerListener = OrderedRealmCollectionChangeListener<RealmResults<ReadMarkerEntity>> { readMarkers, changeSet ->
|
|
||||||
if (!readMarkers.isLoaded || !readMarkers.isValid) {
|
|
||||||
return@OrderedRealmCollectionChangeListener
|
|
||||||
}
|
|
||||||
var hasChange = false
|
|
||||||
if (changeSet.deletions.isNotEmpty()) {
|
|
||||||
previousDisplayedEventId?.also {
|
|
||||||
hasChange = delegate.rebuildEvent(it, false)
|
|
||||||
previousDisplayedEventId = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val readMarker = readMarkers.firstOrNull() ?: return@OrderedRealmCollectionChangeListener
|
|
||||||
val hiddenEvent = readMarker.timelineEvent?.firstOrNull()
|
|
||||||
?: return@OrderedRealmCollectionChangeListener
|
|
||||||
|
|
||||||
val isLoaded = nonFilteredEvents.where()
|
|
||||||
.equalTo(TimelineEventEntityFields.EVENT_ID, hiddenEvent.eventId)
|
|
||||||
.findFirst() != null
|
|
||||||
|
|
||||||
val displayIndex = hiddenEvent.root?.displayIndex
|
|
||||||
if (isLoaded && displayIndex != null) {
|
|
||||||
// Then we are looking for the first displayable event after the hidden one
|
|
||||||
val firstDisplayedEvent = filteredEvents.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,
|
|
||||||
filteredEvents: RealmResults<TimelineEventEntity>,
|
|
||||||
nonFilteredEvents: RealmResults<TimelineEventEntity>,
|
|
||||||
delegate: Delegate) {
|
|
||||||
this.filteredEvents = filteredEvents
|
|
||||||
this.nonFilteredEvents = nonFilteredEvents
|
|
||||||
this.delegate = delegate
|
|
||||||
// We are looking for read receipts set on hidden events.
|
|
||||||
// We only accept those with a timelineEvent (so coming from pagination/sync).
|
|
||||||
hiddenReadMarker = ReadMarkerEntity.where(realm, roomId = roomId)
|
|
||||||
.isNotEmpty(ReadMarkerEntityFields.TIMELINE_EVENT)
|
|
||||||
.filterReceiptsWithSettings()
|
|
||||||
.findAllAsync()
|
|
||||||
.also { it.addChangeListener(readMarkerListener) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispose the realm query subscription. Has to be called on an HandlerThread
|
|
||||||
*/
|
|
||||||
fun dispose() {
|
|
||||||
this.hiddenReadMarker?.removeAllChangeListeners()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We are looking for readMarker related to filtered events. So, it's the opposite of [DefaultTimeline.filterEventsWithSettings] method.
|
|
||||||
*/
|
|
||||||
private fun RealmQuery<ReadMarkerEntity>.filterReceiptsWithSettings(): RealmQuery<ReadMarkerEntity> {
|
|
||||||
beginGroup()
|
|
||||||
if (settings.filterTypes) {
|
|
||||||
not().`in`("${ReadMarkerEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.TYPE}", settings.allowedTypes.toTypedArray())
|
|
||||||
}
|
|
||||||
if (settings.filterTypes && settings.filterEdits) {
|
|
||||||
or()
|
|
||||||
}
|
|
||||||
if (settings.filterEdits) {
|
|
||||||
like("${ReadMarkerEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", FilterContent.EDIT_TYPE)
|
|
||||||
}
|
|
||||||
endGroup()
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -16,14 +16,10 @@
|
|||||||
|
|
||||||
package im.vector.matrix.android.internal.session.sync
|
package im.vector.matrix.android.internal.session.sync
|
||||||
|
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
|
||||||
import im.vector.matrix.android.internal.session.room.read.FullyReadContent
|
|
||||||
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
|
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
|
||||||
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.TimelineEventEntityFields
|
|
||||||
import im.vector.matrix.android.internal.database.query.getOrCreate
|
import im.vector.matrix.android.internal.database.query.getOrCreate
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.session.room.read.FullyReadContent
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -39,18 +35,8 @@ internal class RoomFullyReadHandler @Inject constructor() {
|
|||||||
RoomSummaryEntity.getOrCreate(realm, roomId).apply {
|
RoomSummaryEntity.getOrCreate(realm, roomId).apply {
|
||||||
readMarkerId = content.eventId
|
readMarkerId = content.eventId
|
||||||
}
|
}
|
||||||
// Remove the old markers if any
|
ReadMarkerEntity.getOrCreate(realm, roomId).apply {
|
||||||
val oldReadMarkerEvents = TimelineEventEntity
|
|
||||||
.where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.BOTH)
|
|
||||||
.isNotNull(TimelineEventEntityFields.READ_MARKER.`$`)
|
|
||||||
.findAll()
|
|
||||||
|
|
||||||
oldReadMarkerEvents.forEach { it.readMarker = null }
|
|
||||||
val readMarkerEntity = ReadMarkerEntity.getOrCreate(realm, roomId).apply {
|
|
||||||
this.eventId = content.eventId
|
this.eventId = content.eventId
|
||||||
}
|
}
|
||||||
// Attach to timelineEvent if known
|
|
||||||
val timelineEventEntities = TimelineEventEntity.where(realm, roomId = roomId, eventId = content.eventId).findAll()
|
|
||||||
timelineEventEntities.forEach { it.readMarker = readMarkerEntity }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,7 +29,7 @@ class SasEmojiController : TypedEpoxyController<SasState>() {
|
|||||||
if (data == null) return
|
if (data == null) return
|
||||||
|
|
||||||
data.emojiList.forEachIndexed { idx, emojiRepresentation ->
|
data.emojiList.forEachIndexed { idx, emojiRepresentation ->
|
||||||
itemSasEmoji {
|
sasEmojiItem {
|
||||||
id(idx)
|
id(idx)
|
||||||
index(idx)
|
index(idx)
|
||||||
emojiRepresentation(emojiRepresentation)
|
emojiRepresentation(emojiRepresentation)
|
||||||
|
|||||||
@ -25,7 +25,7 @@ import im.vector.riotx.core.epoxy.VectorEpoxyHolder
|
|||||||
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||||
|
|
||||||
@EpoxyModelClass(layout = im.vector.riotx.R.layout.item_sas_emoji)
|
@EpoxyModelClass(layout = im.vector.riotx.R.layout.item_sas_emoji)
|
||||||
abstract class ItemSasEmoji : VectorEpoxyModel<ItemSasEmoji.Holder>() {
|
abstract class SasEmojiItem : VectorEpoxyModel<SasEmojiItem.Holder>() {
|
||||||
|
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
var index: Int = 0
|
var index: Int = 0
|
||||||
@ -37,7 +37,7 @@ import im.vector.riotx.features.themes.ThemeUtils
|
|||||||
* A action for bottom sheet.
|
* A action for bottom sheet.
|
||||||
*/
|
*/
|
||||||
@EpoxyModelClass(layout = R.layout.item_bottom_sheet_action)
|
@EpoxyModelClass(layout = R.layout.item_bottom_sheet_action)
|
||||||
abstract class BottomSheetItemAction : VectorEpoxyModel<BottomSheetItemAction.Holder>() {
|
abstract class BottomSheetActionItem : VectorEpoxyModel<BottomSheetActionItem.Holder>() {
|
||||||
|
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
@DrawableRes
|
@DrawableRes
|
||||||
@ -16,6 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
package im.vector.riotx.core.epoxy.bottomsheet
|
package im.vector.riotx.core.epoxy.bottomsheet
|
||||||
|
|
||||||
|
import android.text.method.MovementMethod
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
@ -25,12 +26,13 @@ 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.extensions.setTextOrHide
|
import im.vector.riotx.core.extensions.setTextOrHide
|
||||||
import im.vector.riotx.features.home.AvatarRenderer
|
import im.vector.riotx.features.home.AvatarRenderer
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.tools.findPillsAndProcess
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A message preview for bottom sheet.
|
* A message preview for bottom sheet.
|
||||||
*/
|
*/
|
||||||
@EpoxyModelClass(layout = R.layout.item_bottom_sheet_message_preview)
|
@EpoxyModelClass(layout = R.layout.item_bottom_sheet_message_preview)
|
||||||
abstract class BottomSheetItemMessagePreview : VectorEpoxyModel<BottomSheetItemMessagePreview.Holder>() {
|
abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel<BottomSheetMessagePreviewItem.Holder>() {
|
||||||
|
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
lateinit var avatarRenderer: AvatarRenderer
|
lateinit var avatarRenderer: AvatarRenderer
|
||||||
@ -44,11 +46,15 @@ abstract class BottomSheetItemMessagePreview : VectorEpoxyModel<BottomSheetItemM
|
|||||||
lateinit var body: CharSequence
|
lateinit var body: CharSequence
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
var time: CharSequence? = null
|
var time: CharSequence? = null
|
||||||
|
@EpoxyAttribute
|
||||||
|
var movementMethod: MovementMethod? = null
|
||||||
|
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
avatarRenderer.render(avatarUrl, senderId, senderName, holder.avatar)
|
avatarRenderer.render(avatarUrl, senderId, senderName, holder.avatar)
|
||||||
holder.sender.setTextOrHide(senderName)
|
holder.sender.setTextOrHide(senderName)
|
||||||
|
holder.body.movementMethod = movementMethod
|
||||||
holder.body.text = body
|
holder.body.text = body
|
||||||
|
body.findPillsAndProcess { it.bind(holder.body) }
|
||||||
holder.timestamp.setTextOrHide(time)
|
holder.timestamp.setTextOrHide(time)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,7 +29,7 @@ import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
|||||||
* A quick reaction list for bottom sheet.
|
* A quick reaction list for bottom sheet.
|
||||||
*/
|
*/
|
||||||
@EpoxyModelClass(layout = R.layout.item_bottom_sheet_quick_reaction)
|
@EpoxyModelClass(layout = R.layout.item_bottom_sheet_quick_reaction)
|
||||||
abstract class BottomSheetItemQuickReactions : VectorEpoxyModel<BottomSheetItemQuickReactions.Holder>() {
|
abstract class BottomSheetQuickReactionsItem : VectorEpoxyModel<BottomSheetQuickReactionsItem.Holder>() {
|
||||||
|
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
lateinit var fontProvider: EmojiCompatFontProvider
|
lateinit var fontProvider: EmojiCompatFontProvider
|
||||||
@ -31,7 +31,7 @@ import im.vector.riotx.features.home.AvatarRenderer
|
|||||||
* A room preview for bottom sheet.
|
* A room preview for bottom sheet.
|
||||||
*/
|
*/
|
||||||
@EpoxyModelClass(layout = R.layout.item_bottom_sheet_room_preview)
|
@EpoxyModelClass(layout = R.layout.item_bottom_sheet_room_preview)
|
||||||
abstract class BottomSheetItemRoomPreview : VectorEpoxyModel<BottomSheetItemRoomPreview.Holder>() {
|
abstract class BottomSheetRoomPreviewItem : VectorEpoxyModel<BottomSheetRoomPreviewItem.Holder>() {
|
||||||
|
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
lateinit var avatarRenderer: AvatarRenderer
|
lateinit var avatarRenderer: AvatarRenderer
|
||||||
@ -30,7 +30,7 @@ import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
|||||||
* A send state for bottom sheet.
|
* A send state for bottom sheet.
|
||||||
*/
|
*/
|
||||||
@EpoxyModelClass(layout = R.layout.item_bottom_sheet_message_status)
|
@EpoxyModelClass(layout = R.layout.item_bottom_sheet_message_status)
|
||||||
abstract class BottomSheetItemSendState : VectorEpoxyModel<BottomSheetItemSendState.Holder>() {
|
abstract class BottomSheetSendStateItem : VectorEpoxyModel<BottomSheetSendStateItem.Holder>() {
|
||||||
|
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
var showProgress: Boolean = false
|
var showProgress: Boolean = false
|
||||||
@ -22,7 +22,7 @@ import im.vector.riotx.core.epoxy.VectorEpoxyHolder
|
|||||||
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||||
|
|
||||||
@EpoxyModelClass(layout = R.layout.item_bottom_sheet_divider)
|
@EpoxyModelClass(layout = R.layout.item_bottom_sheet_divider)
|
||||||
abstract class BottomSheetItemSeparator : VectorEpoxyModel<BottomSheetItemSeparator.Holder>() {
|
abstract class BottomSheetSeparatorItem : VectorEpoxyModel<BottomSheetSeparatorItem.Holder>() {
|
||||||
|
|
||||||
class Holder : VectorEpoxyHolder()
|
class Holder : VectorEpoxyHolder()
|
||||||
}
|
}
|
||||||
@ -24,7 +24,3 @@ fun TimelineEvent.canReact(): Boolean {
|
|||||||
// 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
|
||||||
return root.getClearType() == EventType.MESSAGE && root.sendState == SendState.SYNCED && !root.isRedacted()
|
return root.getClearType() == EventType.MESSAGE && root.sendState == SendState.SYNCED && !root.isRedacted()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun TimelineEvent.displayReadMarker(myUserId: String): Boolean {
|
|
||||||
return hasReadMarker && readReceipts.find { it.user.userId == myUserId } == null
|
|
||||||
}
|
|
||||||
|
|||||||
@ -23,7 +23,6 @@ import android.util.AttributeSet
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.isInvisible
|
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import kotlinx.android.synthetic.main.view_jump_to_read_marker.view.*
|
import kotlinx.android.synthetic.main.view_jump_to_read_marker.view.*
|
||||||
|
|
||||||
@ -34,7 +33,7 @@ class JumpToReadMarkerView @JvmOverloads constructor(
|
|||||||
) : RelativeLayout(context, attrs, defStyleAttr) {
|
) : RelativeLayout(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
fun onJumpToReadMarkerClicked(readMarkerId: String)
|
fun onJumpToReadMarkerClicked()
|
||||||
fun onClearReadMarkerClicked()
|
fun onClearReadMarkerClicked()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,24 +43,15 @@ class JumpToReadMarkerView @JvmOverloads constructor(
|
|||||||
setupView()
|
setupView()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var readMarkerId: String? = null
|
|
||||||
|
|
||||||
private fun setupView() {
|
private fun setupView() {
|
||||||
inflate(context, R.layout.view_jump_to_read_marker, this)
|
inflate(context, R.layout.view_jump_to_read_marker, this)
|
||||||
setBackgroundColor(ContextCompat.getColor(context, R.color.notification_accent_color))
|
setBackgroundColor(ContextCompat.getColor(context, R.color.notification_accent_color))
|
||||||
jumpToReadMarkerLabelView.setOnClickListener {
|
jumpToReadMarkerLabelView.setOnClickListener {
|
||||||
readMarkerId?.also {
|
callback?.onJumpToReadMarkerClicked()
|
||||||
callback?.onJumpToReadMarkerClicked(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
closeJumpToReadMarkerView.setOnClickListener {
|
closeJumpToReadMarkerView.setOnClickListener {
|
||||||
visibility = View.INVISIBLE
|
visibility = View.INVISIBLE
|
||||||
callback?.onClearReadMarkerClicked()
|
callback?.onClearReadMarkerClicked()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun render(show: Boolean, readMarkerId: String?) {
|
|
||||||
this.readMarkerId = readMarkerId
|
|
||||||
isInvisible = !show
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,89 +0,0 @@
|
|||||||
/*
|
|
||||||
|
|
||||||
* Copyright 2019 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
package im.vector.riotx.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 kotlinx.coroutines.*
|
|
||||||
|
|
||||||
private const val DELAY_IN_MS = 1_000L
|
|
||||||
|
|
||||||
class ReadMarkerView @JvmOverloads constructor(
|
|
||||||
context: Context,
|
|
||||||
attrs: AttributeSet? = null,
|
|
||||||
defStyleAttr: Int = 0
|
|
||||||
) : View(context, attrs, defStyleAttr) {
|
|
||||||
|
|
||||||
interface Callback {
|
|
||||||
fun onReadMarkerLongBound(isDisplayed: Boolean)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var eventId: String? = null
|
|
||||||
private var callback: Callback? = null
|
|
||||||
private var callbackDispatcherJob: Job? = null
|
|
||||||
|
|
||||||
fun bindView(eventId: String?, hasReadMarker: Boolean, displayReadMarker: Boolean, readMarkerCallback: Callback) {
|
|
||||||
this.eventId = eventId
|
|
||||||
this.callback = readMarkerCallback
|
|
||||||
if (displayReadMarker) {
|
|
||||||
startAnimation()
|
|
||||||
} else {
|
|
||||||
this.animation?.cancel()
|
|
||||||
this.visibility = INVISIBLE
|
|
||||||
}
|
|
||||||
if (hasReadMarker) {
|
|
||||||
callbackDispatcherJob = GlobalScope.launch(Dispatchers.Main) {
|
|
||||||
delay(DELAY_IN_MS)
|
|
||||||
callback?.onReadMarkerLongBound(displayReadMarker)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun unbind() {
|
|
||||||
this.callbackDispatcherJob?.cancel()
|
|
||||||
this.callback = null
|
|
||||||
this.eventId = 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) {}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
visibility = VISIBLE
|
|
||||||
animation.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,99 +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 androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import im.vector.riotx.core.di.ScreenScope
|
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@ScreenScope
|
|
||||||
class ReadMarkerHelper @Inject constructor() {
|
|
||||||
|
|
||||||
lateinit var timelineEventController: TimelineEventController
|
|
||||||
lateinit var layoutManager: LinearLayoutManager
|
|
||||||
var callback: Callback? = null
|
|
||||||
|
|
||||||
private var onReadMarkerLongDisplayed = false
|
|
||||||
private var jumpToReadMarkerVisible = false
|
|
||||||
private var readMarkerVisible: Boolean = true
|
|
||||||
private var state: RoomDetailViewState? = null
|
|
||||||
|
|
||||||
fun readMarkerVisible(): Boolean {
|
|
||||||
return readMarkerVisible
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onResume() {
|
|
||||||
onReadMarkerLongDisplayed = false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onReadMarkerLongDisplayed() {
|
|
||||||
onReadMarkerLongDisplayed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateWith(newState: RoomDetailViewState) {
|
|
||||||
state = newState
|
|
||||||
checkReadMarkerVisibility()
|
|
||||||
checkJumpToReadMarkerVisibility()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onTimelineScrolled() {
|
|
||||||
checkJumpToReadMarkerVisibility()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkReadMarkerVisibility() {
|
|
||||||
val nonNullState = this.state ?: return
|
|
||||||
val firstVisibleItem = layoutManager.findFirstVisibleItemPosition()
|
|
||||||
val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
|
|
||||||
readMarkerVisible = if (!onReadMarkerLongDisplayed) {
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
if (nonNullState.timeline?.isLive == false) {
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
!(firstVisibleItem == 0 && lastVisibleItem > 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkJumpToReadMarkerVisibility() {
|
|
||||||
val nonNullState = this.state ?: return
|
|
||||||
val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
|
|
||||||
val readMarkerId = nonNullState.asyncRoomSummary()?.readMarkerId
|
|
||||||
val newJumpToReadMarkerVisible = if (readMarkerId == null) {
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
val correctedReadMarkerId = nonNullState.timeline?.getFirstDisplayableEventId(readMarkerId)
|
|
||||||
?: readMarkerId
|
|
||||||
val positionOfReadMarker = timelineEventController.searchPositionOfEvent(correctedReadMarkerId)
|
|
||||||
if (positionOfReadMarker == null) {
|
|
||||||
nonNullState.timeline?.isLive == true && lastVisibleItem > 0
|
|
||||||
} else {
|
|
||||||
positionOfReadMarker > lastVisibleItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (newJumpToReadMarkerVisible != jumpToReadMarkerVisible) {
|
|
||||||
jumpToReadMarkerVisible = newJumpToReadMarkerVisible
|
|
||||||
callback?.onJumpToReadMarkerVisibilityUpdate(jumpToReadMarkerVisible, readMarkerId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Callback {
|
|
||||||
fun onJumpToReadMarkerVisibilityUpdate(show: Boolean, readMarkerId: String?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -35,13 +35,15 @@ sealed class RoomDetailAction : VectorViewModelAction {
|
|||||||
data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailAction()
|
data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailAction()
|
||||||
data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailAction()
|
data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailAction()
|
||||||
data class NavigateToEvent(val eventId: String, val highlight: Boolean) : RoomDetailAction()
|
data class NavigateToEvent(val eventId: String, val highlight: Boolean) : RoomDetailAction()
|
||||||
data class SetReadMarkerAction(val eventId: String) : RoomDetailAction()
|
|
||||||
object MarkAllAsRead : RoomDetailAction()
|
object MarkAllAsRead : RoomDetailAction()
|
||||||
data class DownloadFile(val eventId: String, val messageFileContent: MessageFileContent) : RoomDetailAction()
|
data class DownloadFile(val eventId: String, val messageFileContent: MessageFileContent) : RoomDetailAction()
|
||||||
data class HandleTombstoneEvent(val event: Event) : RoomDetailAction()
|
data class HandleTombstoneEvent(val event: Event) : RoomDetailAction()
|
||||||
object AcceptInvite : RoomDetailAction()
|
object AcceptInvite : RoomDetailAction()
|
||||||
object RejectInvite : RoomDetailAction()
|
object RejectInvite : RoomDetailAction()
|
||||||
|
|
||||||
|
object EnterTrackingUnreadMessagesState : RoomDetailAction()
|
||||||
|
object ExitTrackingUnreadMessagesState : RoomDetailAction()
|
||||||
|
|
||||||
data class EnterEditMode(val eventId: String, val text: String) : RoomDetailAction()
|
data class EnterEditMode(val eventId: String, val text: String) : RoomDetailAction()
|
||||||
data class EnterQuoteMode(val eventId: String, val text: String) : RoomDetailAction()
|
data class EnterQuoteMode(val eventId: String, val text: String) : RoomDetailAction()
|
||||||
data class EnterReplyMode(val eventId: String, val text: String) : RoomDetailAction()
|
data class EnterReplyMode(val eventId: String, val text: String) : RoomDetailAction()
|
||||||
|
|||||||
@ -39,6 +39,7 @@ import androidx.core.text.buildSpannedString
|
|||||||
import androidx.core.util.Pair
|
import androidx.core.util.Pair
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.forEach
|
import androidx.core.view.forEach
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
@ -57,7 +58,6 @@ 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.content.ContentAttachmentData
|
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||||
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.events.model.LocalEcho
|
|
||||||
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.*
|
import im.vector.matrix.android.api.session.room.model.message.*
|
||||||
import im.vector.matrix.android.api.session.room.send.SendState
|
import im.vector.matrix.android.api.session.room.send.SendState
|
||||||
@ -145,8 +145,7 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
val textComposerViewModelFactory: TextComposerViewModel.Factory,
|
val textComposerViewModelFactory: TextComposerViewModel.Factory,
|
||||||
private val errorFormatter: ErrorFormatter,
|
private val errorFormatter: ErrorFormatter,
|
||||||
private val eventHtmlRenderer: EventHtmlRenderer,
|
private val eventHtmlRenderer: EventHtmlRenderer,
|
||||||
private val vectorPreferences: VectorPreferences,
|
private val vectorPreferences: VectorPreferences
|
||||||
private val readMarkerHelper: ReadMarkerHelper
|
|
||||||
) :
|
) :
|
||||||
VectorBaseFragment(),
|
VectorBaseFragment(),
|
||||||
TimelineEventController.Callback,
|
TimelineEventController.Callback,
|
||||||
@ -292,6 +291,7 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState)
|
||||||
debouncer.cancelAll()
|
debouncer.cancelAll()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
@ -299,6 +299,7 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
private fun setupJumpToBottomView() {
|
private fun setupJumpToBottomView() {
|
||||||
jumpToBottomView.visibility = View.INVISIBLE
|
jumpToBottomView.visibility = View.INVISIBLE
|
||||||
jumpToBottomView.setOnClickListener {
|
jumpToBottomView.setOnClickListener {
|
||||||
|
roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState)
|
||||||
jumpToBottomView.visibility = View.INVISIBLE
|
jumpToBottomView.visibility = View.INVISIBLE
|
||||||
withState(roomDetailViewModel) { state ->
|
withState(roomDetailViewModel) { state ->
|
||||||
if (state.timeline?.isLive == false) {
|
if (state.timeline?.isLive == false) {
|
||||||
@ -423,12 +424,12 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
if (text != composerLayout.composerEditText.text.toString()) {
|
if (text != composerLayout.composerEditText.text.toString()) {
|
||||||
// Ignore update to avoid saving a draft
|
// Ignore update to avoid saving a draft
|
||||||
composerLayout.composerEditText.setText(text)
|
composerLayout.composerEditText.setText(text)
|
||||||
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length ?: 0)
|
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length
|
||||||
|
?: 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
readMarkerHelper.onResume()
|
|
||||||
super.onResume()
|
super.onResume()
|
||||||
notificationDrawerManager.setCurrentRoom(roomDetailArgs.roomId)
|
notificationDrawerManager.setCurrentRoom(roomDetailArgs.roomId)
|
||||||
}
|
}
|
||||||
@ -473,24 +474,12 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
it.dispatchTo(stateRestorer)
|
it.dispatchTo(stateRestorer)
|
||||||
it.dispatchTo(scrollOnNewMessageCallback)
|
it.dispatchTo(scrollOnNewMessageCallback)
|
||||||
it.dispatchTo(scrollOnHighlightedEventCallback)
|
it.dispatchTo(scrollOnHighlightedEventCallback)
|
||||||
}
|
updateJumpToReadMarkerViewVisibility()
|
||||||
readMarkerHelper.timelineEventController = timelineEventController
|
updateJumpToBottomViewVisibility()
|
||||||
readMarkerHelper.layoutManager = layoutManager
|
|
||||||
readMarkerHelper.callback = object : ReadMarkerHelper.Callback {
|
|
||||||
override fun onJumpToReadMarkerVisibilityUpdate(show: Boolean, readMarkerId: String?) {
|
|
||||||
jumpToReadMarkerView.render(show, readMarkerId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
recyclerView.adapter = timelineEventController.adapter
|
recyclerView.adapter = timelineEventController.adapter
|
||||||
|
|
||||||
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
|
||||||
if (recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE) {
|
|
||||||
updateJumpToBottomViewVisibility()
|
|
||||||
}
|
|
||||||
readMarkerHelper.onTimelineScrolled()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||||
when (newState) {
|
when (newState) {
|
||||||
RecyclerView.SCROLL_STATE_IDLE -> {
|
RecyclerView.SCROLL_STATE_IDLE -> {
|
||||||
@ -532,6 +521,30 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateJumpToReadMarkerViewVisibility() = jumpToReadMarkerView.post {
|
||||||
|
withState(roomDetailViewModel) {
|
||||||
|
val showJumpToUnreadBanner = when (it.unreadState) {
|
||||||
|
UnreadState.Unknown,
|
||||||
|
UnreadState.HasNoUnread -> false
|
||||||
|
is UnreadState.ReadMarkerNotLoaded -> true
|
||||||
|
is UnreadState.HasUnread -> {
|
||||||
|
if (it.canShowJumpToReadMarker) {
|
||||||
|
val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
|
||||||
|
val positionOfReadMarker = timelineEventController.getPositionOfReadMarker()
|
||||||
|
if (positionOfReadMarker == null) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
positionOfReadMarker > lastVisibleItem
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jumpToReadMarkerView.isVisible = showJumpToUnreadBanner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateJumpToBottomViewVisibility() {
|
private fun updateJumpToBottomViewVisibility() {
|
||||||
debouncer.debounce("jump_to_bottom_visibility", 250, Runnable {
|
debouncer.debounce("jump_to_bottom_visibility", 250, Runnable {
|
||||||
Timber.v("First visible: ${layoutManager.findFirstCompletelyVisibleItemPosition()}")
|
Timber.v("First visible: ${layoutManager.findFirstCompletelyVisibleItemPosition()}")
|
||||||
@ -662,13 +675,12 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun renderState(state: RoomDetailViewState) {
|
private fun renderState(state: RoomDetailViewState) {
|
||||||
readMarkerHelper.updateWith(state)
|
|
||||||
renderRoomSummary(state)
|
renderRoomSummary(state)
|
||||||
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) {
|
||||||
scrollOnHighlightedEventCallback.timeline = state.timeline
|
scrollOnHighlightedEventCallback.timeline = state.timeline
|
||||||
timelineEventController.update(state, readMarkerHelper.readMarkerVisible())
|
timelineEventController.update(state)
|
||||||
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)
|
||||||
@ -1024,28 +1036,9 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
.show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS")
|
.show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReadMarkerLongBound(readMarkerId: String, isDisplayed: Boolean) {
|
override fun onReadMarkerVisible() {
|
||||||
readMarkerHelper.onReadMarkerLongDisplayed()
|
updateJumpToReadMarkerViewVisibility()
|
||||||
val readMarkerIndex = timelineEventController.searchPositionOfEvent(readMarkerId) ?: return
|
roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState)
|
||||||
val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition()
|
|
||||||
if (readMarkerIndex > lastVisibleItemPosition) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
|
|
||||||
var nextReadMarkerId: String? = null
|
|
||||||
for (itemPosition in firstVisibleItemPosition until lastVisibleItemPosition) {
|
|
||||||
val timelineItem = timelineEventController.adapter.getModelAtPosition(itemPosition)
|
|
||||||
if (timelineItem is BaseEventItem) {
|
|
||||||
val eventId = timelineItem.getEventIds().firstOrNull() ?: continue
|
|
||||||
if (!LocalEcho.isLocalEchoId(eventId)) {
|
|
||||||
nextReadMarkerId = eventId
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (nextReadMarkerId != null) {
|
|
||||||
roomDetailViewModel.handle(RoomDetailAction.SetReadMarkerAction(nextReadMarkerId))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AutocompleteUserPresenter.Callback
|
// AutocompleteUserPresenter.Callback
|
||||||
@ -1163,6 +1156,12 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
is EventSharedAction.IgnoreUser -> {
|
is EventSharedAction.IgnoreUser -> {
|
||||||
roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(action.senderId))
|
roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(action.senderId))
|
||||||
}
|
}
|
||||||
|
is EventSharedAction.OnUrlClicked -> {
|
||||||
|
onUrlClicked(action.url)
|
||||||
|
}
|
||||||
|
is EventSharedAction.OnUrlLongClicked -> {
|
||||||
|
onUrlLongClicked(action.url)
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
Toast.makeText(context, "Action $action is not implemented yet", Toast.LENGTH_LONG).show()
|
Toast.makeText(context, "Action $action is not implemented yet", Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
@ -1246,8 +1245,14 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
|
|
||||||
// JumpToReadMarkerView.Callback
|
// JumpToReadMarkerView.Callback
|
||||||
|
|
||||||
override fun onJumpToReadMarkerClicked(readMarkerId: String) {
|
override fun onJumpToReadMarkerClicked() = withState(roomDetailViewModel) {
|
||||||
roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(readMarkerId, false))
|
jumpToReadMarkerView.isVisible = false
|
||||||
|
if (it.unreadState is UnreadState.HasUnread) {
|
||||||
|
roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(it.unreadState.firstUnreadEventId, false))
|
||||||
|
}
|
||||||
|
if (it.unreadState is UnreadState.ReadMarkerNotLoaded) {
|
||||||
|
roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(it.unreadState.readMarkerId, false))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClearReadMarkerClicked() {
|
override fun onClearReadMarkerClicked() {
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import androidx.lifecycle.LiveData
|
|||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.airbnb.mvrx.*
|
import com.airbnb.mvrx.*
|
||||||
import com.jakewharton.rxrelay2.BehaviorRelay
|
import com.jakewharton.rxrelay2.BehaviorRelay
|
||||||
|
import com.jakewharton.rxrelay2.PublishRelay
|
||||||
import com.squareup.inject.assisted.Assisted
|
import com.squareup.inject.assisted.Assisted
|
||||||
import com.squareup.inject.assisted.AssistedInject
|
import com.squareup.inject.assisted.AssistedInject
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
@ -35,11 +36,14 @@ import im.vector.matrix.android.api.session.file.FileService
|
|||||||
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities
|
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities
|
||||||
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.RoomMember
|
import im.vector.matrix.android.api.session.room.model.RoomMember
|
||||||
|
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
|
||||||
import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent
|
import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent
|
||||||
import im.vector.matrix.android.api.session.room.send.UserDraft
|
import im.vector.matrix.android.api.session.room.send.UserDraft
|
||||||
|
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||||
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
|
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
|
||||||
import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent
|
import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent
|
||||||
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
|
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
|
||||||
@ -58,19 +62,23 @@ 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.BiFunction
|
||||||
import io.reactivex.rxkotlin.subscribeBy
|
import io.reactivex.rxkotlin.subscribeBy
|
||||||
|
import io.reactivex.schedulers.Schedulers
|
||||||
import org.commonmark.parser.Parser
|
import org.commonmark.parser.Parser
|
||||||
import org.commonmark.renderer.html.HtmlRenderer
|
import org.commonmark.renderer.html.HtmlRenderer
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: RoomDetailViewState,
|
class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: RoomDetailViewState,
|
||||||
userPreferencesProvider: UserPreferencesProvider,
|
userPreferencesProvider: UserPreferencesProvider,
|
||||||
private val vectorPreferences: VectorPreferences,
|
private val vectorPreferences: VectorPreferences,
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
private val session: Session
|
private val session: Session
|
||||||
) : VectorViewModel<RoomDetailViewState, RoomDetailAction>(initialState) {
|
) : VectorViewModel<RoomDetailViewState, RoomDetailAction>(initialState), Timeline.Listener {
|
||||||
|
|
||||||
private val room = session.getRoom(initialState.roomId)!!
|
private val room = session.getRoom(initialState.roomId)!!
|
||||||
private val eventId = initialState.eventId
|
private val eventId = initialState.eventId
|
||||||
@ -90,6 +98,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||||||
buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
|
buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var timelineEvents = PublishRelay.create<List<TimelineEvent>>()
|
||||||
private var timeline = room.createTimeline(eventId, timelineSettings)
|
private var timeline = room.createTimeline(eventId, timelineSettings)
|
||||||
|
|
||||||
// Can be used for several actions, for a one shot result
|
// Can be used for several actions, for a one shot result
|
||||||
@ -102,6 +111,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||||||
// Slot to keep a pending uri during permission request
|
// Slot to keep a pending uri during permission request
|
||||||
var pendingUri: Uri? = null
|
var pendingUri: Uri? = null
|
||||||
|
|
||||||
|
private var trackUnreadMessages = AtomicBoolean(false)
|
||||||
|
private var mostRecentDisplayedEvent: TimelineEvent? = null
|
||||||
|
|
||||||
@AssistedInject.Factory
|
@AssistedInject.Factory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(initialState: RoomDetailViewState): RoomDetailViewModel
|
fun create(initialState: RoomDetailViewState): RoomDetailViewModel
|
||||||
@ -120,48 +132,67 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
getUnreadState()
|
||||||
observeSyncState()
|
observeSyncState()
|
||||||
observeRoomSummary()
|
observeRoomSummary()
|
||||||
observeEventDisplayedActions()
|
observeEventDisplayedActions()
|
||||||
observeSummaryState()
|
observeSummaryState()
|
||||||
observeDrafts()
|
observeDrafts()
|
||||||
|
observeUnreadState()
|
||||||
room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear()
|
room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear()
|
||||||
|
timeline.addListener(this)
|
||||||
timeline.start()
|
timeline.start()
|
||||||
setState { copy(timeline = this@RoomDetailViewModel.timeline) }
|
setState { copy(timeline = this@RoomDetailViewModel.timeline) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handle(action: RoomDetailAction) {
|
override fun handle(action: RoomDetailAction) {
|
||||||
when (action) {
|
when (action) {
|
||||||
is RoomDetailAction.SaveDraft -> handleSaveDraft(action)
|
is RoomDetailAction.SaveDraft -> handleSaveDraft(action)
|
||||||
is RoomDetailAction.SendMessage -> handleSendMessage(action)
|
is RoomDetailAction.SendMessage -> handleSendMessage(action)
|
||||||
is RoomDetailAction.SendMedia -> handleSendMedia(action)
|
is RoomDetailAction.SendMedia -> handleSendMedia(action)
|
||||||
is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action)
|
is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action)
|
||||||
is RoomDetailAction.TimelineEventTurnsInvisible -> handleEventInvisible(action)
|
is RoomDetailAction.TimelineEventTurnsInvisible -> handleEventInvisible(action)
|
||||||
is RoomDetailAction.LoadMoreTimelineEvents -> handleLoadMore(action)
|
is RoomDetailAction.LoadMoreTimelineEvents -> handleLoadMore(action)
|
||||||
is RoomDetailAction.SendReaction -> handleSendReaction(action)
|
is RoomDetailAction.SendReaction -> handleSendReaction(action)
|
||||||
is RoomDetailAction.AcceptInvite -> handleAcceptInvite()
|
is RoomDetailAction.AcceptInvite -> handleAcceptInvite()
|
||||||
is RoomDetailAction.RejectInvite -> handleRejectInvite()
|
is RoomDetailAction.RejectInvite -> handleRejectInvite()
|
||||||
is RoomDetailAction.RedactAction -> handleRedactEvent(action)
|
is RoomDetailAction.RedactAction -> handleRedactEvent(action)
|
||||||
is RoomDetailAction.UndoReaction -> handleUndoReact(action)
|
is RoomDetailAction.UndoReaction -> handleUndoReact(action)
|
||||||
is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
|
is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
|
||||||
is RoomDetailAction.ExitSpecialMode -> handleExitSpecialMode(action)
|
is RoomDetailAction.ExitSpecialMode -> handleExitSpecialMode(action)
|
||||||
is RoomDetailAction.EnterEditMode -> handleEditAction(action)
|
is RoomDetailAction.EnterEditMode -> handleEditAction(action)
|
||||||
is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action)
|
is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action)
|
||||||
is RoomDetailAction.EnterReplyMode -> handleReplyAction(action)
|
is RoomDetailAction.EnterReplyMode -> handleReplyAction(action)
|
||||||
is RoomDetailAction.DownloadFile -> handleDownloadFile(action)
|
is RoomDetailAction.DownloadFile -> handleDownloadFile(action)
|
||||||
is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action)
|
is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action)
|
||||||
is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action)
|
is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action)
|
||||||
is RoomDetailAction.ResendMessage -> handleResendEvent(action)
|
is RoomDetailAction.ResendMessage -> handleResendEvent(action)
|
||||||
is RoomDetailAction.RemoveFailedEcho -> handleRemove(action)
|
is RoomDetailAction.RemoveFailedEcho -> handleRemove(action)
|
||||||
is RoomDetailAction.ClearSendQueue -> handleClearSendQueue()
|
is RoomDetailAction.ClearSendQueue -> handleClearSendQueue()
|
||||||
is RoomDetailAction.ResendAll -> handleResendAll()
|
is RoomDetailAction.ResendAll -> handleResendAll()
|
||||||
is RoomDetailAction.SetReadMarkerAction -> handleSetReadMarkerAction(action)
|
is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead()
|
||||||
is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead()
|
is RoomDetailAction.ReportContent -> handleReportContent(action)
|
||||||
is RoomDetailAction.ReportContent -> handleReportContent(action)
|
is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action)
|
||||||
is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action)
|
is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages()
|
||||||
|
is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun startTrackingUnreadMessages() {
|
||||||
|
trackUnreadMessages.set(true)
|
||||||
|
setState { copy(canShowJumpToReadMarker = false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopTrackingUnreadMessages() {
|
||||||
|
if (trackUnreadMessages.getAndSet(false)) {
|
||||||
|
mostRecentDisplayedEvent?.root?.eventId?.also {
|
||||||
|
room.setReadMarker(it, callback = object : MatrixCallback<Unit> {})
|
||||||
|
}
|
||||||
|
mostRecentDisplayedEvent = null
|
||||||
|
}
|
||||||
|
setState { copy(canShowJumpToReadMarker = true) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleEventInvisible(action: RoomDetailAction.TimelineEventTurnsInvisible) {
|
private fun handleEventInvisible(action: RoomDetailAction.TimelineEventTurnsInvisible) {
|
||||||
invisibleEventsObservable.accept(action)
|
invisibleEventsObservable.accept(action)
|
||||||
}
|
}
|
||||||
@ -627,6 +658,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleNavigateToEvent(action: RoomDetailAction.NavigateToEvent) {
|
private fun handleNavigateToEvent(action: RoomDetailAction.NavigateToEvent) {
|
||||||
|
stopTrackingUnreadMessages()
|
||||||
val targetEventId: String = action.eventId
|
val targetEventId: String = action.eventId
|
||||||
val correctedEventId = timeline.getFirstDisplayableEventId(targetEventId) ?: targetEventId
|
val correctedEventId = timeline.getFirstDisplayableEventId(targetEventId) ?: targetEventId
|
||||||
val indexOfEvent = timeline.getIndexOfEvent(correctedEventId)
|
val indexOfEvent = timeline.getIndexOfEvent(correctedEventId)
|
||||||
@ -685,26 +717,23 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||||||
.buffer(1, TimeUnit.SECONDS)
|
.buffer(1, TimeUnit.SECONDS)
|
||||||
.filter { it.isNotEmpty() }
|
.filter { it.isNotEmpty() }
|
||||||
.subscribeBy(onNext = { actions ->
|
.subscribeBy(onNext = { actions ->
|
||||||
val mostRecentEvent = actions.maxBy { it.event.displayIndex }
|
val bufferedMostRecentDisplayedEvent = actions.maxBy { it.event.displayIndex }?.event
|
||||||
mostRecentEvent?.event?.root?.eventId?.let { eventId ->
|
?: return@subscribeBy
|
||||||
|
val globalMostRecentDisplayedEvent = mostRecentDisplayedEvent
|
||||||
|
if (trackUnreadMessages.get()) {
|
||||||
|
if (globalMostRecentDisplayedEvent == null) {
|
||||||
|
mostRecentDisplayedEvent = bufferedMostRecentDisplayedEvent
|
||||||
|
} else if (bufferedMostRecentDisplayedEvent.displayIndex > globalMostRecentDisplayedEvent.displayIndex) {
|
||||||
|
mostRecentDisplayedEvent = bufferedMostRecentDisplayedEvent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bufferedMostRecentDisplayedEvent.root.eventId?.let { eventId ->
|
||||||
room.setReadReceipt(eventId, callback = object : MatrixCallback<Unit> {})
|
room.setReadReceipt(eventId, callback = object : MatrixCallback<Unit> {})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.disposeOnClear()
|
.disposeOnClear()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleSetReadMarkerAction(action: RoomDetailAction.SetReadMarkerAction) = withState {
|
|
||||||
var readMarkerId = action.eventId
|
|
||||||
val indexOfEvent = timeline.getIndexOfEvent(readMarkerId)
|
|
||||||
// force to set the read marker on the next event
|
|
||||||
if (indexOfEvent != null) {
|
|
||||||
timeline.getTimelineEventAtIndex(indexOfEvent - 1)?.root?.eventId?.also { eventIdOfNext ->
|
|
||||||
readMarkerId = eventIdOfNext
|
|
||||||
}
|
|
||||||
}
|
|
||||||
room.setReadMarker(readMarkerId, callback = object : MatrixCallback<Unit> {})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleMarkAllAsRead() {
|
private fun handleMarkAllAsRead() {
|
||||||
room.markAllAsRead(object : MatrixCallback<Any> {})
|
room.markAllAsRead(object : MatrixCallback<Any> {})
|
||||||
}
|
}
|
||||||
@ -759,6 +788,56 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getUnreadState() {
|
||||||
|
Observable
|
||||||
|
.combineLatest<List<TimelineEvent>, RoomSummary, UnreadState>(
|
||||||
|
timelineEvents.observeOn(Schedulers.computation()),
|
||||||
|
room.rx().liveRoomSummary().unwrap(),
|
||||||
|
BiFunction { timelineEvents, roomSummary ->
|
||||||
|
computeUnreadState(timelineEvents, roomSummary)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// We don't want live update of unread so we skip when we already had a HasUnread or HasNoUnread
|
||||||
|
.distinctUntilChanged { previous, current ->
|
||||||
|
when {
|
||||||
|
previous is UnreadState.Unknown || previous is UnreadState.ReadMarkerNotLoaded -> false
|
||||||
|
current is UnreadState.HasUnread || current is UnreadState.HasNoUnread -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.subscribe {
|
||||||
|
setState { copy(unreadState = it) }
|
||||||
|
}
|
||||||
|
.disposeOnClear()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun computeUnreadState(events: List<TimelineEvent>, roomSummary: RoomSummary): UnreadState {
|
||||||
|
if (events.isEmpty()) return UnreadState.Unknown
|
||||||
|
val readMarkerIdSnapshot = roomSummary.readMarkerId ?: return UnreadState.Unknown
|
||||||
|
val firstDisplayableEventId = timeline.getFirstDisplayableEventId(readMarkerIdSnapshot)
|
||||||
|
?: return UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot)
|
||||||
|
val firstDisplayableEventIndex = timeline.getIndexOfEvent(firstDisplayableEventId)
|
||||||
|
?: return UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot)
|
||||||
|
for (i in (firstDisplayableEventIndex - 1) downTo 0) {
|
||||||
|
val timelineEvent = events.getOrNull(i) ?: return UnreadState.Unknown
|
||||||
|
val eventId = timelineEvent.root.eventId ?: return UnreadState.Unknown
|
||||||
|
val isFromMe = timelineEvent.root.senderId == session.myUserId
|
||||||
|
if (!isFromMe) {
|
||||||
|
return UnreadState.HasUnread(eventId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return UnreadState.HasNoUnread
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observeUnreadState() {
|
||||||
|
selectSubscribe(RoomDetailViewState::unreadState) {
|
||||||
|
Timber.v("Unread state: $it")
|
||||||
|
if (it is UnreadState.HasNoUnread) {
|
||||||
|
startTrackingUnreadMessages()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun observeSummaryState() {
|
private fun observeSummaryState() {
|
||||||
asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary ->
|
asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary ->
|
||||||
if (summary.membership == Membership.INVITE) {
|
if (summary.membership == Membership.INVITE) {
|
||||||
@ -774,8 +853,13 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onUpdated(snapshot: List<TimelineEvent>) {
|
||||||
|
timelineEvents.accept(snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
timeline.dispose()
|
timeline.dispose()
|
||||||
|
timeline.removeAllListeners()
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,6 +41,13 @@ sealed class SendMode(open val text: String) {
|
|||||||
data class REPLY(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text)
|
data class REPLY(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sealed class UnreadState {
|
||||||
|
object Unknown : UnreadState()
|
||||||
|
object HasNoUnread : UnreadState()
|
||||||
|
data class ReadMarkerNotLoaded(val readMarkerId: String): UnreadState()
|
||||||
|
data class HasUnread(val firstUnreadEventId: String) : UnreadState()
|
||||||
|
}
|
||||||
|
|
||||||
data class RoomDetailViewState(
|
data class RoomDetailViewState(
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
val eventId: String?,
|
val eventId: String?,
|
||||||
@ -52,7 +59,9 @@ data class RoomDetailViewState(
|
|||||||
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 highlightedEventId: String? = null
|
val highlightedEventId: String? = null,
|
||||||
|
val unreadState: UnreadState = UnreadState.Unknown,
|
||||||
|
val canShowJumpToReadMarker: Boolean = true
|
||||||
) : MvRxState {
|
) : MvRxState {
|
||||||
|
|
||||||
constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId)
|
constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId)
|
||||||
|
|||||||
@ -66,7 +66,7 @@ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
|||||||
super.onActivityCreated(savedInstanceState)
|
super.onActivityCreated(savedInstanceState)
|
||||||
recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
|
recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
|
||||||
recyclerView.adapter = epoxyController.adapter
|
recyclerView.adapter = epoxyController.adapter
|
||||||
bottomSheetTitle.text = getString(R.string.read_at)
|
bottomSheetTitle.text = getString(R.string.seen_by)
|
||||||
epoxyController.setData(displayReadReceiptArgs.readReceipts)
|
epoxyController.setData(displayReadReceiptArgs.readReceipts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -25,21 +25,18 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import com.airbnb.epoxy.EpoxyController
|
import com.airbnb.epoxy.EpoxyController
|
||||||
import com.airbnb.epoxy.EpoxyModel
|
import com.airbnb.epoxy.EpoxyModel
|
||||||
import com.airbnb.epoxy.VisibilityState
|
import com.airbnb.epoxy.VisibilityState
|
||||||
|
import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.matrix.android.api.session.room.model.message.*
|
import im.vector.matrix.android.api.session.room.model.message.*
|
||||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.riotx.core.date.VectorDateFormatter
|
import im.vector.riotx.core.date.VectorDateFormatter
|
||||||
import im.vector.riotx.core.epoxy.LoadingItem_
|
import im.vector.riotx.core.epoxy.LoadingItem_
|
||||||
import im.vector.riotx.core.extensions.localDateTime
|
import im.vector.riotx.core.extensions.localDateTime
|
||||||
import im.vector.riotx.core.utils.DimensionConverter
|
|
||||||
import im.vector.riotx.features.home.AvatarRenderer
|
|
||||||
import im.vector.riotx.features.home.room.detail.RoomDetailViewState
|
import im.vector.riotx.features.home.room.detail.RoomDetailViewState
|
||||||
|
import im.vector.riotx.features.home.room.detail.UnreadState
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.factory.MergedHeaderItemFactory
|
import im.vector.riotx.features.home.room.detail.timeline.factory.MergedHeaderItemFactory
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.factory.TimelineItemFactory
|
import im.vector.riotx.features.home.room.detail.timeline.factory.TimelineItemFactory
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback
|
import im.vector.riotx.features.home.room.detail.timeline.helper.*
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener
|
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.nextOrNull
|
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.*
|
import im.vector.riotx.features.home.room.detail.timeline.item.*
|
||||||
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
|
||||||
@ -47,11 +44,10 @@ import org.threeten.bp.LocalDateTime
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter,
|
class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter,
|
||||||
|
private val session: Session,
|
||||||
private val timelineItemFactory: TimelineItemFactory,
|
private val timelineItemFactory: TimelineItemFactory,
|
||||||
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
|
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
|
||||||
private val mergedHeaderItemFactory: MergedHeaderItemFactory,
|
private val mergedHeaderItemFactory: MergedHeaderItemFactory,
|
||||||
private val avatarRenderer: AvatarRenderer,
|
|
||||||
private val dimensionConverter: DimensionConverter,
|
|
||||||
@TimelineEventControllerHandler
|
@TimelineEventControllerHandler
|
||||||
private val backgroundHandler: Handler
|
private val backgroundHandler: Handler
|
||||||
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor {
|
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor {
|
||||||
@ -86,7 +82,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||||||
|
|
||||||
interface ReadReceiptsCallback {
|
interface ReadReceiptsCallback {
|
||||||
fun onReadReceiptsClicked(readReceipts: List<ReadReceiptData>)
|
fun onReadReceiptsClicked(readReceipts: List<ReadReceiptData>)
|
||||||
fun onReadMarkerLongBound(readMarkerId: String, isDisplayed: Boolean)
|
fun onReadMarkerVisible()
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UrlClickCallback {
|
interface UrlClickCallback {
|
||||||
@ -101,6 +97,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||||||
private var currentSnapshot: List<TimelineEvent> = emptyList()
|
private var currentSnapshot: List<TimelineEvent> = emptyList()
|
||||||
private var inSubmitList: Boolean = false
|
private var inSubmitList: Boolean = false
|
||||||
private var timeline: Timeline? = null
|
private var timeline: Timeline? = null
|
||||||
|
private var unreadState: UnreadState = UnreadState.Unknown
|
||||||
|
private var positionOfReadMarker: Int? = null
|
||||||
|
private var eventIdToHighlight: String? = null
|
||||||
|
|
||||||
var callback: Callback? = null
|
var callback: Callback? = null
|
||||||
|
|
||||||
@ -152,7 +151,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update position when we are building new items
|
// Update position when we are building new items
|
||||||
override fun intercept(models: MutableList<EpoxyModel<*>>) {
|
override fun intercept(models: MutableList<EpoxyModel<*>>) = synchronized(modelCache) {
|
||||||
|
positionOfReadMarker = null
|
||||||
adapterPositionMapping.clear()
|
adapterPositionMapping.clear()
|
||||||
models.forEachIndexed { index, epoxyModel ->
|
models.forEachIndexed { index, epoxyModel ->
|
||||||
if (epoxyModel is BaseEventItem) {
|
if (epoxyModel is BaseEventItem) {
|
||||||
@ -161,18 +161,25 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val currentUnreadState = this.unreadState
|
||||||
|
if (currentUnreadState is UnreadState.HasUnread) {
|
||||||
|
val position = adapterPositionMapping[currentUnreadState.firstUnreadEventId]?.plus(1)
|
||||||
|
positionOfReadMarker = position
|
||||||
|
if (position != null) {
|
||||||
|
val readMarker = TimelineReadMarkerItem_()
|
||||||
|
.also {
|
||||||
|
it.id("read_marker")
|
||||||
|
it.setOnVisibilityStateChanged(ReadMarkerVisibilityStateChangedListener(callback))
|
||||||
|
}
|
||||||
|
models.add(position, readMarker)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun update(viewState: RoomDetailViewState, readMarkerVisible: Boolean) {
|
fun update(viewState: RoomDetailViewState) {
|
||||||
if (timeline != viewState.timeline) {
|
if (timeline?.timelineID != viewState.timeline?.timelineID) {
|
||||||
timeline = viewState.timeline
|
timeline = viewState.timeline
|
||||||
timeline?.listener = this
|
timeline?.addListener(this)
|
||||||
// Clear cache
|
|
||||||
synchronized(modelCache) {
|
|
||||||
for (i in 0 until modelCache.size) {
|
|
||||||
modelCache[i] = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
var requestModelBuild = false
|
var requestModelBuild = false
|
||||||
if (eventIdToHighlight != viewState.highlightedEventId) {
|
if (eventIdToHighlight != viewState.highlightedEventId) {
|
||||||
@ -188,8 +195,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||||||
eventIdToHighlight = viewState.highlightedEventId
|
eventIdToHighlight = viewState.highlightedEventId
|
||||||
requestModelBuild = true
|
requestModelBuild = true
|
||||||
}
|
}
|
||||||
if (this.readMarkerVisible != readMarkerVisible) {
|
if (this.unreadState != viewState.unreadState) {
|
||||||
this.readMarkerVisible = readMarkerVisible
|
this.unreadState = viewState.unreadState
|
||||||
requestModelBuild = true
|
requestModelBuild = true
|
||||||
}
|
}
|
||||||
if (requestModelBuild) {
|
if (requestModelBuild) {
|
||||||
@ -197,9 +204,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var readMarkerVisible: Boolean = false
|
|
||||||
private var eventIdToHighlight: String? = null
|
|
||||||
|
|
||||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||||
super.onAttachedToRecyclerView(recyclerView)
|
super.onAttachedToRecyclerView(recyclerView)
|
||||||
timelineMediaSizeProvider.recyclerView = recyclerView
|
timelineMediaSizeProvider.recyclerView = recyclerView
|
||||||
@ -224,7 +228,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timeline.LISTENER ***************************************************************************
|
// Timeline.LISTENER ***************************************************************************
|
||||||
|
|
||||||
override fun onUpdated(snapshot: List<TimelineEvent>) {
|
override fun onUpdated(snapshot: List<TimelineEvent>) {
|
||||||
submitSnapshot(snapshot)
|
submitSnapshot(snapshot)
|
||||||
@ -246,43 +250,40 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getModels(): List<EpoxyModel<*>> {
|
private fun getModels(): List<EpoxyModel<*>> {
|
||||||
synchronized(modelCache) {
|
buildCacheItemsIfNeeded()
|
||||||
(0 until modelCache.size).forEach { position ->
|
return modelCache
|
||||||
// Should be build if not cached or if cached but contains mergedHeader or formattedDay
|
.map {
|
||||||
// We then are sure we always have items up to date.
|
val eventModel = if (it == null || mergedHeaderItemFactory.isCollapsed(it.localId)) {
|
||||||
if (modelCache[position] == null
|
null
|
||||||
|| modelCache[position]?.mergedHeaderModel != null
|
} else {
|
||||||
|| modelCache[position]?.formattedDayModel != null) {
|
it.eventModel
|
||||||
modelCache[position] = buildItemModels(position, currentSnapshot)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return modelCache
|
|
||||||
.map {
|
|
||||||
val eventModel = if (it == null || mergedHeaderItemFactory.isCollapsed(it.localId)) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
it.eventModel
|
|
||||||
}
|
|
||||||
listOf(eventModel, it?.mergedHeaderModel, it?.formattedDayModel)
|
|
||||||
}
|
}
|
||||||
.flatten()
|
listOf(eventModel, it?.mergedHeaderModel, it?.formattedDayModel)
|
||||||
.filterNotNull()
|
}
|
||||||
|
.flatten()
|
||||||
|
.filterNotNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildCacheItemsIfNeeded() = synchronized(modelCache) {
|
||||||
|
if (modelCache.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
(0 until modelCache.size).forEach { position ->
|
||||||
|
// Should be build if not cached or if cached but contains additional models
|
||||||
|
// We then are sure we always have items up to date.
|
||||||
|
if (modelCache[position] == null || modelCache[position]?.shouldTriggerBuild() == true) {
|
||||||
|
modelCache[position] = buildCacheItem(position, currentSnapshot)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildItemModels(currentPosition: Int, items: List<TimelineEvent>): CacheItemData {
|
private fun buildCacheItem(currentPosition: Int, items: List<TimelineEvent>): CacheItemData {
|
||||||
val event = items[currentPosition]
|
val event = items[currentPosition]
|
||||||
val nextEvent = items.nextOrNull(currentPosition)
|
val nextEvent = items.nextOrNull(currentPosition)
|
||||||
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()
|
||||||
// Don't show read marker if it's on first item
|
val eventModel = timelineItemFactory.create(event, nextEvent, eventIdToHighlight, callback).also {
|
||||||
val showReadMarker = if (currentPosition == 0 && event.hasReadMarker) {
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
readMarkerVisible
|
|
||||||
}
|
|
||||||
val eventModel = timelineItemFactory.create(event, nextEvent, eventIdToHighlight, showReadMarker, callback).also {
|
|
||||||
it.id(event.localId)
|
it.id(event.localId)
|
||||||
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
|
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
|
||||||
}
|
}
|
||||||
@ -290,7 +291,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||||||
nextEvent = nextEvent,
|
nextEvent = nextEvent,
|
||||||
items = items,
|
items = items,
|
||||||
addDaySeparator = addDaySeparator,
|
addDaySeparator = addDaySeparator,
|
||||||
readMarkerVisible = readMarkerVisible,
|
|
||||||
currentPosition = currentPosition,
|
currentPosition = currentPosition,
|
||||||
eventIdToHighlight = eventIdToHighlight,
|
eventIdToHighlight = eventIdToHighlight,
|
||||||
callback = callback
|
callback = callback
|
||||||
@ -298,7 +298,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||||||
requestModelBuild()
|
requestModelBuild()
|
||||||
}
|
}
|
||||||
val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date)
|
val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date)
|
||||||
|
|
||||||
return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem)
|
return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -335,6 +334,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||||||
return adapterPositionMapping[eventId]
|
return adapterPositionMapping[eventId]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getPositionOfReadMarker(): Int? = synchronized(modelCache) {
|
||||||
|
return positionOfReadMarker
|
||||||
|
}
|
||||||
|
|
||||||
fun isLoadingForward() = showingForwardLoader
|
fun isLoadingForward() = showingForwardLoader
|
||||||
|
|
||||||
private data class CacheItemData(
|
private data class CacheItemData(
|
||||||
@ -343,5 +346,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||||||
val eventModel: EpoxyModel<*>? = null,
|
val eventModel: EpoxyModel<*>? = null,
|
||||||
val mergedHeaderModel: MergedHeaderItem? = null,
|
val mergedHeaderModel: MergedHeaderItem? = null,
|
||||||
val formattedDayModel: DaySeparatorItem? = null
|
val formattedDayModel: DaySeparatorItem? = null
|
||||||
)
|
) {
|
||||||
|
fun shouldTriggerBuild(): Boolean {
|
||||||
|
return mergedHeaderModel != null || formattedDayModel != null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -88,4 +88,12 @@ sealed class EventSharedAction(@StringRes val titleRes: Int, @DrawableRes val ic
|
|||||||
|
|
||||||
data class ViewEditHistory(val messageInformationData: MessageInformationData) :
|
data class ViewEditHistory(val messageInformationData: MessageInformationData) :
|
||||||
EventSharedAction(R.string.message_view_edit_history, R.drawable.ic_view_edit_history)
|
EventSharedAction(R.string.message_view_edit_history, R.drawable.ic_view_edit_history)
|
||||||
|
|
||||||
|
// An url in the event preview has been clicked
|
||||||
|
data class OnUrlClicked(val url: String) :
|
||||||
|
EventSharedAction(0, 0)
|
||||||
|
|
||||||
|
// An url in the event preview has been long clicked
|
||||||
|
data class OnUrlLongClicked(val url: String) :
|
||||||
|
EventSharedAction(0, 0)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -68,6 +68,18 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), Message
|
|||||||
messageActionsEpoxyController.listener = this
|
messageActionsEpoxyController.listener = this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onUrlClicked(url: String): Boolean {
|
||||||
|
sharedActionViewModel.post(EventSharedAction.OnUrlClicked(url))
|
||||||
|
// Always consume
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUrlLongClicked(url: String): Boolean {
|
||||||
|
sharedActionViewModel.post(EventSharedAction.OnUrlLongClicked(url))
|
||||||
|
// Always consume
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
override fun didSelectMenuAction(eventAction: EventSharedAction) {
|
override fun didSelectMenuAction(eventAction: EventSharedAction) {
|
||||||
if (eventAction is EventSharedAction.ReportContent) {
|
if (eventAction is EventSharedAction.ReportContent) {
|
||||||
// Toggle report menu
|
// Toggle report menu
|
||||||
|
|||||||
@ -23,6 +23,9 @@ import im.vector.riotx.R
|
|||||||
import im.vector.riotx.core.epoxy.bottomsheet.*
|
import im.vector.riotx.core.epoxy.bottomsheet.*
|
||||||
import im.vector.riotx.core.resources.StringProvider
|
import im.vector.riotx.core.resources.StringProvider
|
||||||
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.tools.createLinkMovementMethod
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.tools.linkify
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -38,26 +41,27 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid
|
|||||||
// Message preview
|
// Message preview
|
||||||
val body = state.messageBody
|
val body = state.messageBody
|
||||||
if (body != null) {
|
if (body != null) {
|
||||||
bottomSheetItemMessagePreview {
|
bottomSheetMessagePreviewItem {
|
||||||
id("preview")
|
id("preview")
|
||||||
avatarRenderer(avatarRenderer)
|
avatarRenderer(avatarRenderer)
|
||||||
avatarUrl(state.informationData.avatarUrl ?: "")
|
avatarUrl(state.informationData.avatarUrl ?: "")
|
||||||
senderId(state.informationData.senderId)
|
senderId(state.informationData.senderId)
|
||||||
senderName(state.senderName())
|
senderName(state.senderName())
|
||||||
body(body)
|
movementMethod(createLinkMovementMethod(listener))
|
||||||
|
body(body.linkify(listener))
|
||||||
time(state.time())
|
time(state.time())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send state
|
// Send state
|
||||||
if (state.informationData.sendState.isSending()) {
|
if (state.informationData.sendState.isSending()) {
|
||||||
bottomSheetItemSendState {
|
bottomSheetSendStateItem {
|
||||||
id("send_state")
|
id("send_state")
|
||||||
showProgress(true)
|
showProgress(true)
|
||||||
text(stringProvider.getString(R.string.event_status_sending_message))
|
text(stringProvider.getString(R.string.event_status_sending_message))
|
||||||
}
|
}
|
||||||
} else if (state.informationData.sendState.hasFailed()) {
|
} else if (state.informationData.sendState.hasFailed()) {
|
||||||
bottomSheetItemSendState {
|
bottomSheetSendStateItem {
|
||||||
id("send_state")
|
id("send_state")
|
||||||
showProgress(false)
|
showProgress(false)
|
||||||
text(stringProvider.getString(R.string.unable_to_send_message))
|
text(stringProvider.getString(R.string.unable_to_send_message))
|
||||||
@ -68,16 +72,16 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid
|
|||||||
// Quick reactions
|
// Quick reactions
|
||||||
if (state.canReact() && state.quickStates is Success) {
|
if (state.canReact() && state.quickStates is Success) {
|
||||||
// Separator
|
// Separator
|
||||||
bottomSheetItemSeparator {
|
bottomSheetSeparatorItem {
|
||||||
id("reaction_separator")
|
id("reaction_separator")
|
||||||
}
|
}
|
||||||
|
|
||||||
bottomSheetItemQuickReactions {
|
bottomSheetQuickReactionsItem {
|
||||||
id("quick_reaction")
|
id("quick_reaction")
|
||||||
fontProvider(fontProvider)
|
fontProvider(fontProvider)
|
||||||
texts(state.quickStates()?.map { it.reaction }.orEmpty())
|
texts(state.quickStates()?.map { it.reaction }.orEmpty())
|
||||||
selecteds(state.quickStates.invoke().map { it.isSelected })
|
selecteds(state.quickStates.invoke().map { it.isSelected })
|
||||||
listener(object : BottomSheetItemQuickReactions.Listener {
|
listener(object : BottomSheetQuickReactionsItem.Listener {
|
||||||
override fun didSelect(emoji: String, selected: Boolean) {
|
override fun didSelect(emoji: String, selected: Boolean) {
|
||||||
listener?.didSelectMenuAction(EventSharedAction.QuickReact(state.eventId, emoji, selected))
|
listener?.didSelectMenuAction(EventSharedAction.QuickReact(state.eventId, emoji, selected))
|
||||||
}
|
}
|
||||||
@ -86,18 +90,18 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Separator
|
// Separator
|
||||||
bottomSheetItemSeparator {
|
bottomSheetSeparatorItem {
|
||||||
id("actions_separator")
|
id("actions_separator")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Action
|
// Action
|
||||||
state.actions()?.forEachIndexed { index, action ->
|
state.actions()?.forEachIndexed { index, action ->
|
||||||
if (action is EventSharedAction.Separator) {
|
if (action is EventSharedAction.Separator) {
|
||||||
bottomSheetItemSeparator {
|
bottomSheetSeparatorItem {
|
||||||
id("separator_$index")
|
id("separator_$index")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
bottomSheetItemAction {
|
bottomSheetActionItem {
|
||||||
id("action_$index")
|
id("action_$index")
|
||||||
iconRes(action.iconResId)
|
iconRes(action.iconResId)
|
||||||
textRes(action.titleRes)
|
textRes(action.titleRes)
|
||||||
@ -114,7 +118,7 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid
|
|||||||
EventSharedAction.ReportContentInappropriate(action.eventId, action.senderId),
|
EventSharedAction.ReportContentInappropriate(action.eventId, action.senderId),
|
||||||
EventSharedAction.ReportContentCustom(action.eventId, action.senderId)
|
EventSharedAction.ReportContentCustom(action.eventId, action.senderId)
|
||||||
).forEachIndexed { indexReport, actionReport ->
|
).forEachIndexed { indexReport, actionReport ->
|
||||||
bottomSheetItemAction {
|
bottomSheetActionItem {
|
||||||
id("actionReport_$indexReport")
|
id("actionReport_$indexReport")
|
||||||
subMenuItem(true)
|
subMenuItem(true)
|
||||||
iconRes(actionReport.iconResId)
|
iconRes(actionReport.iconResId)
|
||||||
@ -127,7 +131,7 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MessageActionsEpoxyControllerListener {
|
interface MessageActionsEpoxyControllerListener : TimelineEventController.UrlClickCallback {
|
||||||
fun didSelectMenuAction(eventAction: EventSharedAction)
|
fun didSelectMenuAction(eventAction: EventSharedAction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,7 +46,6 @@ class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: Ava
|
|||||||
|
|
||||||
fun create(event: TimelineEvent,
|
fun create(event: TimelineEvent,
|
||||||
highlight: Boolean,
|
highlight: Boolean,
|
||||||
readMarkerVisible: Boolean,
|
|
||||||
callback: TimelineEventController.Callback?,
|
callback: TimelineEventController.Callback?,
|
||||||
exception: Exception? = null): DefaultItem {
|
exception: Exception? = null): DefaultItem {
|
||||||
val text = if (exception == null) {
|
val text = if (exception == null) {
|
||||||
@ -54,7 +53,7 @@ class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: Ava
|
|||||||
} 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, readMarkerVisible)
|
val informationData = informationDataFactory.create(event, null)
|
||||||
return create(text, informationData, highlight, callback)
|
return create(text, informationData, highlight, callback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,9 +25,10 @@ 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.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.AvatarSizeProvider
|
import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem_
|
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory
|
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
|
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem_
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.tools.createLinkMovementMethod
|
||||||
import me.gujun.android.span.span
|
import me.gujun.android.span.span
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ -41,7 +42,6 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
|
|||||||
fun create(event: TimelineEvent,
|
fun create(event: TimelineEvent,
|
||||||
nextEvent: TimelineEvent?,
|
nextEvent: TimelineEvent?,
|
||||||
highlight: Boolean,
|
highlight: Boolean,
|
||||||
readMarkerVisible: Boolean,
|
|
||||||
callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? {
|
callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? {
|
||||||
event.root.eventId ?: return null
|
event.root.eventId ?: return null
|
||||||
|
|
||||||
@ -57,7 +57,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
|
|||||||
}
|
}
|
||||||
|
|
||||||
val message = stringProvider.getString(R.string.encrypted_message).takeIf { cryptoError == null }
|
val message = stringProvider.getString(R.string.encrypted_message).takeIf { cryptoError == null }
|
||||||
?: stringProvider.getString(R.string.notice_crypto_unable_to_decrypt, errorDescription)
|
?: stringProvider.getString(R.string.notice_crypto_unable_to_decrypt, errorDescription)
|
||||||
val spannableStr = span(message) {
|
val spannableStr = span(message) {
|
||||||
textStyle = "italic"
|
textStyle = "italic"
|
||||||
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
|
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
|
||||||
@ -65,14 +65,14 @@ 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, readMarkerVisible)
|
val informationData = messageInformationDataFactory.create(event, nextEvent)
|
||||||
val attributes = attributesFactory.create(null, informationData, callback)
|
val attributes = attributesFactory.create(null, informationData, callback)
|
||||||
return MessageTextItem_()
|
return MessageTextItem_()
|
||||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||||
.highlighted(highlight)
|
.highlighted(highlight)
|
||||||
.attributes(attributes)
|
.attributes(attributes)
|
||||||
.message(spannableStr)
|
.message(spannableStr)
|
||||||
.urlClickCallback(callback)
|
.movementMethod(createLinkMovementMethod(callback))
|
||||||
}
|
}
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,7 +36,6 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act
|
|||||||
nextEvent: TimelineEvent?,
|
nextEvent: TimelineEvent?,
|
||||||
items: List<TimelineEvent>,
|
items: List<TimelineEvent>,
|
||||||
addDaySeparator: Boolean,
|
addDaySeparator: Boolean,
|
||||||
readMarkerVisible: Boolean,
|
|
||||||
currentPosition: Int,
|
currentPosition: Int,
|
||||||
eventIdToHighlight: String?,
|
eventIdToHighlight: String?,
|
||||||
callback: TimelineEventController.Callback?,
|
callback: TimelineEventController.Callback?,
|
||||||
@ -50,20 +49,12 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act
|
|||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
var highlighted = false
|
var highlighted = false
|
||||||
var readMarkerId: String? = null
|
|
||||||
var showReadMarker = false
|
|
||||||
val mergedEvents = (prevSameTypeEvents + listOf(event)).asReversed()
|
val mergedEvents = (prevSameTypeEvents + listOf(event)).asReversed()
|
||||||
val mergedData = ArrayList<MergedHeaderItem.Data>(mergedEvents.size)
|
val mergedData = ArrayList<MergedHeaderItem.Data>(mergedEvents.size)
|
||||||
mergedEvents.forEach { mergedEvent ->
|
mergedEvents.forEach { mergedEvent ->
|
||||||
if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) {
|
if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) {
|
||||||
highlighted = true
|
highlighted = true
|
||||||
}
|
}
|
||||||
if (readMarkerId == null && mergedEvent.hasReadMarker) {
|
|
||||||
readMarkerId = mergedEvent.root.eventId
|
|
||||||
}
|
|
||||||
if (!showReadMarker && mergedEvent.hasReadMarker && readMarkerVisible) {
|
|
||||||
showReadMarker = true
|
|
||||||
}
|
|
||||||
val senderAvatar = mergedEvent.senderAvatar
|
val senderAvatar = mergedEvent.senderAvatar
|
||||||
val senderName = mergedEvent.getDisambiguatedDisplayName()
|
val senderName = mergedEvent.getDisambiguatedDisplayName()
|
||||||
val data = MergedHeaderItem.Data(
|
val data = MergedHeaderItem.Data(
|
||||||
@ -96,8 +87,6 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act
|
|||||||
mergeItemCollapseStates[event.localId] = it
|
mergeItemCollapseStates[event.localId] = it
|
||||||
requestModelBuild()
|
requestModelBuild()
|
||||||
},
|
},
|
||||||
readMarkerId = readMarkerId,
|
|
||||||
showReadMarker = isCollapsed && showReadMarker,
|
|
||||||
readReceiptsCallback = callback
|
readReceiptsCallback = callback
|
||||||
)
|
)
|
||||||
MergedHeaderItem_()
|
MergedHeaderItem_()
|
||||||
|
|||||||
@ -24,8 +24,6 @@ import android.text.style.ClickableSpan
|
|||||||
import android.text.style.ForegroundColorSpan
|
import android.text.style.ForegroundColorSpan
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import dagger.Lazy
|
import dagger.Lazy
|
||||||
import im.vector.matrix.android.api.permalinks.MatrixLinkify
|
|
||||||
import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan
|
|
||||||
import im.vector.matrix.android.api.session.events.model.RelationType
|
import im.vector.matrix.android.api.session.events.model.RelationType
|
||||||
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.message.*
|
import im.vector.matrix.android.api.session.room.model.message.*
|
||||||
@ -35,7 +33,6 @@ import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
|
|||||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||||
import im.vector.riotx.core.linkify.VectorLinkify
|
|
||||||
import im.vector.riotx.core.resources.ColorProvider
|
import im.vector.riotx.core.resources.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.core.utils.DebouncedClickListener
|
||||||
@ -45,8 +42,10 @@ import im.vector.riotx.core.utils.isLocalFile
|
|||||||
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.*
|
import im.vector.riotx.features.home.room.detail.timeline.helper.*
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.*
|
import im.vector.riotx.features.home.room.detail.timeline.item.*
|
||||||
import im.vector.riotx.features.html.EventHtmlRenderer
|
import im.vector.riotx.features.home.room.detail.timeline.tools.createLinkMovementMethod
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.tools.linkify
|
||||||
import im.vector.riotx.features.html.CodeVisitor
|
import im.vector.riotx.features.html.CodeVisitor
|
||||||
|
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
|
||||||
import me.gujun.android.span.span
|
import me.gujun.android.span.span
|
||||||
@ -70,12 +69,11 @@ class MessageItemFactory @Inject constructor(
|
|||||||
fun create(event: TimelineEvent,
|
fun create(event: TimelineEvent,
|
||||||
nextEvent: TimelineEvent?,
|
nextEvent: TimelineEvent?,
|
||||||
highlight: Boolean,
|
highlight: Boolean,
|
||||||
readMarkerVisible: Boolean,
|
|
||||||
callback: TimelineEventController.Callback?
|
callback: TimelineEventController.Callback?
|
||||||
): VectorEpoxyModel<*>? {
|
): VectorEpoxyModel<*>? {
|
||||||
event.root.eventId ?: return null
|
event.root.eventId ?: return null
|
||||||
|
|
||||||
val informationData = messageInformationDataFactory.create(event, nextEvent, readMarkerVisible)
|
val informationData = messageInformationDataFactory.create(event, nextEvent)
|
||||||
|
|
||||||
if (event.root.isRedacted()) {
|
if (event.root.isRedacted()) {
|
||||||
// message is redacted
|
// message is redacted
|
||||||
@ -89,10 +87,10 @@ class MessageItemFactory @Inject constructor(
|
|||||||
return defaultItemFactory.create(malformedText, informationData, highlight, callback)
|
return defaultItemFactory.create(malformedText, informationData, highlight, callback)
|
||||||
}
|
}
|
||||||
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, readMarkerVisible, callback)
|
return noticeItemFactory.create(event, highlight, callback)
|
||||||
}
|
}
|
||||||
val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback)
|
val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback)
|
||||||
|
|
||||||
@ -195,8 +193,7 @@ class MessageItemFactory @Inject constructor(
|
|||||||
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,
|
||||||
@ -258,7 +255,7 @@ class MessageItemFactory @Inject constructor(
|
|||||||
highlight: Boolean,
|
highlight: Boolean,
|
||||||
callback: TimelineEventController.Callback?,
|
callback: TimelineEventController.Callback?,
|
||||||
attributes: AbsMessageItem.Attributes): MessageTextItem? {
|
attributes: AbsMessageItem.Attributes): MessageTextItem? {
|
||||||
val linkifiedBody = linkifyBody(body, callback)
|
val linkifiedBody = body.linkify(callback)
|
||||||
|
|
||||||
return MessageTextItem_().apply {
|
return MessageTextItem_().apply {
|
||||||
if (informationData.hasBeenEdited) {
|
if (informationData.hasBeenEdited) {
|
||||||
@ -273,7 +270,7 @@ class MessageItemFactory @Inject constructor(
|
|||||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||||
.attributes(attributes)
|
.attributes(attributes)
|
||||||
.highlighted(highlight)
|
.highlighted(highlight)
|
||||||
.urlClickCallback(callback)
|
.movementMethod(createLinkMovementMethod(callback))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildCodeBlockItem(formattedBody: CharSequence,
|
private fun buildCodeBlockItem(formattedBody: CharSequence,
|
||||||
@ -326,9 +323,9 @@ class MessageItemFactory @Inject constructor(
|
|||||||
// nop
|
// nop
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
editStart,
|
editStart,
|
||||||
editEnd,
|
editEnd,
|
||||||
Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
|
Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
|
||||||
return spannable
|
return spannable
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -344,14 +341,14 @@ class MessageItemFactory @Inject constructor(
|
|||||||
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
|
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
|
||||||
textStyle = "italic"
|
textStyle = "italic"
|
||||||
}
|
}
|
||||||
linkifyBody(formattedBody, callback)
|
formattedBody.linkify(callback)
|
||||||
}
|
}
|
||||||
return MessageTextItem_()
|
return MessageTextItem_()
|
||||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||||
.attributes(attributes)
|
.attributes(attributes)
|
||||||
.message(message)
|
.message(message)
|
||||||
.highlighted(highlight)
|
.highlighted(highlight)
|
||||||
.urlClickCallback(callback)
|
.movementMethod(createLinkMovementMethod(callback))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildEmoteMessageItem(messageContent: MessageEmoteContent,
|
private fun buildEmoteMessageItem(messageContent: MessageEmoteContent,
|
||||||
@ -361,7 +358,7 @@ class MessageItemFactory @Inject constructor(
|
|||||||
attributes: AbsMessageItem.Attributes): 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"
|
||||||
linkifyBody(formattedBody, callback)
|
formattedBody.linkify(callback)
|
||||||
}
|
}
|
||||||
return MessageTextItem_()
|
return MessageTextItem_()
|
||||||
.apply {
|
.apply {
|
||||||
@ -375,7 +372,7 @@ class MessageItemFactory @Inject constructor(
|
|||||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||||
.attributes(attributes)
|
.attributes(attributes)
|
||||||
.highlighted(highlight)
|
.highlighted(highlight)
|
||||||
.urlClickCallback(callback)
|
.movementMethod(createLinkMovementMethod(callback))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildRedactedItem(attributes: AbsMessageItem.Attributes,
|
private fun buildRedactedItem(attributes: AbsMessageItem.Attributes,
|
||||||
@ -386,17 +383,6 @@ class MessageItemFactory @Inject constructor(
|
|||||||
.highlighted(highlight)
|
.highlighted(highlight)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun linkifyBody(body: CharSequence, callback: TimelineEventController.Callback?): CharSequence {
|
|
||||||
val spannable = SpannableStringBuilder(body)
|
|
||||||
MatrixLinkify.addLinks(spannable, object : MatrixPermalinkSpan.Callback {
|
|
||||||
override fun onUrlClicked(url: String) {
|
|
||||||
callback?.onUrlClicked(url)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
VectorLinkify.addLinks(spannable, true)
|
|
||||||
return spannable
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT = 5
|
private const val MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT = 5
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,10 +34,9 @@ class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEv
|
|||||||
|
|
||||||
fun create(event: TimelineEvent,
|
fun create(event: TimelineEvent,
|
||||||
highlight: Boolean,
|
highlight: Boolean,
|
||||||
readMarkerVisible: Boolean,
|
|
||||||
callback: TimelineEventController.Callback?): NoticeItem? {
|
callback: TimelineEventController.Callback?): NoticeItem? {
|
||||||
val formattedText = eventFormatter.format(event) ?: return null
|
val formattedText = eventFormatter.format(event) ?: return null
|
||||||
val informationData = informationDataFactory.create(event, null, readMarkerVisible)
|
val informationData = informationDataFactory.create(event, null)
|
||||||
val attributes = NoticeItem.Attributes(
|
val attributes = NoticeItem.Attributes(
|
||||||
avatarRenderer = avatarRenderer,
|
avatarRenderer = avatarRenderer,
|
||||||
informationData = informationData,
|
informationData = informationData,
|
||||||
|
|||||||
@ -33,14 +33,13 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||||||
fun create(event: TimelineEvent,
|
fun create(event: TimelineEvent,
|
||||||
nextEvent: TimelineEvent?,
|
nextEvent: TimelineEvent?,
|
||||||
eventIdToHighlight: String?,
|
eventIdToHighlight: String?,
|
||||||
readMarkerVisible: Boolean,
|
|
||||||
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 {
|
||||||
when (event.root.getClearType()) {
|
when (event.root.getClearType()) {
|
||||||
EventType.STICKER,
|
EventType.STICKER,
|
||||||
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, readMarkerVisible, callback)
|
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, callback)
|
||||||
// State and call
|
// State and call
|
||||||
EventType.STATE_ROOM_TOMBSTONE,
|
EventType.STATE_ROOM_TOMBSTONE,
|
||||||
EventType.STATE_ROOM_NAME,
|
EventType.STATE_ROOM_NAME,
|
||||||
@ -53,21 +52,21 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||||||
EventType.CALL_ANSWER,
|
EventType.CALL_ANSWER,
|
||||||
EventType.REACTION,
|
EventType.REACTION,
|
||||||
EventType.REDACTION,
|
EventType.REDACTION,
|
||||||
EventType.ENCRYPTION -> noticeItemFactory.create(event, highlight, readMarkerVisible, 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.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
|
||||||
messageItemFactory.create(event, nextEvent, highlight, readMarkerVisible, callback)
|
messageItemFactory.create(event, nextEvent, highlight, callback)
|
||||||
} else {
|
} else {
|
||||||
encryptedItemFactory.create(event, nextEvent, highlight, readMarkerVisible, callback)
|
encryptedItemFactory.create(event, nextEvent, highlight, callback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unhandled event types (yet)
|
// Unhandled event types (yet)
|
||||||
EventType.STATE_ROOM_THIRD_PARTY_INVITE -> defaultItemFactory.create(event, highlight, readMarkerVisible, callback)
|
EventType.STATE_ROOM_THIRD_PARTY_INVITE -> 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
|
||||||
@ -75,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, readMarkerVisible, callback, e)
|
defaultItemFactory.create(event, highlight, callback, e)
|
||||||
}
|
}
|
||||||
return (computedModel ?: EmptyItem_())
|
return (computedModel ?: EmptyItem_())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,7 +39,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
|||||||
private val dateFormatter: VectorDateFormatter,
|
private val dateFormatter: VectorDateFormatter,
|
||||||
private val colorProvider: ColorProvider) {
|
private val colorProvider: ColorProvider) {
|
||||||
|
|
||||||
fun create(event: TimelineEvent, nextEvent: TimelineEvent?, readMarkerVisible: Boolean): MessageInformationData {
|
fun create(event: TimelineEvent, nextEvent: TimelineEvent?): MessageInformationData {
|
||||||
// Non nullability has been tested before
|
// Non nullability has been tested before
|
||||||
val eventId = event.root.eventId!!
|
val eventId = event.root.eventId!!
|
||||||
|
|
||||||
@ -47,7 +47,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
|||||||
val nextDate = nextEvent?.root?.localDateTime()
|
val nextDate = nextEvent?.root?.localDateTime()
|
||||||
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
|
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
|
||||||
val isNextMessageReceivedMoreThanOneHourAgo = nextDate?.isBefore(date.minusMinutes(60))
|
val isNextMessageReceivedMoreThanOneHourAgo = nextDate?.isBefore(date.minusMinutes(60))
|
||||||
?: false
|
?: false
|
||||||
|
|
||||||
val showInformation =
|
val showInformation =
|
||||||
addDaySeparator
|
addDaySeparator
|
||||||
@ -63,8 +63,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 = readMarkerVisible && event.hasReadMarker
|
|
||||||
|
|
||||||
return MessageInformationData(
|
return MessageInformationData(
|
||||||
eventId = eventId,
|
eventId = eventId,
|
||||||
senderId = event.root.senderId ?: "",
|
senderId = event.root.senderId ?: "",
|
||||||
@ -88,9 +86,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()
|
||||||
hasReadMarker = event.hasReadMarker,
|
|
||||||
displayReadMarker = displayReadMarker
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,16 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
|||||||
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||||
|
|
||||||
|
class ReadMarkerVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?)
|
||||||
|
: VectorEpoxyModel.OnVisibilityStateChangedListener {
|
||||||
|
|
||||||
|
override fun onVisibilityStateChanged(visibilityState: Int) {
|
||||||
|
if (visibilityState == VisibilityState.VISIBLE) {
|
||||||
|
callback?.onReadMarkerVisible()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class TimelineEventVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?,
|
class TimelineEventVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?,
|
||||||
private val event: TimelineEvent)
|
private val event: TimelineEvent)
|
||||||
: VectorEpoxyModel.OnVisibilityStateChangedListener {
|
: VectorEpoxyModel.OnVisibilityStateChangedListener {
|
||||||
@ -27,7 +27,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.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
|
||||||
@ -50,13 +49,6 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
|||||||
attributes.readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts)
|
attributes.readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts)
|
||||||
})
|
})
|
||||||
|
|
||||||
private val _readMarkerCallback = object : ReadMarkerView.Callback {
|
|
||||||
|
|
||||||
override fun onReadMarkerLongBound(isDisplayed: Boolean) {
|
|
||||||
attributes.readReceiptsCallback?.onReadMarkerLongBound(attributes.informationData.eventId, isDisplayed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
attributes.reactionPillCallback?.onClickOnReactionPill(attributes.informationData, reactionButton.reactionString, true)
|
||||||
@ -110,12 +102,6 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
|||||||
attributes.avatarRenderer,
|
attributes.avatarRenderer,
|
||||||
_readReceiptsClickListener
|
_readReceiptsClickListener
|
||||||
)
|
)
|
||||||
holder.readMarkerView.bindView(
|
|
||||||
attributes.informationData.eventId,
|
|
||||||
attributes.informationData.hasReadMarker,
|
|
||||||
attributes.informationData.displayReadMarker,
|
|
||||||
_readMarkerCallback
|
|
||||||
)
|
|
||||||
|
|
||||||
val reactions = attributes.informationData.orderedReactionList
|
val reactions = attributes.informationData.orderedReactionList
|
||||||
if (!shouldShowReactionAtBottom() || reactions.isNullOrEmpty()) {
|
if (!shouldShowReactionAtBottom() || reactions.isNullOrEmpty()) {
|
||||||
@ -138,7 +124,6 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun unbind(holder: H) {
|
override fun unbind(holder: H) {
|
||||||
holder.readMarkerView.unbind()
|
|
||||||
holder.readReceiptsView.unbind()
|
holder.readReceiptsView.unbind()
|
||||||
super.unbind(holder)
|
super.unbind(holder)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,14 +19,12 @@ import android.view.View
|
|||||||
import android.view.ViewStub
|
import android.view.ViewStub
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
import androidx.annotation.IdRes
|
import androidx.annotation.IdRes
|
||||||
import androidx.core.view.marginStart
|
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
import im.vector.riotx.R
|
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.ui.views.ReadMarkerView
|
|
||||||
import im.vector.riotx.core.ui.views.ReadReceiptsView
|
import im.vector.riotx.core.ui.views.ReadReceiptsView
|
||||||
import im.vector.riotx.core.utils.DimensionConverter
|
import im.vector.riotx.core.utils.DimensionConverter
|
||||||
|
|
||||||
@ -62,7 +60,6 @@ abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>
|
|||||||
val leftGuideline by bind<View>(R.id.messageStartGuideline)
|
val leftGuideline by bind<View>(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)
|
val readReceiptsView by bind<ReadReceiptsView>(R.id.readReceiptsView)
|
||||||
val readMarkerView by bind<ReadMarkerView>(R.id.readMarkerView)
|
|
||||||
|
|
||||||
override fun bindView(itemView: View) {
|
override fun bindView(itemView: View) {
|
||||||
super.bindView(itemView)
|
super.bindView(itemView)
|
||||||
|
|||||||
@ -25,7 +25,6 @@ import androidx.core.view.isVisible
|
|||||||
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.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
|
||||||
|
|
||||||
@ -39,13 +38,6 @@ abstract class MergedHeaderItem : BaseEventItem<MergedHeaderItem.Holder>() {
|
|||||||
attributes.mergeData.distinctBy { it.userId }
|
attributes.mergeData.distinctBy { it.userId }
|
||||||
}
|
}
|
||||||
|
|
||||||
private val _readMarkerCallback = object : ReadMarkerView.Callback {
|
|
||||||
|
|
||||||
override fun onReadMarkerLongBound(isDisplayed: Boolean) {
|
|
||||||
attributes.readReceiptsCallback?.onReadMarkerLongBound(attributes.readMarkerId ?: "", isDisplayed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getViewType() = STUB_ID
|
override fun getViewType() = STUB_ID
|
||||||
|
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
@ -77,20 +69,14 @@ abstract class MergedHeaderItem : BaseEventItem<MergedHeaderItem.Holder>() {
|
|||||||
}
|
}
|
||||||
// No read receipt for this item
|
// No read receipt for this item
|
||||||
holder.readReceiptsView.isVisible = false
|
holder.readReceiptsView.isVisible = false
|
||||||
holder.readMarkerView.bindView(
|
|
||||||
attributes.readMarkerId,
|
|
||||||
!attributes.readMarkerId.isNullOrEmpty(),
|
|
||||||
attributes.showReadMarker,
|
|
||||||
_readMarkerCallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun unbind(holder: Holder) {
|
|
||||||
holder.readMarkerView.unbind()
|
|
||||||
super.unbind(holder)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getEventIds(): List<String> {
|
override fun getEventIds(): List<String> {
|
||||||
return attributes.mergeData.map { it.eventId }
|
return if (attributes.isCollapsed) {
|
||||||
|
attributes.mergeData.map { it.eventId }
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Data(
|
data class Data(
|
||||||
@ -102,9 +88,7 @@ abstract class MergedHeaderItem : BaseEventItem<MergedHeaderItem.Holder>() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
data class Attributes(
|
data class Attributes(
|
||||||
val readMarkerId: String?,
|
|
||||||
val isCollapsed: Boolean,
|
val isCollapsed: Boolean,
|
||||||
val showReadMarker: Boolean,
|
|
||||||
val mergeData: List<Data>,
|
val mergeData: List<Data>,
|
||||||
val avatarRenderer: AvatarRenderer,
|
val avatarRenderer: AvatarRenderer,
|
||||||
val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
|
val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
|
||||||
@ -119,6 +103,6 @@ abstract class MergedHeaderItem : BaseEventItem<MergedHeaderItem.Holder>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val STUB_ID = R.id.messageContentMergedheaderStub
|
private const val STUB_ID = R.id.messageContentMergedHeaderStub
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,9 +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 hasReadMarker: Boolean = false,
|
|
||||||
val displayReadMarker: Boolean = false
|
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
|||||||
@ -16,22 +16,14 @@
|
|||||||
|
|
||||||
package im.vector.riotx.features.home.room.detail.timeline.item
|
package im.vector.riotx.features.home.room.detail.timeline.item
|
||||||
|
|
||||||
import android.view.MotionEvent
|
import android.text.method.MovementMethod
|
||||||
import androidx.appcompat.widget.AppCompatTextView
|
import androidx.appcompat.widget.AppCompatTextView
|
||||||
import androidx.core.text.PrecomputedTextCompat
|
import androidx.core.text.PrecomputedTextCompat
|
||||||
import androidx.core.text.toSpannable
|
|
||||||
import androidx.core.widget.TextViewCompat
|
import androidx.core.widget.TextViewCompat
|
||||||
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.isValidUrl
|
import im.vector.riotx.features.home.room.detail.timeline.tools.findPillsAndProcess
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
|
||||||
import im.vector.riotx.features.html.PillImageSpan
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import me.saket.bettermovementmethod.BetterLinkMovementMethod
|
|
||||||
|
|
||||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||||
abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
||||||
@ -43,30 +35,11 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
|||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
var useBigFont: Boolean = false
|
var useBigFont: Boolean = false
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
var urlClickCallback: TimelineEventController.UrlClickCallback? = null
|
var movementMethod: MovementMethod? = null
|
||||||
|
|
||||||
// Better link movement methods fixes the issue when
|
|
||||||
// long pressing to open the context menu on a TextView also triggers an autoLink click.
|
|
||||||
private val mvmtMethod = BetterLinkMovementMethod.newInstance().also {
|
|
||||||
it.setOnLinkClickListener { _, url ->
|
|
||||||
// Return false to let android manage the click on the link, or true if the link is handled by the application
|
|
||||||
url.isValidUrl() && urlClickCallback?.onUrlClicked(url) == true
|
|
||||||
}
|
|
||||||
// We need also to fix the case when long click on link will trigger long click on cell
|
|
||||||
it.setOnLinkLongClickListener { tv, url ->
|
|
||||||
// Long clicks are handled by parent, return true to block android to do something with url
|
|
||||||
if (url.isValidUrl() && urlClickCallback?.onUrlLongClicked(url) == true) {
|
|
||||||
tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0))
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
super.bind(holder)
|
super.bind(holder)
|
||||||
holder.messageView.movementMethod = mvmtMethod
|
holder.messageView.movementMethod = movementMethod
|
||||||
if (useBigFont) {
|
if (useBigFont) {
|
||||||
holder.messageView.textSize = 44F
|
holder.messageView.textSize = 44F
|
||||||
} else {
|
} else {
|
||||||
@ -76,7 +49,7 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
|||||||
holder.messageView.setOnClickListener(attributes.itemClickListener)
|
holder.messageView.setOnClickListener(attributes.itemClickListener)
|
||||||
holder.messageView.setOnLongClickListener(attributes.itemLongClickListener)
|
holder.messageView.setOnLongClickListener(attributes.itemLongClickListener)
|
||||||
if (searchForPills) {
|
if (searchForPills) {
|
||||||
findPillsAndProcess { it.bind(holder.messageView) }
|
message?.findPillsAndProcess { it.bind(holder.messageView) }
|
||||||
}
|
}
|
||||||
val textFuture = PrecomputedTextCompat.getTextFuture(
|
val textFuture = PrecomputedTextCompat.getTextFuture(
|
||||||
message ?: "",
|
message ?: "",
|
||||||
@ -85,17 +58,6 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
|||||||
holder.messageView.setTextFuture(textFuture)
|
holder.messageView.setTextFuture(textFuture)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findPillsAndProcess(processBlock: (span: PillImageSpan) -> Unit) {
|
|
||||||
GlobalScope.launch(Dispatchers.Main) {
|
|
||||||
val pillImageSpans: Array<PillImageSpan>? = withContext(Dispatchers.IO) {
|
|
||||||
message?.toSpannable()?.let { spannable ->
|
|
||||||
spannable.getSpans(0, spannable.length, PillImageSpan::class.java)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pillImageSpans?.forEach { processBlock(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getViewType() = STUB_ID
|
override fun getViewType() = STUB_ID
|
||||||
|
|
||||||
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
||||||
|
|||||||
@ -22,7 +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.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
|
||||||
@ -37,13 +36,6 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
|
|||||||
attributes.readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts)
|
attributes.readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts)
|
||||||
})
|
})
|
||||||
|
|
||||||
private val _readMarkerCallback = object : ReadMarkerView.Callback {
|
|
||||||
|
|
||||||
override fun onReadMarkerLongBound(isDisplayed: Boolean) {
|
|
||||||
attributes.readReceiptsCallback?.onReadMarkerLongBound(attributes.informationData.eventId, isDisplayed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
super.bind(holder)
|
super.bind(holder)
|
||||||
holder.noticeTextView.text = attributes.noticeText
|
holder.noticeTextView.text = attributes.noticeText
|
||||||
@ -56,17 +48,6 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
|
|||||||
)
|
)
|
||||||
holder.view.setOnLongClickListener(attributes.itemLongClickListener)
|
holder.view.setOnLongClickListener(attributes.itemLongClickListener)
|
||||||
holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener)
|
holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener)
|
||||||
holder.readMarkerView.bindView(
|
|
||||||
attributes.informationData.eventId,
|
|
||||||
attributes.informationData.hasReadMarker,
|
|
||||||
attributes.informationData.displayReadMarker,
|
|
||||||
_readMarkerCallback
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun unbind(holder: Holder) {
|
|
||||||
holder.readMarkerView.unbind()
|
|
||||||
super.unbind(holder)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getEventIds(): List<String> {
|
override fun getEventIds(): List<String> {
|
||||||
|
|||||||
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.features.home.room.detail.timeline.item
|
||||||
|
|
||||||
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
|
||||||
|
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||||
|
|
||||||
|
@EpoxyModelClass(layout = R.layout.item_timeline_read_marker)
|
||||||
|
abstract class TimelineReadMarkerItem : VectorEpoxyModel<TimelineReadMarkerItem.Holder>() {
|
||||||
|
|
||||||
|
override fun bind(holder: Holder) {
|
||||||
|
}
|
||||||
|
|
||||||
|
class Holder : VectorEpoxyHolder()
|
||||||
|
}
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* 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.tools
|
||||||
|
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import androidx.core.text.toSpannable
|
||||||
|
import im.vector.matrix.android.api.permalinks.MatrixLinkify
|
||||||
|
import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan
|
||||||
|
import im.vector.riotx.core.linkify.VectorLinkify
|
||||||
|
import im.vector.riotx.core.utils.isValidUrl
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||||
|
import im.vector.riotx.features.html.PillImageSpan
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import me.saket.bettermovementmethod.BetterLinkMovementMethod
|
||||||
|
|
||||||
|
fun CharSequence.findPillsAndProcess(processBlock: (PillImageSpan) -> Unit) {
|
||||||
|
GlobalScope.launch(Dispatchers.Main) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
toSpannable().let { spannable ->
|
||||||
|
spannable.getSpans(0, spannable.length, PillImageSpan::class.java)
|
||||||
|
}
|
||||||
|
}.forEach { processBlock(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun CharSequence.linkify(callback: TimelineEventController.UrlClickCallback?): CharSequence {
|
||||||
|
val spannable = SpannableStringBuilder(this)
|
||||||
|
MatrixLinkify.addLinks(spannable, object : MatrixPermalinkSpan.Callback {
|
||||||
|
override fun onUrlClicked(url: String) {
|
||||||
|
callback?.onUrlClicked(url)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
VectorLinkify.addLinks(spannable, true)
|
||||||
|
return spannable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Better link movement methods fixes the issue when
|
||||||
|
// long pressing to open the context menu on a TextView also triggers an autoLink click.
|
||||||
|
fun createLinkMovementMethod(urlClickCallback: TimelineEventController.UrlClickCallback?): BetterLinkMovementMethod {
|
||||||
|
return BetterLinkMovementMethod.newInstance()
|
||||||
|
.apply {
|
||||||
|
setOnLinkClickListener { _, url ->
|
||||||
|
// Return false to let android manage the click on the link, or true if the link is handled by the application
|
||||||
|
url.isValidUrl() && urlClickCallback?.onUrlClicked(url) == true
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need also to fix the case when long click on link will trigger long click on cell
|
||||||
|
setOnLinkLongClickListener { tv, url ->
|
||||||
|
// Long clicks are handled by parent, return true to block android to do something with url
|
||||||
|
if (url.isValidUrl() && urlClickCallback?.onUrlLongClicked(url) == true) {
|
||||||
|
tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0))
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -18,9 +18,9 @@ package im.vector.riotx.features.home.room.list.actions
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import com.airbnb.epoxy.TypedEpoxyController
|
import com.airbnb.epoxy.TypedEpoxyController
|
||||||
import im.vector.matrix.android.api.session.room.notification.RoomNotificationState
|
import im.vector.matrix.android.api.session.room.notification.RoomNotificationState
|
||||||
import im.vector.riotx.core.epoxy.bottomsheet.bottomSheetItemAction
|
import im.vector.riotx.core.epoxy.bottomsheet.bottomSheetActionItem
|
||||||
import im.vector.riotx.core.epoxy.bottomsheet.bottomSheetItemRoomPreview
|
import im.vector.riotx.core.epoxy.bottomsheet.bottomSheetRoomPreviewItem
|
||||||
import im.vector.riotx.core.epoxy.bottomsheet.bottomSheetItemSeparator
|
import im.vector.riotx.core.epoxy.bottomsheet.bottomSheetSeparatorItem
|
||||||
import im.vector.riotx.features.home.AvatarRenderer
|
import im.vector.riotx.features.home.AvatarRenderer
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ -36,7 +36,7 @@ class RoomListQuickActionsEpoxyController @Inject constructor(private val avatar
|
|||||||
val roomSummary = state.roomSummary() ?: return
|
val roomSummary = state.roomSummary() ?: return
|
||||||
|
|
||||||
// Preview
|
// Preview
|
||||||
bottomSheetItemRoomPreview {
|
bottomSheetRoomPreviewItem {
|
||||||
id("preview")
|
id("preview")
|
||||||
avatarRenderer(avatarRenderer)
|
avatarRenderer(avatarRenderer)
|
||||||
roomName(roomSummary.displayName)
|
roomName(roomSummary.displayName)
|
||||||
@ -46,7 +46,7 @@ class RoomListQuickActionsEpoxyController @Inject constructor(private val avatar
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Notifications
|
// Notifications
|
||||||
bottomSheetItemSeparator {
|
bottomSheetSeparatorItem {
|
||||||
id("notifications_separator")
|
id("notifications_separator")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,7 +57,7 @@ class RoomListQuickActionsEpoxyController @Inject constructor(private val avatar
|
|||||||
RoomListQuickActionsSharedAction.NotificationsMute(roomSummary.roomId).toBottomSheetItem(3, selectedRoomState)
|
RoomListQuickActionsSharedAction.NotificationsMute(roomSummary.roomId).toBottomSheetItem(3, selectedRoomState)
|
||||||
|
|
||||||
// Leave
|
// Leave
|
||||||
bottomSheetItemSeparator {
|
bottomSheetSeparatorItem {
|
||||||
id("leave_separator")
|
id("leave_separator")
|
||||||
}
|
}
|
||||||
RoomListQuickActionsSharedAction.Leave(roomSummary.roomId).toBottomSheetItem(5)
|
RoomListQuickActionsSharedAction.Leave(roomSummary.roomId).toBottomSheetItem(5)
|
||||||
@ -72,7 +72,7 @@ class RoomListQuickActionsEpoxyController @Inject constructor(private val avatar
|
|||||||
is RoomListQuickActionsSharedAction.Settings,
|
is RoomListQuickActionsSharedAction.Settings,
|
||||||
is RoomListQuickActionsSharedAction.Leave -> false
|
is RoomListQuickActionsSharedAction.Leave -> false
|
||||||
}
|
}
|
||||||
return bottomSheetItemAction {
|
return bottomSheetActionItem {
|
||||||
id("action_$index")
|
id("action_$index")
|
||||||
selected(selected)
|
selected(selected)
|
||||||
iconRes(iconResId)
|
iconRes(iconResId)
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
android:id="@+id/messageSelectedBackground"
|
android:id="@+id/messageSelectedBackground"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_alignBottom="@+id/readMarkerView"
|
android:layout_alignBottom="@+id/informationBottom"
|
||||||
android:layout_alignParentTop="true"
|
android:layout_alignParentTop="true"
|
||||||
android:background="?riotx_highlighted_message_background" />
|
android:background="?riotx_highlighted_message_background" />
|
||||||
|
|
||||||
@ -145,15 +145,4 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<im.vector.riotx.core.ui.views.ReadMarkerView
|
|
||||||
android:id="@+id/readMarkerView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="2dp"
|
|
||||||
android:layout_below="@+id/informationBottom"
|
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:layout_marginEnd="8dp"
|
|
||||||
android:layout_marginBottom="2dp"
|
|
||||||
android:background="?attr/vctr_unread_marker_line_color"
|
|
||||||
android:visibility="invisible" />
|
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
@ -10,7 +10,7 @@
|
|||||||
android:id="@+id/messageSelectedBackground"
|
android:id="@+id/messageSelectedBackground"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_alignBottom="@+id/informationBottom"
|
android:layout_alignBottom="@+id/readReceiptsView"
|
||||||
android:layout_alignParentTop="true"
|
android:layout_alignParentTop="true"
|
||||||
android:background="?riotx_highlighted_message_background" />
|
android:background="?riotx_highlighted_message_background" />
|
||||||
|
|
||||||
@ -47,37 +47,19 @@
|
|||||||
android:layout="@layout/item_timeline_event_blank_stub" />
|
android:layout="@layout/item_timeline_event_blank_stub" />
|
||||||
|
|
||||||
<ViewStub
|
<ViewStub
|
||||||
android:id="@+id/messageContentMergedheaderStub"
|
android:id="@+id/messageContentMergedHeaderStub"
|
||||||
style="@style/TimelineContentStubBaseParams"
|
style="@style/TimelineContentStubBaseParams"
|
||||||
android:layout="@layout/item_timeline_event_merged_header_stub" />
|
android:layout="@layout/item_timeline_event_merged_header_stub" />
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<im.vector.riotx.core.ui.views.ReadReceiptsView
|
||||||
android:id="@+id/informationBottom"
|
android:id="@+id/readReceiptsView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_below="@id/viewStubContainer"
|
android:layout_below="@id/viewStubContainer"
|
||||||
android:orientation="vertical">
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
<im.vector.riotx.core.ui.views.ReadReceiptsView
|
android:layout_marginBottom="4dp" />
|
||||||
android:id="@+id/readReceiptsView"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="end"
|
|
||||||
android:layout_marginEnd="8dp"
|
|
||||||
android:layout_marginBottom="4dp" />
|
|
||||||
|
|
||||||
<im.vector.riotx.core.ui.views.ReadMarkerView
|
|
||||||
android:id="@+id/readMarkerView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
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" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@ -8,50 +7,23 @@
|
|||||||
android:padding="8dp">
|
android:padding="8dp">
|
||||||
|
|
||||||
<View
|
<View
|
||||||
android:id="@+id/itemDayLineViewLeft"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="1dp"
|
android:layout_height="1dp"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
android:layout_marginStart="8dp"
|
android:layout_marginStart="8dp"
|
||||||
android:layout_marginLeft="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
android:layout_marginTop="8dp"
|
android:background="?riotx_header_panel_background" />
|
||||||
android:layout_marginEnd="32dp"
|
|
||||||
android:layout_marginRight="32dp"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
android:background="?riotx_header_panel_background"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/itemDayTextView"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/itemDayTextView"
|
android:id="@+id/itemDayTextView"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center"
|
android:layout_gravity="center"
|
||||||
|
android:background="?riotx_background"
|
||||||
|
android:paddingStart="32dp"
|
||||||
|
android:paddingEnd="32dp"
|
||||||
android:textColor="?riotx_header_panel_text_primary"
|
android:textColor="?riotx_header_panel_text_primary"
|
||||||
android:textSize="15sp"
|
android:textSize="15sp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
tools:text="@tools:sample/date/day_of_week" />
|
tools:text="@tools:sample/date/day_of_week" />
|
||||||
|
|
||||||
<View
|
</FrameLayout>
|
||||||
android:id="@+id/itemDayLineViewRight"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="1dp"
|
|
||||||
android:layout_marginStart="32dp"
|
|
||||||
android:layout_marginLeft="32dp"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:layout_marginEnd="8dp"
|
|
||||||
android:layout_marginRight="8dp"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
android:background="?riotx_header_panel_background"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/itemDayTextView"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
28
vector/src/main/res/layout/item_timeline_read_marker.xml
Normal file
28
vector/src/main/res/layout/item_timeline_read_marker.xml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?riotx_background"
|
||||||
|
android:padding="8dp">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:background="@color/notification_accent_color" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:background="?riotx_background"
|
||||||
|
android:fontFamily="sans-serif-medium"
|
||||||
|
android:paddingStart="8dp"
|
||||||
|
android:paddingEnd="8dp"
|
||||||
|
android:text="@string/timeline_unread_messages"
|
||||||
|
android:textColor="@color/notification_accent_color"
|
||||||
|
android:textSize="15sp" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
@ -1783,7 +1783,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
|
|||||||
|
|
||||||
<string name="report_content_spam">"It's spam"</string>
|
<string name="report_content_spam">"It's spam"</string>
|
||||||
<string name="report_content_inappropriate">"It's inappropriate"</string>
|
<string name="report_content_inappropriate">"It's inappropriate"</string>
|
||||||
<string name="report_content_custom">"Custom report"</string>
|
<string name="report_content_custom">"Custom report…"</string>
|
||||||
<string name="report_content_custom_title">"Report this content"</string>
|
<string name="report_content_custom_title">"Report this content"</string>
|
||||||
<string name="report_content_custom_hint">"Reason for reporting this content"</string>
|
<string name="report_content_custom_hint">"Reason for reporting this content"</string>
|
||||||
<string name="report_content_custom_submit">"REPORT"</string>
|
<string name="report_content_custom_submit">"REPORT"</string>
|
||||||
|
|||||||
@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
<string name="room_join_rules_public">%1$s made the room public to whoever knows the link.</string>
|
<string name="room_join_rules_public">%1$s made the room public to whoever knows the link.</string>
|
||||||
<string name="room_join_rules_invite">%1$s made the room invite only.</string>
|
<string name="room_join_rules_invite">%1$s made the room invite only.</string>
|
||||||
|
<string name="timeline_unread_messages">Unread messages</string>
|
||||||
|
|
||||||
<string name="login_splash_title">Liberate your communication</string>
|
<string name="login_splash_title">Liberate your communication</string>
|
||||||
<string name="login_splash_text1">Chat with people directly or in groups</string>
|
<string name="login_splash_text1">Chat with people directly or in groups</string>
|
||||||
@ -137,4 +138,6 @@
|
|||||||
<item quantity="other">Too many requests have been sent. You can retry in %1$d seconds…</item>
|
<item quantity="other">Too many requests have been sent. You can retry in %1$d seconds…</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
|
<string name="seen_by">Seen by</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@ -266,6 +266,7 @@
|
|||||||
<item name="android:textColorSecondary">@color/riot_secondary_text_color_dark</item>
|
<item name="android:textColorSecondary">@color/riot_secondary_text_color_dark</item>
|
||||||
<!-- Default color for text View -->
|
<!-- Default color for text View -->
|
||||||
<item name="android:textColorTertiary">@color/riot_tertiary_text_color_dark</item>
|
<item name="android:textColorTertiary">@color/riot_tertiary_text_color_dark</item>
|
||||||
|
<item name="android:textColorLink">@color/riotx_links</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Vector.BottomSheet.Light" parent="Theme.Design.Light.BottomSheetDialog">
|
<style name="Vector.BottomSheet.Light" parent="Theme.Design.Light.BottomSheetDialog">
|
||||||
@ -273,6 +274,7 @@
|
|||||||
<item name="android:textColorSecondary">@color/riot_secondary_text_color_light</item>
|
<item name="android:textColorSecondary">@color/riot_secondary_text_color_light</item>
|
||||||
<!-- Default color for text View -->
|
<!-- Default color for text View -->
|
||||||
<item name="android:textColorTertiary">@color/riot_tertiary_text_color_light</item>
|
<item name="android:textColorTertiary">@color/riot_tertiary_text_color_light</item>
|
||||||
|
<item name="android:textColorLink">@color/riotx_links</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Vector.BottomSheet.Status" parent="Theme.Design.Light.BottomSheetDialog">
|
<style name="Vector.BottomSheet.Status" parent="Theme.Design.Light.BottomSheetDialog">
|
||||||
@ -280,6 +282,7 @@
|
|||||||
<item name="android:textColorSecondary">@color/riot_secondary_text_color_status</item>
|
<item name="android:textColorSecondary">@color/riot_secondary_text_color_status</item>
|
||||||
<!-- Default color for text View -->
|
<!-- Default color for text View -->
|
||||||
<item name="android:textColorTertiary">@color/riot_tertiary_text_color_status</item>
|
<item name="android:textColorTertiary">@color/riot_tertiary_text_color_status</item>
|
||||||
|
<item name="android:textColorLink">@color/link_color_status</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user