Compare commits

..

41 Commits

Author SHA1 Message Date
51a4c93676 Read markers: continue working on ui 2019-08-23 16:54:32 +02:00
d8f449388c Read marker: start working on it (no UI) 2019-08-20 18:30:24 +02:00
8ca829d538 An error was displayed by mistake 2019-08-19 17:22:04 +02:00
e7819ce678 Merge pull request #496 from vector-im/feature/di_clean
Dagger clean
2019-08-19 16:41:50 +02:00
5402902bc2 Merge branch 'develop' into feature/di_clean 2019-08-19 15:04:26 +02:00
bc1350aaf5 Merge pull request #484 from vector-im/feature/timeline_read_receipts
Feature/timeline read receipts
2019-08-19 14:29:59 +02:00
fd74e3dfb1 Read receipts: clean code after review 2019-08-19 14:08:15 +02:00
e0628da1cb Dagger: use AssistedInjectModule for viewModel + use AssistedFactory for room dependencies 2019-08-14 19:09:56 +02:00
aa4e74e986 Merge pull request #487 from vector-im/feature/fix_ui_issues
Feature/fix ui issues
2019-08-14 18:20:08 +02:00
6cc0c0672e Merge pull request #474 from vector-im/feature/dev_suffix
Automatic "-dev" version suffix on non master branch
2019-08-14 18:15:44 +02:00
501474b720 Fix code quality issues 2019-08-14 14:53:40 +02:00
e11c66035c Theme: the action menu text items should use colorAccent 2019-08-14 14:19:21 +02:00
3d2d219d79 Room list: let the fab animation be quicker 2019-08-14 14:18:56 +02:00
63af03bedd List: add overScroll 2019-08-14 14:18:42 +02:00
d3827b8673 Read receipts: branch settings to show/hide them 2019-08-14 10:51:09 +02:00
4ca2531e47 develop branch will have version code from timestamp, to ensure each build from CI has a incremented versionCode
Other branches (master, features, etc.) will have version code based on application version.
2019-08-14 10:45:17 +02:00
4e8dc72439 Update CHANGES 2019-08-13 15:17:04 +02:00
25a4240a5a Merge branch 'develop' into feature/timeline_read_receipts 2019-08-13 15:16:10 +02:00
b9cfda23b6 Read receipts: just juste invisible on hidden avatars, to have a bigger touch zone 2019-08-13 15:06:00 +02:00
06dcf75a32 Read receipts: fix not appearing RR 2019-08-13 12:06:49 +02:00
21deb2551d Read receipts: handle read receipts set on filtered events + let BottomSheet takes a snapshot instead of being live. 2019-08-12 17:59:07 +02:00
70639f180c Read receipts: add read receipts bottom sheet 2019-08-08 19:59:20 +02:00
1dbb02a80d Read receipts: create custom view to use it wherever we want easily 2019-08-08 17:51:06 +02:00
825463d9cd Change package for NotificationAreaView 2019-08-08 17:50:33 +02:00
c313ce78cb Read receipts: sort descending by timestamp 2019-08-08 17:49:50 +02:00
39f58d048b Read receipts: fix dummy being overrided 2019-08-08 17:49:31 +02:00
3f792c7a84 Automatic "-dev" version suffix on non master branch 2019-08-08 16:57:03 +02:00
347dcb469a Version++ 2019-08-08 16:47:13 +02:00
79fb1985aa Merge branch 'release/0.3.0' into develop 2019-08-08 16:45:02 +02:00
e216cd15a8 Prepare release 0.3.0 2019-08-08 16:44:53 +02:00
37fde374b3 Merge pull request #469 from vector-im/feature/versionCode_auto
Ensure versionCode is the wanted one for GPlay and F-Droid build
2019-08-08 16:32:10 +02:00
f7b471f141 Stop using BuildConfig.VERSION_CODE, it is not the correct value 2019-08-08 16:31:45 +02:00
93fd56a7ca Ensure versionCode is the wanted one for GPlay and F-Droid build 2019-08-08 16:30:44 +02:00
5a9d88e791 Merge pull request #473 from vector-im/feature/sync_room
Feature/sync room
2019-08-08 16:15:26 +02:00
eaf6a9923a Cancel sync request on pause and timeout to 0 after pause (#404) 2019-08-08 16:04:53 +02:00
d98567045c Read receipts: use a simpler strategy when it's initialSync 2019-08-08 15:03:36 +02:00
b4ce8748cb First step in handling read receipts 2019-08-08 14:32:11 +02:00
9d5433a857 Show sync progress also in room detail screen (#403) 2019-08-08 14:14:10 +02:00
6d4ee83e65 Merge pull request #472 from vector-im/feature/vectorPref
Dagger for VectorPreferences and /markdown command as a bonus
2019-08-08 12:43:22 +02:00
6e44cca17d Handle /markdown command 2019-08-08 12:09:05 +02:00
0a73887c70 Daggerization of VectorPreferences 2019-08-08 11:52:50 +02:00
141 changed files with 3355 additions and 1715 deletions

View File

@ -1,8 +1,30 @@
Changes in RiotX 0.3.0 (2019-XX-XX)
Changes in RiotX 0.4.0 (2019-XX-XX)
===================================================
Features:
- Display read receipts in timeline (#81)
Improvements:
-
Other changes:
-
Bugfix:
-
Translations:
-
Build:
-
Changes in RiotX 0.3.0 (2019-08-08)
===================================================
Features:
- Create Direct Room flow
- Handle `/markdown` command
Improvements:
- UI for pending edits (#193)
@ -11,9 +33,10 @@ Improvements:
- Enable proper cancellation of suspending functions (including db transaction)
- Enhances network connectivity checks in SDK
- Add "View Edit History" item in the message bottom sheet (#401)
- Cancel sync request on pause and timeout to 0 after pause (#404)
Other changes:
-
- Show sync progress also in room detail screen (#403)
Bugfix:
- Edited message: link confusion when (edited) appears in body (#398)
@ -23,9 +46,6 @@ Bugfix:
- Fix clear cache (#408) and Logout (#205)
- Fix `(edited)` link can be copied to clipboard (#402)
Translations:
-
Build:
- Split APK: generate one APK per arch, to reduce APK size of about 30%

View File

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

View File

@ -18,6 +18,7 @@ package im.vector.matrix.rx
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
import im.vector.matrix.android.api.session.room.model.ReadReceipt
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import io.reactivex.Observable
@ -49,6 +50,10 @@ class RxRoom(private val room: Room) {
room.join(viaServers, MatrixCallbackSingle(it)).toSingle(it)
}
fun liveEventReadReceipts(eventId: String): Observable<List<ReadReceipt>> {
return room.getEventReadReceiptsLive(eventId).asObservable()
}
}
fun Room.rx(): RxRoom {

View File

@ -16,8 +16,9 @@
package im.vector.matrix.android.api.session.room.model
import im.vector.matrix.android.api.session.user.model.User
data class ReadReceipt(
val userId: String,
val eventId: String,
val user: User,
val originServerTs: Long
)

View File

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

View File

@ -0,0 +1,25 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.room.read
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class FullyReadContent(
@Json(name = "event_id") val eventId: String
)

View File

@ -16,7 +16,9 @@
package im.vector.matrix.android.api.session.room.read
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.room.model.ReadReceipt
/**
* This interface defines methods to handle read receipts and read marker in a room. It's implemented at the room level.
@ -39,4 +41,11 @@ interface ReadService {
fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback<Unit>)
fun isEventRead(eventId: String): Boolean
/**
* Returns a nullable read marker for the room.
*/
fun getReadMarkerLive(): LiveData<String?>
fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>>
}

View File

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

View File

@ -20,6 +20,7 @@ import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
import im.vector.matrix.android.api.session.room.model.ReadReceipt
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.isReply
import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply
@ -37,7 +38,9 @@ data class TimelineEvent(
val senderName: String?,
val isUniqueDisplayName: Boolean,
val senderAvatar: String?,
val annotations: EventAnnotationsSummary? = null
val annotations: EventAnnotationsSummary? = null,
val readReceipts: List<ReadReceipt> = emptyList(),
val hasReadMarker: Boolean = false
) {
val metadata = HashMap<String, Any>()
@ -65,8 +68,8 @@ data class TimelineEvent(
"$name (${root.senderId})"
}
}
?: root.senderId
?: ""
?: root.senderId
?: ""
}
/**
@ -94,7 +97,7 @@ fun TimelineEvent.hasBeenEdited() = annotations?.editSummary != null
* Get last MessageContent, after a possible edition
*/
fun TimelineEvent.getLastMessageContent(): MessageContent? = annotations?.editSummary?.aggregatedContent?.toModel()
?: root.getClearContent().toModel()
?: root.getClearContent().toModel()
fun TimelineEvent.getTextEditableContent(): String? {

View File

@ -25,12 +25,12 @@ interface TimelineService {
/**
* Instantiate a [Timeline] with an optional initial eventId, to be used with permalink.
* You can filter the type you want to grab with the allowedTypes param.
* You can also configure some settings with the [settings] param.
* @param eventId the optional initial eventId.
* @param allowedTypes the optional filter types
* @param settings settings to configure the timeline.
* @return the instantiated timeline
*/
fun createTimeline(eventId: String?, allowedTypes: List<String>? = null): Timeline
fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline
fun getTimeLineEvent(eventId: String): TimelineEvent?

View File

@ -0,0 +1,44 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.room.timeline
/**
* Data class holding setting values for a [Timeline] instance.
*/
data class TimelineSettings(
/**
* The initial number of events to retrieve from cache. You might get less events if you don't have loaded enough yet.
*/
val initialSize: Int,
/**
* A flag to filter edit events
*/
val filterEdits: Boolean = false,
/**
* A flag to filter by types. It should be used with [allowedTypes] field
*/
val filterTypes: Boolean = false,
/**
* If [filterTypes] is true, the list of types allowed by the list.
*/
val allowedTypes: List<String> = emptyList(),
/**
* If true, will build read receipts for each event.
*/
val buildReadReceipts: Boolean = true
)

View File

@ -23,9 +23,13 @@ import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.mapper.toEntity
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
import im.vector.matrix.android.internal.database.query.find
import im.vector.matrix.android.internal.database.query.getOrCreate
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.extensions.assertIsManaged
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
@ -133,6 +137,27 @@ internal fun ChunkEntity.add(roomId: String,
}
val localId = TimelineEventEntity.nextId(realm)
val eventId = event.eventId ?: ""
val senderId = event.senderId ?: ""
val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst()
?: ReadReceiptsSummaryEntity(eventId, roomId)
// Update RR for the sender of a new message with a dummy one
if (event.originServerTs != null) {
val timestampOfEvent = event.originServerTs.toDouble()
val readReceiptOfSender = ReadReceiptEntity.getOrCreate(realm, roomId = roomId, userId = senderId)
// If the synced RR is older, update
if (timestampOfEvent > readReceiptOfSender.originServerTs) {
val previousReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId = readReceiptOfSender.eventId).findFirst()
readReceiptOfSender.eventId = eventId
readReceiptOfSender.originServerTs = timestampOfEvent
previousReceiptsSummary?.readReceipts?.remove(readReceiptOfSender)
readReceiptsSummaryEntity.readReceipts.add(readReceiptOfSender)
}
}
val eventEntity = TimelineEventEntity(localId).also {
it.root = event.toEntity(roomId).apply {
this.stateIndex = currentStateIndex
@ -140,9 +165,11 @@ internal fun ChunkEntity.add(roomId: String,
this.displayIndex = currentDisplayIndex
this.sendState = SendState.SYNCED
}
it.eventId = event.eventId ?: ""
it.eventId = eventId
it.roomId = roomId
it.annotations = EventAnnotationsSummaryEntity.where(realm, it.eventId).findFirst()
it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
it.readReceipts = readReceiptsSummaryEntity
it.readMarker = ReadMarkerEntity.where(realm, roomId = roomId, eventId = eventId).findFirst()
}
val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size
timelineEvents.add(position, eventEntity)
@ -150,14 +177,14 @@ internal fun ChunkEntity.add(roomId: String,
internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
return when (direction) {
PaginationDirection.FORWARDS -> forwardsDisplayIndex
PaginationDirection.BACKWARDS -> backwardsDisplayIndex
} ?: defaultValue
PaginationDirection.FORWARDS -> forwardsDisplayIndex
PaginationDirection.BACKWARDS -> backwardsDisplayIndex
} ?: defaultValue
}
internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
return when (direction) {
PaginationDirection.FORWARDS -> forwardsStateIndex
PaginationDirection.BACKWARDS -> backwardsStateIndex
} ?: defaultValue
PaginationDirection.FORWARDS -> forwardsStateIndex
PaginationDirection.BACKWARDS -> backwardsStateIndex
} ?: defaultValue
}

View File

@ -0,0 +1,45 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.database.mapper
import im.vector.matrix.android.api.session.room.model.ReadReceipt
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity
import im.vector.matrix.android.internal.database.model.UserEntity
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.SessionDatabase
import io.realm.Realm
import io.realm.RealmConfiguration
import javax.inject.Inject
internal class ReadReceiptsSummaryMapper @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration) {
fun map(readReceiptsSummaryEntity: ReadReceiptsSummaryEntity?): List<ReadReceipt> {
if (readReceiptsSummaryEntity == null) {
return emptyList()
}
return Realm.getInstance(realmConfiguration).use { realm ->
val readReceipts = readReceiptsSummaryEntity.readReceipts
readReceipts
.mapNotNull {
val user = UserEntity.where(realm, it.userId).findFirst()
?: return@mapNotNull null
ReadReceipt(user.asDomain(), it.originServerTs.toLong())
}
}
}
}

View File

@ -26,7 +26,8 @@ import java.util.*
import javax.inject.Inject
internal class RoomSummaryMapper @Inject constructor(
val cryptoService: CryptoService
private val cryptoService: CryptoService,
private val timelineEventMapper: TimelineEventMapper
) {
fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary {
@ -34,18 +35,20 @@ internal class RoomSummaryMapper @Inject constructor(
RoomTag(it.tagName, it.tagOrder)
}
val latestEvent = roomSummaryEntity.latestEvent?.asDomain()
val latestEvent = roomSummaryEntity.latestEvent?.let {
timelineEventMapper.map(it)
}
if (latestEvent?.root?.isEncrypted() == true && latestEvent.root.mxDecryptionResult == null) {
//TODO use a global event decryptor? attache to session and that listen to new sessionId?
//for now decrypt sync
try {
val result = cryptoService.decryptEvent(latestEvent.root, latestEvent.root.roomId + UUID.randomUUID().toString())
latestEvent.root.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
latestEvent.root.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
} catch (e: MXCryptoError) {
}
@ -62,7 +65,8 @@ internal class RoomSummaryMapper @Inject constructor(
notificationCount = roomSummaryEntity.notificationCount,
tags = tags,
membership = roomSummaryEntity.membership,
versioningState = roomSummaryEntity.versioningState
versioningState = roomSummaryEntity.versioningState,
readMarkerId = roomSummaryEntity.readMarkerId
)
}
}

View File

@ -17,29 +17,39 @@
package im.vector.matrix.android.internal.database.mapper
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.ReadReceipt
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import javax.inject.Inject
internal object TimelineEventMapper {
fun map(timelineEventEntity: TimelineEventEntity): TimelineEvent {
internal class TimelineEventMapper @Inject constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper) {
fun map(timelineEventEntity: TimelineEventEntity, buildReadReceipts: Boolean = true, correctedReadReceipts: List<ReadReceipt>? = null): TimelineEvent {
val readReceipts = if (buildReadReceipts) {
correctedReadReceipts ?: timelineEventEntity.readReceipts
?.let {
readReceiptsSummaryMapper.map(it)
}
} else {
null
}
return TimelineEvent(
root = timelineEventEntity.root?.asDomain()
?: Event("", timelineEventEntity.eventId),
?: Event("", timelineEventEntity.eventId),
annotations = timelineEventEntity.annotations?.asDomain(),
localId = timelineEventEntity.localId,
displayIndex = timelineEventEntity.root?.displayIndex ?: 0,
senderName = timelineEventEntity.senderName,
isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
senderAvatar = timelineEventEntity.senderAvatar
senderAvatar = timelineEventEntity.senderAvatar,
readReceipts = readReceipts?.sortedByDescending {
it.originServerTs
} ?: emptyList(),
hasReadMarker = timelineEventEntity.readMarker?.eventId?.isEmpty() == false
)
}
}
internal fun TimelineEventEntity.asDomain(): TimelineEvent {
return TimelineEventMapper.map(this)
}

View File

@ -0,0 +1,35 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.database.model
import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.annotations.LinkingObjects
import io.realm.annotations.PrimaryKey
internal open class ReadMarkerEntity(
@PrimaryKey
var roomId: String = "",
var eventId: String = ""
) : RealmObject() {
@LinkingObjects("readMarker")
val timelineEvent: RealmResults<TimelineEventEntity>? = null
companion object
}

View File

@ -17,13 +17,18 @@
package im.vector.matrix.android.internal.database.model
import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.annotations.LinkingObjects
import io.realm.annotations.PrimaryKey
internal open class ReadReceiptEntity(@PrimaryKey var primaryKey: String = "",
var userId: String = "",
var eventId: String = "",
var roomId: String = "",
var originServerTs: Double = 0.0
var eventId: String = "",
var roomId: String = "",
var userId: String = "",
var originServerTs: Double = 0.0
) : RealmObject() {
companion object
@LinkingObjects("readReceipts")
val summary: RealmResults<ReadReceiptsSummaryEntity>? = null
}

View File

@ -0,0 +1,37 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.database.model
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.annotations.LinkingObjects
import io.realm.annotations.PrimaryKey
internal open class ReadReceiptsSummaryEntity(
@PrimaryKey
var eventId: String = "",
var roomId: String = "",
var readReceipts: RealmList<ReadReceiptEntity> = RealmList()
) : RealmObject() {
@LinkingObjects("readReceipts")
val timelineEvent: RealmResults<TimelineEventEntity>? = null
companion object
}

View File

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

View File

@ -22,26 +22,28 @@ import io.realm.annotations.RealmModule
* Realm module for Session
*/
@RealmModule(library = true,
classes = [
ChunkEntity::class,
EventEntity::class,
TimelineEventEntity::class,
FilterEntity::class,
GroupEntity::class,
GroupSummaryEntity::class,
ReadReceiptEntity::class,
RoomEntity::class,
RoomSummaryEntity::class,
RoomTagEntity::class,
SyncEntity::class,
UserEntity::class,
EventAnnotationsSummaryEntity::class,
ReactionAggregatedSummaryEntity::class,
EditAggregatedSummaryEntity::class,
PushRulesEntity::class,
PushRuleEntity::class,
PushConditionEntity::class,
PusherEntity::class,
PusherDataEntity::class
])
classes = [
ChunkEntity::class,
EventEntity::class,
TimelineEventEntity::class,
FilterEntity::class,
GroupEntity::class,
GroupSummaryEntity::class,
ReadReceiptEntity::class,
RoomEntity::class,
RoomSummaryEntity::class,
RoomTagEntity::class,
SyncEntity::class,
UserEntity::class,
EventAnnotationsSummaryEntity::class,
ReactionAggregatedSummaryEntity::class,
EditAggregatedSummaryEntity::class,
PushRulesEntity::class,
PushRuleEntity::class,
PushConditionEntity::class,
PusherEntity::class,
PusherDataEntity::class,
ReadReceiptsSummaryEntity::class,
ReadMarkerEntity::class
])
internal class SessionRealmModule

View File

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

View File

@ -0,0 +1,23 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.database.query
internal object FilterContent {
internal const val EDIT_TYPE = """{*"m.relates_to"*"rel_type":*"m.replace"*}"""
}

View File

@ -0,0 +1,37 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.database.query
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
import im.vector.matrix.android.internal.database.model.ReadMarkerEntityFields
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.kotlin.where
internal fun ReadMarkerEntity.Companion.where(realm: Realm, roomId: String, eventId: String? = null): RealmQuery<ReadMarkerEntity> {
val query = realm.where<ReadMarkerEntity>()
.equalTo(ReadMarkerEntityFields.ROOM_ID, roomId)
if (eventId != null) {
query.equalTo(ReadMarkerEntityFields.EVENT_ID, eventId)
}
return query
}
internal fun ReadMarkerEntity.Companion.getOrCreate(realm: Realm, roomId: String): ReadMarkerEntity {
return where(realm, roomId).findFirst()
?: realm.createObject(ReadMarkerEntity::class.java, roomId)
}

View File

@ -20,10 +20,35 @@ import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptEntityFields
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.RealmResults
import io.realm.kotlin.where
internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, userId: String): RealmQuery<ReadReceiptEntity> {
return realm.where<ReadReceiptEntity>()
.equalTo(ReadReceiptEntityFields.ROOM_ID, roomId)
.equalTo(ReadReceiptEntityFields.USER_ID, userId)
}
}
internal fun ReadReceiptEntity.Companion.whereUserId(realm: Realm, userId: String): RealmQuery<ReadReceiptEntity> {
return realm.where<ReadReceiptEntity>()
.equalTo(ReadReceiptEntityFields.USER_ID, userId)
}
internal fun ReadReceiptEntity.Companion.createUnmanaged(roomId: String, eventId: String, userId: String, originServerTs: Double): ReadReceiptEntity {
return ReadReceiptEntity().apply {
this.primaryKey = "${roomId}_$userId"
this.eventId = eventId
this.roomId = roomId
this.userId = userId
this.originServerTs = originServerTs
}
}
internal fun ReadReceiptEntity.Companion.getOrCreate(realm: Realm, roomId: String, userId: String): ReadReceiptEntity {
return ReadReceiptEntity.where(realm, roomId, userId).findFirst()
?: realm.createObject(ReadReceiptEntity::class.java, "${roomId}_$userId").apply {
this.roomId = roomId
this.userId = userId
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.database.query
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntityFields
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.kotlin.where
internal fun ReadReceiptsSummaryEntity.Companion.where(realm: Realm, eventId: String): RealmQuery<ReadReceiptsSummaryEntity> {
return realm.where<ReadReceiptsSummaryEntity>()
.equalTo(ReadReceiptsSummaryEntityFields.EVENT_ID, eventId)
}
internal fun ReadReceiptsSummaryEntity.Companion.whereInRoom(realm: Realm, roomId: String?): RealmQuery<ReadReceiptsSummaryEntity> {
val query = realm.where<ReadReceiptsSummaryEntity>()
if (roomId != null) {
query.equalTo(ReadReceiptsSummaryEntityFields.ROOM_ID, roomId)
}
return query
}

View File

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

View File

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

View File

@ -0,0 +1,24 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.di
import com.squareup.inject.assisted.dagger2.AssistedModule
import dagger.Module
@AssistedModule
@Module(includes = [AssistedInject_SessionAssistedInjectModule::class])
interface SessionAssistedInjectModule

View File

@ -22,6 +22,7 @@ import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.internal.crypto.CryptoModule
import im.vector.matrix.android.internal.di.MatrixComponent
import im.vector.matrix.android.internal.di.SessionAssistedInjectModule
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
import im.vector.matrix.android.internal.session.cache.CacheModule
import im.vector.matrix.android.internal.session.content.ContentModule
@ -59,7 +60,8 @@ import im.vector.matrix.android.internal.task.TaskExecutor
CacheModule::class,
CryptoModule::class,
PushersModule::class,
AccountDataModule::class
AccountDataModule::class,
SessionAssistedInjectModule::class
]
)
@SessionScope

View File

@ -114,7 +114,6 @@ internal abstract class SessionModule {
}
}
@Binds
abstract fun bindSession(session: DefaultSession): Session

View File

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

View File

@ -16,70 +16,46 @@
package im.vector.matrix.android.internal.session.room
import android.content.Context
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper
import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService
import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask
import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask
import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask
import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask
import im.vector.matrix.android.internal.session.room.read.DefaultReadService
import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask
import im.vector.matrix.android.internal.session.room.relation.DefaultRelationService
import im.vector.matrix.android.internal.session.room.relation.FetchEditHistoryTask
import im.vector.matrix.android.internal.session.room.relation.FindReactionEventForUndoTask
import im.vector.matrix.android.internal.session.room.send.DefaultSendService
import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory
import im.vector.matrix.android.internal.session.room.state.DefaultStateService
import im.vector.matrix.android.internal.session.room.state.SendStateTask
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService
import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask
import im.vector.matrix.android.internal.session.room.timeline.PaginationTask
import im.vector.matrix.android.internal.task.TaskExecutor
import javax.inject.Inject
internal class RoomFactory @Inject constructor(private val context: Context,
private val credentials: Credentials,
private val monarchy: Monarchy,
private val eventFactory: LocalEchoEventFactory,
private val roomSummaryMapper: RoomSummaryMapper,
private val taskExecutor: TaskExecutor,
private val loadRoomMembersTask: LoadRoomMembersTask,
private val inviteTask: InviteTask,
private val sendStateTask: SendStateTask,
private val paginationTask: PaginationTask,
private val contextOfEventTask: GetContextOfEventTask,
private val setReadMarkersTask: SetReadMarkersTask,
private val cryptoService: CryptoService,
private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
private val fetchEditHistoryTask: FetchEditHistoryTask,
private val joinRoomTask: JoinRoomTask,
private val leaveRoomTask: LeaveRoomTask) {
fun create(roomId: String): Room {
val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, cryptoService, paginationTask)
val sendService = DefaultSendService(context, credentials, roomId, eventFactory, cryptoService, monarchy)
val stateService = DefaultStateService(roomId, monarchy.realmConfiguration, taskExecutor, sendStateTask)
val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask)
val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask, credentials)
val relationService = DefaultRelationService(context,
credentials, roomId, eventFactory, cryptoService, findReactionEventForUndoTask, fetchEditHistoryTask, monarchy, taskExecutor)
internal interface RoomFactory {
fun create(roomId: String): Room
}
internal class DefaultRoomFactory @Inject constructor(private val monarchy: Monarchy,
private val roomSummaryMapper: RoomSummaryMapper,
private val cryptoService: CryptoService,
private val timelineServiceFactory: DefaultTimelineService.Factory,
private val sendServiceFactory: DefaultSendService.Factory,
private val stateServiceFactory: DefaultStateService.Factory,
private val readServiceFactory: DefaultReadService.Factory,
private val relationServiceFactory: DefaultRelationService.Factory,
private val membershipServiceFactory: DefaultMembershipService.Factory) :
RoomFactory {
override fun create(roomId: String): Room {
return DefaultRoom(
roomId,
monarchy,
roomSummaryMapper,
timelineService,
sendService,
stateService,
readService,
timelineServiceFactory.create(roomId),
sendServiceFactory.create(roomId),
stateServiceFactory.create(roomId),
readServiceFactory.create(roomId),
cryptoService,
relationService,
roomMembersService
relationServiceFactory.create(roomId),
membershipServiceFactory.create(roomId)
)
}

View File

@ -22,12 +22,6 @@ import dagger.Provides
import im.vector.matrix.android.api.session.file.FileService
import im.vector.matrix.android.api.session.room.RoomDirectoryService
import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.api.session.room.members.MembershipService
import im.vector.matrix.android.api.session.room.model.relation.RelationService
import im.vector.matrix.android.api.session.room.read.ReadService
import im.vector.matrix.android.api.session.room.send.SendService
import im.vector.matrix.android.api.session.room.state.StateService
import im.vector.matrix.android.api.session.room.timeline.TimelineService
import im.vector.matrix.android.internal.session.DefaultFileService
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
@ -37,7 +31,6 @@ import im.vector.matrix.android.internal.session.room.directory.DefaultGetThirdP
import im.vector.matrix.android.internal.session.room.directory.GetPublicRoomTask
import im.vector.matrix.android.internal.session.room.directory.GetThirdPartyProtocolsTask
import im.vector.matrix.android.internal.session.room.membership.DefaultLoadRoomMembersTask
import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService
import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask
import im.vector.matrix.android.internal.session.room.membership.joining.DefaultInviteTask
import im.vector.matrix.android.internal.session.room.membership.joining.DefaultJoinRoomTask
@ -47,15 +40,20 @@ import im.vector.matrix.android.internal.session.room.membership.leaving.Default
import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask
import im.vector.matrix.android.internal.session.room.prune.DefaultPruneEventTask
import im.vector.matrix.android.internal.session.room.prune.PruneEventTask
import im.vector.matrix.android.internal.session.room.read.DefaultReadService
import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask
import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask
import im.vector.matrix.android.internal.session.room.relation.*
import im.vector.matrix.android.internal.session.room.send.DefaultSendService
import im.vector.matrix.android.internal.session.room.relation.DefaultFetchEditHistoryTask
import im.vector.matrix.android.internal.session.room.relation.DefaultFindReactionEventForUndoTask
import im.vector.matrix.android.internal.session.room.relation.DefaultUpdateQuickReactionTask
import im.vector.matrix.android.internal.session.room.relation.FetchEditHistoryTask
import im.vector.matrix.android.internal.session.room.relation.FindReactionEventForUndoTask
import im.vector.matrix.android.internal.session.room.relation.UpdateQuickReactionTask
import im.vector.matrix.android.internal.session.room.state.DefaultSendStateTask
import im.vector.matrix.android.internal.session.room.state.DefaultStateService
import im.vector.matrix.android.internal.session.room.state.SendStateTask
import im.vector.matrix.android.internal.session.room.timeline.*
import im.vector.matrix.android.internal.session.room.timeline.DefaultGetContextOfEventTask
import im.vector.matrix.android.internal.session.room.timeline.DefaultPaginationTask
import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask
import im.vector.matrix.android.internal.session.room.timeline.PaginationTask
import retrofit2.Retrofit
@Module
@ -71,6 +69,9 @@ internal abstract class RoomModule {
}
}
@Binds
abstract fun bindRoomFactory(roomFactory: DefaultRoomFactory): RoomFactory
@Binds
abstract fun bindRoomService(roomService: DefaultRoomService): RoomService
@ -98,24 +99,15 @@ internal abstract class RoomModule {
@Binds
abstract fun bindLeaveRoomTask(leaveRoomTask: DefaultLeaveRoomTask): LeaveRoomTask
@Binds
abstract fun bindMembershipService(membershipService: DefaultMembershipService): MembershipService
@Binds
abstract fun bindLoadRoomMembersTask(loadRoomMembersTask: DefaultLoadRoomMembersTask): LoadRoomMembersTask
@Binds
abstract fun bindPruneEventTask(pruneEventTask: DefaultPruneEventTask): PruneEventTask
@Binds
abstract fun bindReadService(readService: DefaultReadService): ReadService
@Binds
abstract fun bindSetReadMarkersTask(setReadMarkersTask: DefaultSetReadMarkersTask): SetReadMarkersTask
@Binds
abstract fun bindRelationService(relationService: DefaultRelationService): RelationService
@Binds
abstract fun bindFindReactionEventForUndoTask(findReactionEventForUndoTask: DefaultFindReactionEventForUndoTask): FindReactionEventForUndoTask
@ -125,21 +117,12 @@ internal abstract class RoomModule {
@Binds
abstract fun bindSendStateTask(sendStateTask: DefaultSendStateTask): SendStateTask
@Binds
abstract fun bindSendService(sendService: DefaultSendService): SendService
@Binds
abstract fun bindStateService(stateService: DefaultStateService): StateService
@Binds
abstract fun bindGetContextOfEventTask(getContextOfEventTask: DefaultGetContextOfEventTask): GetContextOfEventTask
@Binds
abstract fun bindPaginationTask(paginationTask: DefaultPaginationTask): PaginationTask
@Binds
abstract fun bindTimelineService(timelineService: DefaultTimelineService): TimelineService
@Binds
abstract fun bindFileService(fileService: DefaultFileService): FileService

View File

@ -17,6 +17,8 @@
package im.vector.matrix.android.internal.session.room.membership
import androidx.lifecycle.LiveData
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.events.model.toModel
@ -31,17 +33,21 @@ import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRo
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.fetchCopied
import javax.inject.Inject
internal class DefaultMembershipService @Inject constructor(private val roomId: String,
private val monarchy: Monarchy,
private val taskExecutor: TaskExecutor,
private val loadRoomMembersTask: LoadRoomMembersTask,
private val inviteTask: InviteTask,
private val joinTask: JoinRoomTask,
private val leaveRoomTask: LeaveRoomTask
internal class DefaultMembershipService @AssistedInject constructor(@Assisted private val roomId: String,
private val monarchy: Monarchy,
private val taskExecutor: TaskExecutor,
private val loadRoomMembersTask: LoadRoomMembersTask,
private val inviteTask: InviteTask,
private val joinTask: JoinRoomTask,
private val leaveRoomTask: LeaveRoomTask
) : MembershipService {
@AssistedInject.Factory
interface Factory {
fun create(roomId: String): MembershipService
}
override fun loadRoomMembersIfNeeded(matrixCallback: MatrixCallback<Unit>): Cancelable {
val params = LoadRoomMembersTask.Params(roomId, Membership.LEAVE)
return loadRoomMembersTask

View File

@ -16,24 +16,40 @@
package im.vector.matrix.android.internal.session.room.read
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.room.model.ReadReceipt
import im.vector.matrix.android.api.session.room.read.ReadService
import im.vector.matrix.android.internal.database.RealmLiveData
import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.query.find
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import javax.inject.Inject
internal class DefaultReadService @Inject constructor(private val roomId: String,
private val monarchy: Monarchy,
private val taskExecutor: TaskExecutor,
private val setReadMarkersTask: SetReadMarkersTask,
private val credentials: Credentials) : ReadService {
internal class DefaultReadService @AssistedInject constructor(@Assisted private val roomId: String,
private val monarchy: Monarchy,
private val taskExecutor: TaskExecutor,
private val setReadMarkersTask: SetReadMarkersTask,
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper,
private val credentials: Credentials
) : ReadService {
@AssistedInject.Factory
interface Factory {
fun create(roomId: String): ReadService
}
override fun markAllAsRead(callback: MatrixCallback<Unit>) {
val params = SetReadMarkersTask.Params(roomId, markAllAsRead = true)
@ -67,16 +83,37 @@ internal class DefaultReadService @Inject constructor(private val roomId: String
var isEventRead = false
monarchy.doWithRealm {
val readReceipt = ReadReceiptEntity.where(it, roomId, credentials.userId).findFirst()
?: return@doWithRealm
?: return@doWithRealm
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(it, roomId)
?: return@doWithRealm
?: return@doWithRealm
val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex
?: Int.MIN_VALUE
?: Int.MIN_VALUE
val eventToCheckIndex = liveChunk.timelineEvents.find(eventId)?.root?.displayIndex
?: Int.MAX_VALUE
?: Int.MAX_VALUE
isEventRead = eventToCheckIndex <= readReceiptIndex
}
return isEventRead
}
override fun getReadMarkerLive(): LiveData<String?> {
val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm ->
ReadMarkerEntity.where(realm, roomId)
}
return Transformations.map(liveRealmData) { results ->
results.firstOrNull()?.eventId
}
}
override fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>> {
val liveEntity = RealmLiveData(monarchy.realmConfiguration) { realm ->
ReadReceiptsSummaryEntity.where(realm, eventId)
}
return Transformations.map(liveEntity) { realmResults ->
realmResults.firstOrNull()?.let {
readReceiptsSummaryMapper.map(it)
}?.sortedByDescending {
it.originServerTs
} ?: emptyList()
}
}
}

View File

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

View File

@ -19,6 +19,8 @@ import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import androidx.work.OneTimeWorkRequest
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials
@ -45,19 +47,23 @@ import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.CancelableWork
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
import timber.log.Timber
import javax.inject.Inject
internal class DefaultRelationService @Inject constructor(private val context: Context,
private val credentials: Credentials,
private val roomId: String,
private val eventFactory: LocalEchoEventFactory,
private val cryptoService: CryptoService,
private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
private val fetchEditHistoryTask: FetchEditHistoryTask,
private val monarchy: Monarchy,
private val taskExecutor: TaskExecutor)
internal class DefaultRelationService @AssistedInject constructor(@Assisted private val roomId: String,
private val context: Context,
private val credentials: Credentials,
private val eventFactory: LocalEchoEventFactory,
private val cryptoService: CryptoService,
private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
private val fetchEditHistoryTask: FetchEditHistoryTask,
private val monarchy: Monarchy,
private val taskExecutor: TaskExecutor)
: RelationService {
@AssistedInject.Factory
interface Factory {
fun create(roomId: String): RelationService
}
override fun sendReaction(reaction: String, targetEventId: String): Cancelable {
val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction)
.also {
@ -148,9 +154,9 @@ internal class DefaultRelationService @Inject constructor(private val context: C
compatibilityBodyText: String): Cancelable {
val event = eventFactory
.createReplaceTextOfReply(roomId,
replyToEdit,
originalEvent,
newBodyText, true, MessageType.MSGTYPE_TEXT, compatibilityBodyText)
replyToEdit,
originalEvent,
newBodyText, true, MessageType.MSGTYPE_TEXT, compatibilityBodyText)
.also {
saveLocalEcho(it)
}
@ -214,7 +220,7 @@ internal class DefaultRelationService @Inject constructor(private val context: C
}
return Transformations.map(liveEntity) { realmResults ->
realmResults.firstOrNull()?.asDomain()
?: EventAnnotationsSummary(eventId, emptyList(), null)
?: EventAnnotationsSummary(eventId, emptyList(), null)
}
}
@ -227,7 +233,7 @@ internal class DefaultRelationService @Inject constructor(private val context: C
private fun saveLocalEcho(event: Event) {
monarchy.writeAsync { realm ->
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst()
?: return@writeAsync
?: return@writeAsync
roomEntity.addSendingEvent(event)
}
}

View File

@ -17,12 +17,22 @@
package im.vector.matrix.android.internal.session.room.send
import android.content.Context
import androidx.work.*
import androidx.work.BackoffPolicy
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.Operation
import androidx.work.WorkManager
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.events.model.*
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.isImageMessage
import im.vector.matrix.android.api.session.events.model.isTextMessage
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.send.SendService
@ -47,18 +57,22 @@ import im.vector.matrix.android.internal.worker.startChain
import timber.log.Timber
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import javax.inject.Inject
private const val UPLOAD_WORK = "UPLOAD_WORK"
private const val BACKOFF_DELAY = 10_000L
internal class DefaultSendService @Inject constructor(private val context: Context,
private val credentials: Credentials,
private val roomId: String,
private val localEchoEventFactory: LocalEchoEventFactory,
private val cryptoService: CryptoService,
private val monarchy: Monarchy)
: SendService {
internal class DefaultSendService @AssistedInject constructor(@Assisted private val roomId: String,
private val context: Context,
private val credentials: Credentials,
private val localEchoEventFactory: LocalEchoEventFactory,
private val cryptoService: CryptoService,
private val monarchy: Monarchy
) : SendService {
@AssistedInject.Factory
interface Factory {
fun create(roomId: String): SendService
}
private val workerFutureListenerExecutor = Executors.newSingleThreadExecutor()
override fun sendTextMessage(text: String, msgType: String, autoMarkdown: Boolean): Cancelable {
@ -152,11 +166,11 @@ internal class DefaultSendService @Inject constructor(private val context: Conte
override fun deleteFailedEcho(localEcho: TimelineEvent) {
monarchy.writeAsync { realm ->
TimelineEventEntity.where(realm, eventId = localEcho.root.eventId
?: "").findFirst()?.let {
?: "").findFirst()?.let {
it.deleteFromRealm()
}
EventEntity.where(realm, eventId = localEcho.root.eventId
?: "").findFirst()?.let {
?: "").findFirst()?.let {
it.deleteFromRealm()
}
}

View File

@ -16,6 +16,8 @@
package im.vector.matrix.android.internal.session.room.state
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
@ -29,13 +31,18 @@ import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import io.realm.Realm
import io.realm.RealmConfiguration
import javax.inject.Inject
internal class DefaultStateService @Inject constructor(private val roomId: String,
@SessionDatabase
private val realmConfiguration: RealmConfiguration,
private val taskExecutor: TaskExecutor,
private val sendStateTask: SendStateTask) : StateService {
internal class DefaultStateService @AssistedInject constructor(@Assisted private val roomId: String,
@SessionDatabase
private val realmConfiguration: RealmConfiguration,
private val taskExecutor: TaskExecutor,
private val sendStateTask: SendStateTask
) : StateService {
@AssistedInject.Factory
interface Factory {
fun create(roomId: String): StateService
}
override fun getStateEvent(eventType: String): Event? {
return Realm.getInstance(realmConfiguration).use { realm ->
@ -45,10 +52,10 @@ internal class DefaultStateService @Inject constructor(private val roomId: Strin
override fun updateTopic(topic: String, callback: MatrixCallback<Unit>) {
val params = SendStateTask.Params(roomId,
EventType.STATE_ROOM_TOPIC,
mapOf(
"topic" to topic
))
EventType.STATE_ROOM_TOPIC,
mapOf(
"topic" to topic
))
sendStateTask

View File

@ -19,13 +19,15 @@ package im.vector.matrix.android.internal.session.room.timeline
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.model.ReadReceipt
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
import im.vector.matrix.android.api.util.CancelableBag
import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.*
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.query.*
import im.vector.matrix.android.internal.task.TaskConstraints
import im.vector.matrix.android.internal.task.TaskExecutor
@ -42,20 +44,22 @@ import kotlin.collections.ArrayList
import kotlin.collections.HashMap
private const val INITIAL_LOAD_SIZE = 30
private const val MIN_FETCHING_COUNT = 30
private const val DISPLAY_INDEX_UNKNOWN = Int.MIN_VALUE
internal class DefaultTimeline(
private val roomId: String,
private val initialEventId: String? = null,
private var initialEventId: String? = null,
private val realmConfiguration: RealmConfiguration,
private val taskExecutor: TaskExecutor,
private val contextOfEventTask: GetContextOfEventTask,
private val paginationTask: PaginationTask,
cryptoService: CryptoService,
private val allowedTypes: List<String>?
) : Timeline {
private val cryptoService: CryptoService,
private val timelineEventMapper: TimelineEventMapper,
private val settings: TimelineSettings,
private val hiddenReadReceipts: TimelineHiddenReadReceipts,
private val hiddenReadMarker: TimelineHiddenReadMarker
) : Timeline, TimelineHiddenReadReceipts.Delegate, TimelineHiddenReadMarker.Delegate {
private companion object {
val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD")
@ -77,20 +81,21 @@ internal class DefaultTimeline(
private val debouncer = Debouncer(mainHandler)
private lateinit var liveEvents: RealmResults<TimelineEventEntity>
private lateinit var eventRelations: RealmResults<EventAnnotationsSummaryEntity>
private var roomEntity: RoomEntity? = null
private var prevDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN
private var nextDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN
private val isLive = initialEventId == null
private val builtEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
private val builtEventsIdMap = Collections.synchronizedMap(HashMap<String, Int>())
private val backwardsPaginationState = AtomicReference(PaginationState())
private val forwardsPaginationState = AtomicReference(PaginationState())
private val timelineID = UUID.randomUUID().toString()
private lateinit var eventRelations: RealmResults<EventAnnotationsSummaryEntity>
override val isLive
get() = initialEventId == null
private val eventDecryptor = TimelineEventDecryptor(realmConfiguration, timelineID, cryptoService)
@ -100,10 +105,7 @@ internal class DefaultTimeline(
} else {
// If changeSet has deletion we are having a gap, so we clear everything
if (changeSet.deletionRanges.isNotEmpty()) {
prevDisplayIndex = DISPLAY_INDEX_UNKNOWN
nextDisplayIndex = DISPLAY_INDEX_UNKNOWN
builtEvents.clear()
builtEventsIdMap.clear()
clearAllValues()
}
changeSet.insertionRanges.forEach { range ->
val (startDisplayIndex, direction) = if (range.startIndex == 0) {
@ -129,13 +131,9 @@ internal class DefaultTimeline(
changeSet.changes.forEach { index ->
val eventEntity = results[index]
eventEntity?.eventId?.let { eventId ->
builtEventsIdMap[eventId]?.let { builtIndex ->
//Update the relation of existing event
builtEvents[builtIndex]?.let { te ->
builtEvents[builtIndex] = eventEntity.asDomain()
hasChanged = true
}
}
hasChanged = rebuildEvent(eventId) {
buildTimelineEvent(eventEntity)
} || hasChanged
}
}
if (hasChanged) postSnapshot()
@ -143,51 +141,19 @@ internal class DefaultTimeline(
}
private val relationsListener = OrderedRealmCollectionChangeListener<RealmResults<EventAnnotationsSummaryEntity>> { collection, changeSet ->
var hasChange = false
(changeSet.insertions + changeSet.changes).forEach {
val eventRelations = collection[it]
if (eventRelations != null) {
builtEventsIdMap[eventRelations.eventId]?.let { builtIndex ->
//Update the relation of existing event
builtEvents[builtIndex]?.let { te ->
builtEvents[builtIndex] = te.copy(annotations = eventRelations.asDomain())
hasChange = true
}
}
hasChange = rebuildEvent(eventRelations.eventId) { te ->
te.copy(annotations = eventRelations.asDomain())
} || hasChange
}
}
if (hasChange)
postSnapshot()
if (hasChange) postSnapshot()
}
// private val newSessionListener = object : NewSessionListener {
// override fun onNewSession(roomId: String?, senderKey: String, sessionId: String) {
// if (roomId == this@DefaultTimeline.roomId) {
// Timber.v("New session id detected for this room")
// BACKGROUND_HANDLER.post {
// val realm = backgroundRealm.get()
// var hasChange = false
// builtEvents.forEachIndexed { index, timelineEvent ->
// if (timelineEvent.isEncrypted()) {
// val eventContent = timelineEvent.root.content.toModel<EncryptedEventContent>()
// if (eventContent?.sessionId == sessionId
// && (timelineEvent.root.mClearEvent == null || timelineEvent.root.mCryptoError != null)) {
// //we need to rebuild this event
// EventEntity.where(realm, eventId = timelineEvent.root.eventId!!).findFirst()?.let {
// //builtEvents[index] = timelineEventFactory.create(it, realm)
// hasChange = true
// }
// }
// }
// }
// if (hasChange) postSnapshot()
// }
// }
// }
//
// }
// Public methods ******************************************************************************
@ -234,15 +200,20 @@ internal class DefaultTimeline(
}
liveEvents = buildEventQuery(realm)
.filterEventsWithSettings()
.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING)
.findAllAsync()
.also { it.addChangeListener(eventsChangeListener) }
isReady.set(true)
eventRelations = EventAnnotationsSummaryEntity.whereInRoom(realm, roomId)
.findAllAsync()
.also { it.addChangeListener(relationsListener) }
if (settings.buildReadReceipts) {
hiddenReadReceipts.start(realm, liveEvents, this)
}
hiddenReadMarker.start(realm, liveEvents, this)
isReady.set(true)
}
}
}
@ -256,6 +227,11 @@ internal class DefaultTimeline(
roomEntity?.sendingTimelineEvents?.removeAllChangeListeners()
eventRelations.removeAllChangeListeners()
liveEvents.removeAllChangeListeners()
hiddenReadMarker.dispose()
if (settings.buildReadReceipts) {
hiddenReadReceipts.dispose()
}
clearAllValues()
backgroundRealm.getAndSet(null).also {
it.close()
}
@ -263,16 +239,71 @@ internal class DefaultTimeline(
}
}
override fun restartWithEventId(eventId: String) {
dispose()
initialEventId = eventId
start()
postSnapshot()
}
override fun getIndexOfEvent(eventId: String?): Int? {
return builtEventsIdMap[eventId]
}
override fun getTimelineEventAtIndex(index: Int): TimelineEvent? {
return builtEvents.getOrNull(index)
}
override fun getTimelineEventWithId(eventId: String?): TimelineEvent? {
return builtEventsIdMap[eventId]?.let {
getTimelineEventAtIndex(it)
}
}
override fun hasMoreToLoad(direction: Timeline.Direction): Boolean {
return hasMoreInCache(direction) || !hasReachedEnd(direction)
}
// Private methods *****************************************************************************
// TimelineHiddenReadReceipts.Delegate
override fun rebuildEvent(eventId: String, readReceipts: List<ReadReceipt>): Boolean {
return rebuildEvent(eventId) { te ->
te.copy(readReceipts = readReceipts)
}
}
override fun onReadReceiptsUpdated() {
postSnapshot()
}
// TimelineHiddenReadMarker.Delegate
override fun rebuildEvent(eventId: String, hasReadMarker: Boolean): Boolean {
return rebuildEvent(eventId) { te ->
te.copy(hasReadMarker = hasReadMarker)
}
}
override fun onReadMarkerUpdated() {
postSnapshot()
}
// Private methods *****************************************************************************
private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean {
return builtEventsIdMap[eventId]?.let { builtIndex ->
//Update the relation of existing event
builtEvents[builtIndex]?.let { te ->
builtEvents[builtIndex] = builder(te)
true
}
} ?: false
}
private fun hasMoreInCache(direction: Timeline.Direction): Boolean {
return Realm.getInstance(realmConfiguration).use { localRealm ->
val timelineEventEntity = buildEventQuery(localRealm).findFirst(direction)
?: return false
?: return false
if (direction == Timeline.Direction.FORWARDS) {
if (findCurrentChunk(localRealm)?.isLastForward == true) {
return false
@ -329,9 +360,11 @@ internal class DefaultTimeline(
val sendingEvents = ArrayList<TimelineEvent>()
if (hasReachedEnd(Timeline.Direction.FORWARDS)) {
roomEntity?.sendingTimelineEvents
?.filter { allowedTypes?.contains(it.root?.type) ?: false }
?.where()
?.filterEventsWithSettings()
?.findAll()
?.forEach {
sendingEvents.add(it.asDomain())
sendingEvents.add(timelineEventMapper.map(it))
}
}
return sendingEvents
@ -375,10 +408,11 @@ internal class DefaultTimeline(
prevDisplayIndex = initialDisplayIndex
nextDisplayIndex = initialDisplayIndex
if (initialEventId != null && shouldFetchInitialEvent) {
fetchEvent(initialEventId)
val currentInitialEventId = initialEventId
if (currentInitialEventId != null && shouldFetchInitialEvent) {
fetchEvent(currentInitialEventId)
} else {
val count = Math.min(INITIAL_LOAD_SIZE, liveEvents.size)
val count = Math.min(settings.initialSize, liveEvents.size)
if (isLive) {
paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count)
} else {
@ -395,9 +429,9 @@ internal class DefaultTimeline(
private fun executePaginationTask(direction: Timeline.Direction, limit: Int) {
val token = getTokenLive(direction) ?: return
val params = PaginationTask.Params(roomId = roomId,
from = token,
direction = direction.toPaginationDirection(),
limit = limit)
from = token,
direction = direction.toPaginationDirection(),
limit = limit)
Timber.v("Should fetch $limit items $direction")
cancelableBag += paginationTask
@ -463,10 +497,11 @@ internal class DefaultTimeline(
nextDisplayIndex = offsetIndex + 1
}
offsetResults.forEach { eventEntity ->
val timelineEvent = eventEntity.asDomain()
val timelineEvent = buildTimelineEvent(eventEntity)
if (timelineEvent.isEncrypted()
&& timelineEvent.root.mxDecryptionResult == null) {
&& timelineEvent.root.mxDecryptionResult == null) {
timelineEvent.root.eventId?.let { eventDecryptor.requestDecryption(it) }
}
@ -481,6 +516,12 @@ internal class DefaultTimeline(
return offsetResults.size
}
private fun buildTimelineEvent(eventEntity: TimelineEventEntity) = timelineEventMapper.map(
timelineEventEntity = eventEntity,
buildReadReceipts = settings.buildReadReceipts,
correctedReadReceipts = hiddenReadReceipts.correctedReadReceipts(eventEntity.eventId)
)
/**
* This has to be called on TimelineThread as it access realm live results
*/
@ -498,7 +539,6 @@ internal class DefaultTimeline(
.greaterThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex)
}
return offsetQuery
.filterAllowedTypes()
.limit(count)
.findAll()
}
@ -517,10 +557,11 @@ internal class DefaultTimeline(
}
private fun findCurrentChunk(realm: Realm): ChunkEntity? {
return if (initialEventId == null) {
val currentInitialEventId = initialEventId
return if (currentInitialEventId == null) {
ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
} else {
ChunkEntity.findIncludingEvent(realm, initialEventId)
ChunkEntity.findIncludingEvent(realm, currentInitialEventId)
}
}
@ -540,11 +581,23 @@ internal class DefaultTimeline(
}
private fun postSnapshot() {
val snapshot = createSnapshot()
val runnable = Runnable { listener?.onUpdated(snapshot) }
debouncer.debounce("post_snapshot", runnable, 50)
BACKGROUND_HANDLER.post {
val snapshot = createSnapshot()
val runnable = Runnable { listener?.onUpdated(snapshot) }
debouncer.debounce("post_snapshot", runnable, 50)
}
}
private fun clearAllValues() {
prevDisplayIndex = DISPLAY_INDEX_UNKNOWN
nextDisplayIndex = DISPLAY_INDEX_UNKNOWN
builtEvents.clear()
builtEventsIdMap.clear()
backwardsPaginationState.set(PaginationState())
forwardsPaginationState.set(PaginationState())
}
// Extension methods ***************************************************************************
private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {
@ -557,16 +610,20 @@ internal class DefaultTimeline(
} else {
sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.ASCENDING)
}
.filterAllowedTypes()
.filterEventsWithSettings()
.findFirst()
}
private fun RealmQuery<TimelineEventEntity>.filterAllowedTypes(): RealmQuery<TimelineEventEntity> {
if (allowedTypes != null) {
`in`(TimelineEventEntityFields.ROOT.TYPE, allowedTypes.toTypedArray())
private fun RealmQuery<TimelineEventEntity>.filterEventsWithSettings(): RealmQuery<TimelineEventEntity> {
if (settings.filterTypes) {
`in`(TimelineEventEntityFields.ROOT.TYPE, settings.allowedTypes.toTypedArray())
}
if (settings.filterEdits) {
not().like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.EDIT_TYPE)
}
return this
}
}
private data class PaginationState(

View File

@ -18,28 +18,38 @@ package im.vector.matrix.android.internal.session.room.timeline
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.TimelineService
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
import im.vector.matrix.android.internal.database.RealmLiveData
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper
import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.fetchCopyMap
import javax.inject.Inject
internal class DefaultTimelineService @Inject constructor(private val roomId: String,
private val monarchy: Monarchy,
private val taskExecutor: TaskExecutor,
private val contextOfEventTask: GetContextOfEventTask,
private val cryptoService: CryptoService,
private val paginationTask: PaginationTask
internal class DefaultTimelineService @AssistedInject constructor(@Assisted private val roomId: String,
private val monarchy: Monarchy,
private val taskExecutor: TaskExecutor,
private val contextOfEventTask: GetContextOfEventTask,
private val cryptoService: CryptoService,
private val paginationTask: PaginationTask,
private val timelineEventMapper: TimelineEventMapper,
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper
) : TimelineService {
override fun createTimeline(eventId: String?, allowedTypes: List<String>?): Timeline {
@AssistedInject.Factory
interface Factory {
fun create(roomId: String): TimelineService
}
override fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline {
return DefaultTimeline(roomId,
eventId,
monarchy.realmConfiguration,
@ -47,7 +57,11 @@ internal class DefaultTimelineService @Inject constructor(private val roomId: St
contextOfEventTask,
paginationTask,
cryptoService,
allowedTypes)
timelineEventMapper,
settings,
TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings),
TimelineHiddenReadMarker(roomId)
)
}
override fun getTimeLineEvent(eventId: String): TimelineEvent? {
@ -55,7 +69,7 @@ internal class DefaultTimelineService @Inject constructor(private val roomId: St
.fetchCopyMap({
TimelineEventEntity.where(it, eventId = eventId).findFirst()
}, { entity, realm ->
entity.asDomain()
timelineEventMapper.map(entity)
})
}
@ -63,8 +77,8 @@ internal class DefaultTimelineService @Inject constructor(private val roomId: St
val liveData = RealmLiveData(monarchy.realmConfiguration) {
TimelineEventEntity.where(it, eventId = eventId)
}
return Transformations.map(liveData) {
it.firstOrNull()?.asDomain()
return Transformations.map(liveData) { events ->
events.firstOrNull()?.let { timelineEventMapper.map(it) }
}
}

View File

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

View File

@ -0,0 +1,153 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.room.timeline
import android.util.SparseArray
import im.vector.matrix.android.api.session.room.model.ReadReceipt
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntityFields
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
import im.vector.matrix.android.internal.database.query.FilterContent
import im.vector.matrix.android.internal.database.query.whereInRoom
import io.realm.OrderedRealmCollectionChangeListener
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.RealmResults
/**
* This class is responsible for handling the read receipts for hidden events (check [TimelineSettings] to see filtering).
* When an hidden event has read receipts, we want to transfer these read receipts on the first older displayed event.
* It has to be used in [DefaultTimeline] and we should call the [start] and [dispose] methods to properly handle realm subscription.
*/
internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper,
private val roomId: String,
private val settings: TimelineSettings) {
interface Delegate {
fun rebuildEvent(eventId: String, readReceipts: List<ReadReceipt>): Boolean
fun onReadReceiptsUpdated()
}
private val correctedReadReceiptsEventByIndex = SparseArray<String>()
private val correctedReadReceiptsByEvent = HashMap<String, MutableList<ReadReceipt>>()
private lateinit var hiddenReadReceipts: RealmResults<ReadReceiptsSummaryEntity>
private lateinit var liveEvents: RealmResults<TimelineEventEntity>
private lateinit var delegate: Delegate
private val hiddenReadReceiptsListener = OrderedRealmCollectionChangeListener<RealmResults<ReadReceiptsSummaryEntity>> { collection, changeSet ->
var hasChange = false
// Deletion here means we don't have any readReceipts for the given hidden events
changeSet.deletions.forEach {
val eventId = correctedReadReceiptsEventByIndex[it]
val timelineEvent = liveEvents.where()
.equalTo(TimelineEventEntityFields.EVENT_ID, eventId)
.findFirst()
// We are rebuilding the corresponding event with only his own RR
val readReceipts = readReceiptsSummaryMapper.map(timelineEvent?.readReceipts)
hasChange = delegate.rebuildEvent(eventId, readReceipts) || hasChange
}
correctedReadReceiptsEventByIndex.clear()
correctedReadReceiptsByEvent.clear()
hiddenReadReceipts.forEachIndexed { index, summary ->
val timelineEvent = summary?.timelineEvent?.firstOrNull()
val displayIndex = timelineEvent?.root?.displayIndex
if (displayIndex != null) {
// Then we are looking for the first displayable event after the hidden one
val firstDisplayedEvent = liveEvents.where()
.lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex)
.findFirst()
// If we find one, we should
if (firstDisplayedEvent != null) {
correctedReadReceiptsEventByIndex.put(index, firstDisplayedEvent.eventId)
correctedReadReceiptsByEvent
.getOrPut(firstDisplayedEvent.eventId, {
ArrayList(readReceiptsSummaryMapper.map(firstDisplayedEvent.readReceipts))
})
.addAll(readReceiptsSummaryMapper.map(summary))
}
}
}
if (correctedReadReceiptsByEvent.isNotEmpty()) {
correctedReadReceiptsByEvent.forEach { (eventId, correctedReadReceipts) ->
val sortedReadReceipts = correctedReadReceipts.sortedByDescending {
it.originServerTs
}
hasChange = delegate.rebuildEvent(eventId, sortedReadReceipts) || hasChange
}
}
if (hasChange) {
delegate.onReadReceiptsUpdated()
}
}
/**
* Start the realm query subscription. Has to be called on an HandlerThread
*/
fun start(realm: Realm, liveEvents: RealmResults<TimelineEventEntity>, delegate: Delegate) {
this.liveEvents = liveEvents
this.delegate = delegate
// We are looking for read receipts set on hidden events.
// We only accept those with a timelineEvent (so coming from pagination/sync).
this.hiddenReadReceipts = ReadReceiptsSummaryEntity.whereInRoom(realm, roomId)
.isNotEmpty(ReadReceiptsSummaryEntityFields.TIMELINE_EVENT)
.isNotEmpty(ReadReceiptsSummaryEntityFields.READ_RECEIPTS.`$`)
.filterReceiptsWithSettings()
.findAllAsync()
.also { it.addChangeListener(hiddenReadReceiptsListener) }
}
/**
* Dispose the realm query subscription. Has to be called on an HandlerThread
*/
fun dispose() {
this.hiddenReadReceipts.removeAllChangeListeners()
}
/**
* Return the current corrected [ReadReceipt] list for an event, or null
*/
fun correctedReadReceipts(eventId: String?): List<ReadReceipt>? {
return correctedReadReceiptsByEvent[eventId]
}
/**
* We are looking for receipts related to filtered events. So, it's the opposite of [DefaultTimeline.filterEventsWithSettings] method.
*/
private fun RealmQuery<ReadReceiptsSummaryEntity>.filterReceiptsWithSettings(): RealmQuery<ReadReceiptsSummaryEntity> {
beginGroup()
if (settings.filterTypes) {
not().`in`("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.TYPE}", settings.allowedTypes.toTypedArray())
}
if (settings.filterTypes && settings.filterEdits) {
or()
}
if (settings.filterEdits) {
like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", FilterContent.EDIT_TYPE)
}
endGroup()
return this
}
}

View File

@ -17,6 +17,10 @@
package im.vector.matrix.android.internal.session.sync
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity
import im.vector.matrix.android.internal.database.query.createUnmanaged
import im.vector.matrix.android.internal.database.query.getOrCreate
import im.vector.matrix.android.internal.database.query.where
import io.realm.Realm
import timber.log.Timber
import javax.inject.Inject
@ -29,34 +33,70 @@ import javax.inject.Inject
// dict value ts value
typealias ReadReceiptContent = Map<String, Map<String, Map<String, Map<String, Double>>>>
private const val READ_KEY = "m.read"
private const val TIMESTAMP_KEY = "ts"
internal class ReadReceiptHandler @Inject constructor() {
fun handle(realm: Realm, roomId: String, content: ReadReceiptContent?) {
fun handle(realm: Realm, roomId: String, content: ReadReceiptContent?, isInitialSync: Boolean) {
if (content == null) {
return
}
try {
val readReceipts = mapContentToReadReceiptEntities(roomId, content)
realm.insertOrUpdate(readReceipts)
handleReadReceiptContent(realm, roomId, content, isInitialSync)
} catch (exception: Exception) {
Timber.e("Fail to handle read receipt for room $roomId")
}
}
private fun mapContentToReadReceiptEntities(roomId: String, content: ReadReceiptContent): List<ReadReceiptEntity> {
return content
.flatMap { (eventId, receiptDict) ->
receiptDict
.filterKeys { it == "m.read" }
.flatMap { (_, userIdsDict) ->
userIdsDict.map { (userId, paramsDict) ->
val ts = paramsDict.filterKeys { it == "ts" }
.values
.firstOrNull() ?: 0.0
val primaryKey = roomId + userId
ReadReceiptEntity(primaryKey, userId, eventId, roomId, ts)
}
}
}
private fun handleReadReceiptContent(realm: Realm, roomId: String, content: ReadReceiptContent, isInitialSync: Boolean) {
if (isInitialSync) {
initialSyncStrategy(realm, roomId, content)
} else {
incrementalSyncStrategy(realm, roomId, content)
}
}
private fun initialSyncStrategy(realm: Realm, roomId: String, content: ReadReceiptContent) {
val readReceiptSummaries = ArrayList<ReadReceiptsSummaryEntity>()
for ((eventId, receiptDict) in content) {
val userIdsDict = receiptDict[READ_KEY] ?: continue
val readReceiptsSummary = ReadReceiptsSummaryEntity(eventId = eventId, roomId = roomId)
for ((userId, paramsDict) in userIdsDict) {
val ts = paramsDict[TIMESTAMP_KEY] ?: 0.0
val receiptEntity = ReadReceiptEntity.createUnmanaged(roomId, eventId, userId, ts)
readReceiptsSummary.readReceipts.add(receiptEntity)
}
readReceiptSummaries.add(readReceiptsSummary)
}
realm.insertOrUpdate(readReceiptSummaries)
}
private fun incrementalSyncStrategy(realm: Realm, roomId: String, content: ReadReceiptContent) {
for ((eventId, receiptDict) in content) {
val userIdsDict = receiptDict[READ_KEY] ?: continue
val readReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst()
?: realm.createObject(ReadReceiptsSummaryEntity::class.java, eventId).apply {
this.roomId = roomId
}
for ((userId, paramsDict) in userIdsDict) {
val ts = paramsDict[TIMESTAMP_KEY] ?: 0.0
val receiptEntity = ReadReceiptEntity.getOrCreate(realm, roomId, userId)
// ensure new ts is superior to the previous one
if (ts > receiptEntity.originServerTs) {
ReadReceiptsSummaryEntity.where(realm, receiptEntity.eventId).findFirst()?.also {
it.readReceipts.remove(receiptEntity)
}
receiptEntity.eventId = eventId
receiptEntity.originServerTs = ts
readReceiptsSummary.readReceipts.add(receiptEntity)
}
}
}
}
}

View File

@ -0,0 +1,51 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.sync
import im.vector.matrix.android.api.session.room.read.FullyReadContent
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.query.getOrCreate
import im.vector.matrix.android.internal.database.query.where
import io.realm.Realm
import timber.log.Timber
import javax.inject.Inject
internal class RoomFullyReadHandler @Inject constructor() {
fun handle(realm: Realm, roomId: String, content: FullyReadContent?) {
if (content == null) {
return
}
Timber.v("Handle for roomId: $roomId eventId: ${content.eventId}")
RoomSummaryEntity.getOrCreate(realm, roomId).apply {
readMarkerId = content.eventId
}
val readMarkerEntity = ReadMarkerEntity.getOrCreate(realm, roomId).apply {
eventId = content.eventId
}
// Remove the old marker if any
readMarkerEntity.timelineEvent?.firstOrNull()?.readMarker = null
// Attach to timelineEvent if known
val timelineEventEntity = TimelineEventEntity.where(realm, eventId = content.eventId).findFirst()
timelineEventEntity?.readMarker = readMarkerEntity
}
}

View File

@ -23,8 +23,13 @@ import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.tag.RoomTagContent
import im.vector.matrix.android.api.session.room.read.FullyReadContent
import im.vector.matrix.android.internal.crypto.CryptoManager
import im.vector.matrix.android.internal.database.helper.*
import im.vector.matrix.android.internal.database.helper.add
import im.vector.matrix.android.internal.database.helper.addOrUpdate
import im.vector.matrix.android.internal.database.helper.addStateEvent
import im.vector.matrix.android.internal.database.helper.lastStateIndex
import im.vector.matrix.android.internal.database.helper.updateSenderDataFor
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.model.RoomEntity
@ -37,7 +42,11 @@ import im.vector.matrix.android.internal.session.notification.DefaultPushRuleSer
import im.vector.matrix.android.internal.session.notification.ProcessEventForPushTask
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import im.vector.matrix.android.internal.session.sync.model.*
import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync
import im.vector.matrix.android.internal.session.sync.model.RoomSync
import im.vector.matrix.android.internal.session.sync.model.RoomSyncAccountData
import im.vector.matrix.android.internal.session.sync.model.RoomSyncEphemeral
import im.vector.matrix.android.internal.session.sync.model.RoomsSyncResponse
import im.vector.matrix.android.internal.session.user.UserEntityFactory
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
@ -50,6 +59,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
private val readReceiptHandler: ReadReceiptHandler,
private val roomSummaryUpdater: RoomSummaryUpdater,
private val roomTagHandler: RoomTagHandler,
private val roomFullyReadHandler: RoomFullyReadHandler,
private val cryptoManager: CryptoManager,
private val tokenStore: SyncTokenStore,
private val pushRuleService: DefaultPushRuleService,
@ -62,11 +72,11 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
data class LEFT(val data: Map<String, RoomSync>) : HandlingStrategy()
}
fun handle(roomsSyncResponse: RoomsSyncResponse, reporter: DefaultInitialSyncProgressService? = null) {
fun handle(roomsSyncResponse: RoomsSyncResponse, isInitialSync: Boolean, reporter: DefaultInitialSyncProgressService? = null) {
monarchy.runTransactionSync { realm ->
handleRoomSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), reporter)
handleRoomSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), reporter)
handleRoomSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), reporter)
handleRoomSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), isInitialSync, reporter)
handleRoomSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), isInitialSync, reporter)
handleRoomSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), isInitialSync, reporter)
}
//handle event for bing rule checks
@ -89,12 +99,12 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
// PRIVATE METHODS *****************************************************************************
private fun handleRoomSync(realm: Realm, handlingStrategy: HandlingStrategy, reporter: DefaultInitialSyncProgressService?) {
private fun handleRoomSync(realm: Realm, handlingStrategy: HandlingStrategy, isInitialSync: Boolean, reporter: DefaultInitialSyncProgressService?) {
val rooms = when (handlingStrategy) {
is HandlingStrategy.JOINED ->
handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_joined_rooms, 0.6f) {
handleJoinedRoom(realm, it.key, it.value)
handleJoinedRoom(realm, it.key, it.value, isInitialSync)
}
is HandlingStrategy.INVITED ->
handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_invited_rooms, 0.4f) {
@ -112,12 +122,21 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
private fun handleJoinedRoom(realm: Realm,
roomId: String,
roomSync: RoomSync): RoomEntity {
roomSync: RoomSync,
isInitalSync: Boolean): RoomEntity {
Timber.v("Handle join sync for room $roomId")
if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) {
handleEphemeral(realm, roomId, roomSync.ephemeral, isInitalSync)
}
if (roomSync.accountData != null && roomSync.accountData.events.isNullOrEmpty().not()) {
handleRoomAccountDataEvents(realm, roomId, roomSync.accountData)
}
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId)
?: realm.createObject(roomId)
if (roomEntity.membership == Membership.INVITE) {
roomEntity.chunks.deleteAllFromRealm()
@ -127,7 +146,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
// State event
if (roomSync.state != null && roomSync.state.events.isNotEmpty()) {
val minStateIndex = roomEntity.untimelinedStateEvents.where().min(EventEntityFields.STATE_INDEX)?.toInt()
?: Int.MIN_VALUE
?: Int.MIN_VALUE
val untimelinedStateIndex = minStateIndex + 1
roomSync.state.events.forEach { event ->
roomEntity.addStateEvent(event, filterDuplicates = true, stateIndex = untimelinedStateIndex)
@ -150,14 +169,6 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
roomEntity.addOrUpdate(chunkEntity)
}
roomSummaryUpdater.update(realm, roomId, Membership.JOIN, roomSync.summary, roomSync.unreadNotifications)
if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) {
handleEphemeral(realm, roomId, roomSync.ephemeral)
}
if (roomSync.accountData != null && roomSync.accountData.events.isNullOrEmpty().not()) {
handleRoomAccountDataEvents(realm, roomId, roomSync.accountData)
}
return roomEntity
}
@ -167,7 +178,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
InvitedRoomSync): RoomEntity {
Timber.v("Handle invited sync for room $roomId")
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId)
?: realm.createObject(roomId)
roomEntity.membership = Membership.INVITE
if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) {
val chunkEntity = handleTimelineEvents(realm, roomEntity, roomSync.inviteState.events)
@ -181,7 +192,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
roomId: String,
roomSync: RoomSync): RoomEntity {
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId)
?: realm.createObject(roomId)
roomEntity.membership = Membership.LEAVE
roomEntity.chunks.deleteAllFromRealm()
@ -233,20 +244,29 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
}
@Suppress("UNCHECKED_CAST")
private fun handleEphemeral(realm: Realm,
roomId: String,
ephemeral: RoomSyncEphemeral) {
ephemeral.events
.filter { it.getClearType() == EventType.RECEIPT }
.map { it.content.toModel<ReadReceiptContent>() }
.forEach { readReceiptHandler.handle(realm, roomId, it) }
ephemeral: RoomSyncEphemeral,
isInitalSync: Boolean) {
for (event in ephemeral.events) {
if (event.type != EventType.RECEIPT) continue
val readReceiptContent = event.content as? ReadReceiptContent ?: continue
readReceiptHandler.handle(realm, roomId, readReceiptContent, isInitalSync)
}
}
private fun handleRoomAccountDataEvents(realm: Realm, roomId: String, accountData: RoomSyncAccountData) {
accountData.events
.filter { it.getClearType() == EventType.TAG }
.map { it.content.toModel<RoomTagContent>() }
.forEach { roomTagHandler.handle(realm, roomId, it) }
for (event in accountData.events) {
val eventType = event.getClearType()
if (eventType == EventType.TAG) {
val content = event.getClearContent().toModel<RoomTagContent>()
roomTagHandler.handle(realm, roomId, content)
} else if (eventType == EventType.FULLY_READ) {
val content = event.getClearContent().toModel<FullyReadContent>()
roomFullyReadHandler.handle(realm, roomId, content)
}
}
}
}

View File

@ -66,7 +66,7 @@ internal class SyncResponseHandler @Inject constructor(private val roomSyncHandl
reportSubtask(reporter, R.string.initial_sync_start_importing_account_rooms, 100, 0.7f) {
if (syncResponse.rooms != null) {
roomSyncHandler.handle(syncResponse.rooms, reporter)
roomSyncHandler.handle(syncResponse.rooms, isInitialSync, reporter)
}
}
}.also {

View File

@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.TaskThread
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
import kotlinx.coroutines.CancellationException
import timber.log.Timber
import java.net.SocketTimeoutException
import java.util.concurrent.CountDownLatch
@ -70,6 +71,8 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
if (state is SyncState.RUNNING) {
Timber.v("Pause sync...")
updateStateTo(SyncState.PAUSED)
cancelableTask?.cancel()
lock.notify()
}
}
@ -90,18 +93,25 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
backgroundDetectionObserver.register(this)
while (state != SyncState.KILLING) {
Timber.v("Entering loop, state: $state")
if (!networkConnectivityChecker.isConnected() || state == SyncState.PAUSED) {
Timber.v("Sync is Paused. Waiting...")
Timber.v("No network or sync is Paused. Waiting...")
synchronized(lock) {
lock.wait()
}
Timber.v("...unlocked")
} else {
if (state !is SyncState.RUNNING) {
updateStateTo(SyncState.RUNNING(afterPause = true))
}
Timber.v("[$this] Execute sync request with timeout $DEFAULT_LONG_POOL_TIMEOUT")
// No timeout after a pause
val timeout = state.let { if (it is SyncState.RUNNING && it.afterPause) 0 else DEFAULT_LONG_POOL_TIMEOUT }
Timber.v("Execute sync request with timeout $timeout")
val latch = CountDownLatch(1)
val params = SyncTask.Params(DEFAULT_LONG_POOL_TIMEOUT)
val params = SyncTask.Params(timeout)
cancelableTask = syncTask.configureWith(params) {
this.callbackThread = TaskThread.SYNC
@ -109,29 +119,31 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
this.callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
Timber.v("onSuccess")
latch.countDown()
}
override fun onFailure(failure: Throwable) {
if (failure is Failure.NetworkConnection
&& failure.cause is SocketTimeoutException) {
if (failure is Failure.NetworkConnection && failure.cause is SocketTimeoutException) {
// Timeout are not critical
Timber.v("Timeout")
} else {
Timber.e(failure)
}
if (failure !is Failure.NetworkConnection
|| failure.cause is JsonEncodingException) {
// Wait 10s before retrying
sleep(RETRY_WAIT_TIME_MS)
}
if (failure is Failure.ServerError
} else if (failure is Failure.Unknown && failure.throwable is CancellationException) {
Timber.v("Cancelled")
} else if (failure is Failure.ServerError
&& (failure.error.code == MatrixError.UNKNOWN_TOKEN || failure.error.code == MatrixError.MISSING_TOKEN)) {
// No token or invalid token, stop the thread
Timber.w(failure)
updateStateTo(SyncState.KILLING)
} else {
Timber.e(failure)
if (failure !is Failure.NetworkConnection || failure.cause is JsonEncodingException) {
// Wait 10s before retrying
Timber.v("Wait 10s")
sleep(RETRY_WAIT_TIME_MS)
}
}
latch.countDown()
}
}
@ -139,8 +151,10 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
.executeBy(taskExecutor)
latch.await()
if (state is SyncState.RUNNING) {
updateStateTo(SyncState.RUNNING(afterPause = false))
state.let {
if (it is SyncState.RUNNING && it.afterPause) {
updateStateTo(SyncState.RUNNING(afterPause = false))
}
}
Timber.v("...Continue")

View File

@ -29,7 +29,15 @@ static def generateVersionCodeFromTimestamp() {
}
def generateVersionCodeFromVersionName() {
return versionMajor * 10000 + versionMinor * 100 + versionPatch
return versionMajor * 1_00_00 + versionMinor * 1_00 + versionPatch
}
def getVersionCode() {
if (gitBranchName() == "develop") {
return generateVersionCodeFromTimestamp()
} else {
return generateVersionCodeFromVersionName()
}
}
static def gitRevision() {
@ -47,6 +55,14 @@ static def gitBranchName() {
return cmd.execute().text.trim()
}
static def getVersionSuffix() {
if (gitBranchName() == "master") {
return ""
} else {
return "-dev"
}
}
project.android.buildTypes.all { buildType ->
buildType.javaCompileOptions.annotationProcessorOptions.arguments =
[
@ -71,11 +87,11 @@ android {
targetSdkVersion 28
multiDexEnabled true
// For release, use generateVersionCodeFromVersionName()
versionCode generateVersionCodeFromTimestamp()
//versionCode generateVersionCodeFromVersionName()
// `develop` branch will have version code from timestamp, to ensure each build from CI has a incremented versionCode.
// Other branches (master, features, etc.) will have version code based on application version.
versionCode project.getVersionCode()
versionName "${versionMajor}.${versionMinor}.${versionPatch}-dev"
versionName "${versionMajor}.${versionMinor}.${versionPatch}${getVersionSuffix()}"
buildConfigField "String", "GIT_REVISION", "\"${gitRevision()}\""
resValue "string", "git_revision", "\"${gitRevision()}\""
@ -117,9 +133,10 @@ android {
}
}
android.applicationVariants.all { variant ->
applicationVariants.all { variant ->
variant.outputs.each { output ->
def baseAbiVersionCode = project.ext.abiVersionCodes.get(output.getFilter(OutputFile.ABI))
// Known limitation: it does not modify the value in the BuildConfig.java generated file
output.versionCodeOverride = baseAbiVersionCode * 10_000_000 + variant.versionCode
}
}

View File

@ -15,7 +15,6 @@
*/
package im.vector.riotx.fdroid.features.settings.troubleshoot
import androidx.appcompat.app.AppCompatActivity
import im.vector.riotx.R
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.settings.VectorPreferences
@ -25,12 +24,12 @@ import javax.inject.Inject
/**
* Test that the application is started on boot
*/
class TestAutoStartBoot @Inject constructor(private val context: AppCompatActivity,
class TestAutoStartBoot @Inject constructor(private val vectorPreferences: VectorPreferences,
private val stringProvider: StringProvider)
: TroubleshootTest(R.string.settings_troubleshoot_test_service_boot_title) {
override fun perform() {
if (VectorPreferences.autoStartOnBoot(context)) {
if (vectorPreferences.autoStartOnBoot()) {
description = stringProvider.getString(R.string.settings_troubleshoot_test_service_boot_success)
status = TestStatus.SUCCESS
quickFix = null
@ -38,7 +37,7 @@ class TestAutoStartBoot @Inject constructor(private val context: AppCompatActivi
description = stringProvider.getString(R.string.settings_troubleshoot_test_service_boot_failed)
quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_service_boot_quickfix) {
override fun doFix() {
VectorPreferences.setAutoStartOnBoot(context, true)
vectorPreferences.setAutoStartOnBoot(true)
manager?.retry()
}
}

View File

@ -63,9 +63,9 @@ object FcmHelper {
AlarmSyncBroadcastReceiver.cancelAlarm(context)
}
fun onEnterBackground(context: Context, activeSessionHolder: ActiveSessionHolder) {
fun onEnterBackground(context: Context, vectorPreferences: VectorPreferences, activeSessionHolder: ActiveSessionHolder) {
//We need to use alarm in this mode
if (VectorPreferences.areNotificationEnabledForDevice(context) && activeSessionHolder.hasActiveSession()) {
if (vectorPreferences.areNotificationEnabledForDevice() && activeSessionHolder.hasActiveSession()) {
val currentSession = activeSessionHolder.getActiveSession()
AlarmSyncBroadcastReceiver.scheduleAlarm(context, currentSession.myUserId, 4_000L)
Timber.i("Alarm scheduled to restart service")

View File

@ -52,6 +52,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
private lateinit var notifiableEventResolver: NotifiableEventResolver
private lateinit var pusherManager: PushersManager
private lateinit var activeSessionHolder: ActiveSessionHolder
private lateinit var vectorPreferences: VectorPreferences
// UI handler
private val mUIHandler by lazy {
@ -64,6 +65,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
notifiableEventResolver = vectorComponent().notifiableEventResolver()
pusherManager = vectorComponent().pusherManager()
activeSessionHolder = vectorComponent().activeSessionHolder()
vectorPreferences = vectorComponent().vectorPreferences()
}
/**
@ -72,7 +74,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
* @param message the message
*/
override fun onMessageReceived(message: RemoteMessage?) {
if (!VectorPreferences.areNotificationEnabledForDevice(applicationContext)) {
if (!vectorPreferences.areNotificationEnabledForDevice()) {
Timber.i("Notification are disabled for this device")
return
}
@ -107,7 +109,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
if (refreshedToken == null) {
Timber.w("onNewToken:received null token")
} else {
if (VectorPreferences.areNotificationEnabledForDevice(applicationContext) && activeSessionHolder.hasActiveSession()) {
if (vectorPreferences.areNotificationEnabledForDevice() && activeSessionHolder.hasActiveSession()) {
pusherManager.registerPusherWithFcmKey(refreshedToken)
}
}

View File

@ -27,6 +27,7 @@ import com.google.firebase.iid.FirebaseInstanceId
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.pushers.PushersManager
import im.vector.riotx.features.settings.VectorPreferences
import timber.log.Timber
/**
@ -105,7 +106,7 @@ object FcmHelper {
// No op
}
fun onEnterBackground(context: Context, activeSessionHolder: ActiveSessionHolder) {
fun onEnterBackground(context: Context, vectorPreferences: VectorPreferences, activeSessionHolder: ActiveSessionHolder) {
// TODO FCM fallback
}
}

View File

@ -42,6 +42,7 @@ import im.vector.riotx.core.di.DaggerVectorComponent
import im.vector.riotx.core.di.HasVectorInjector
import im.vector.riotx.core.di.VectorComponent
import im.vector.riotx.core.extensions.configureAndStart
import im.vector.riotx.core.utils.initKnownEmojiHashSet
import im.vector.riotx.features.configuration.VectorConfiguration
import im.vector.riotx.features.lifecycle.VectorActivityLifecycleCallbacks
import im.vector.riotx.features.notifications.NotificationDrawerManager
@ -49,12 +50,12 @@ import im.vector.riotx.features.notifications.NotificationUtils
import im.vector.riotx.features.notifications.PushRuleTriggerListener
import im.vector.riotx.features.rageshake.VectorFileLogger
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
import im.vector.riotx.features.version.getVersion
import im.vector.riotx.features.settings.VectorPreferences
import im.vector.riotx.features.version.VersionProvider
import im.vector.riotx.push.fcm.FcmHelper
import timber.log.Timber
import java.text.SimpleDateFormat
import java.util.*
import im.vector.riotx.core.utils.initKnownEmojiHashSet
import javax.inject.Inject
class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.Provider, androidx.work.Configuration.Provider {
@ -69,6 +70,8 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
@Inject lateinit var activeSessionHolder: ActiveSessionHolder
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
@Inject lateinit var pushRuleTriggerListener: PushRuleTriggerListener
@Inject lateinit var vectorPreferences: VectorPreferences
@Inject lateinit var versionProvider: VersionProvider
lateinit var vectorComponent: VectorComponent
private var fontThreadHandler: Handler? = null
@ -122,7 +125,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
fun entersBackground() {
Timber.i("App entered background") // call persistInfo
notificationDrawerManager.persistInfo()
FcmHelper.onEnterBackground(appContext, activeSessionHolder)
FcmHelper.onEnterBackground(appContext, vectorPreferences, activeSessionHolder)
}
})
//This should be done as early as possible
@ -138,7 +141,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
}
private fun logInfo() {
val appVersion = getVersion(longFormat = true, useBuildNumber = true)
val appVersion = versionProvider.getVersion(longFormat = true, useBuildNumber = true)
val sdkVersion = Matrix.getSdkVersion()
val date = SimpleDateFormat("MM-dd HH:mm:ss.SSSZ", Locale.US).format(Date())

View File

@ -14,15 +14,18 @@
* limitations under the License.
*/
package im.vector.riotx.features.home.room.detail.timeline.helper
package im.vector.riotx.core.date
import android.content.Context
import android.text.format.DateUtils
import im.vector.riotx.core.resources.LocaleProvider
import org.threeten.bp.LocalDateTime
import org.threeten.bp.format.DateTimeFormatter
import javax.inject.Inject
class TimelineDateFormatter @Inject constructor (private val localeProvider: LocaleProvider) {
class VectorDateFormatter @Inject constructor(private val context: Context,
private val localeProvider: LocaleProvider) {
private val messageHourFormatter by lazy {
DateTimeFormatter.ofPattern("H:mm", localeProvider.current())
@ -39,4 +42,16 @@ class TimelineDateFormatter @Inject constructor (private val localeProvider: Loc
return messageDayFormatter.format(localDateTime)
}
fun formatRelativeDateTime(time: Long?): String {
if (time == null) {
return ""
}
return DateUtils.getRelativeDateTimeString(context,
time,
DateUtils.DAY_IN_MILLIS,
2 * DateUtils.DAY_IN_MILLIS,
DateUtils.FORMAT_SHOW_WEEKDAY
).toString()
}
}

View File

@ -0,0 +1,24 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.core.di
import com.squareup.inject.assisted.dagger2.AssistedModule
import dagger.Module
@AssistedModule
@Module(includes = [AssistedInject_AssistedInjectModule::class])
interface AssistedInjectModule

View File

@ -41,7 +41,12 @@ import im.vector.riotx.features.home.createdirect.CreateDirectRoomDirectoryUsers
import im.vector.riotx.features.home.createdirect.CreateDirectRoomKnownUsersFragment
import im.vector.riotx.features.home.group.GroupListFragment
import im.vector.riotx.features.home.room.detail.RoomDetailFragment
import im.vector.riotx.features.home.room.detail.timeline.action.*
import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.action.MessageMenuFragment
import im.vector.riotx.features.home.room.detail.timeline.action.QuickReactionFragment
import im.vector.riotx.features.home.room.detail.timeline.action.ViewEditHistoryBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionBottomSheet
import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity
import im.vector.riotx.features.home.room.list.RoomListFragment
import im.vector.riotx.features.invite.VectorInviteView
@ -60,12 +65,15 @@ import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment
import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerFragment
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment
import im.vector.riotx.features.settings.VectorSettingsActivity
import im.vector.riotx.features.settings.VectorSettingsAdvancedNotificationPreferenceFragment
import im.vector.riotx.features.settings.VectorSettingsHelpAboutFragment
import im.vector.riotx.features.settings.VectorSettingsNotificationPreferenceFragment
import im.vector.riotx.features.settings.VectorSettingsNotificationsTroubleshootFragment
import im.vector.riotx.features.settings.VectorSettingsPreferencesFragment
import im.vector.riotx.features.settings.VectorSettingsSecurityPrivacyFragment
import im.vector.riotx.features.settings.push.PushGatewaysFragment
@Component(dependencies = [VectorComponent::class], modules = [ViewModelModule::class, HomeModule::class])
@Component(dependencies = [VectorComponent::class], modules = [AssistedInjectModule::class, ViewModelModule::class, HomeModule::class])
@ScreenScope
interface ScreenComponent {
@ -153,6 +161,12 @@ interface ScreenComponent {
fun inject(vectorSettingsPreferencesFragment: VectorSettingsPreferencesFragment)
fun inject(vectorSettingsAdvancedNotificationPreferenceFragment: VectorSettingsAdvancedNotificationPreferenceFragment)
fun inject(vectorSettingsSecurityPrivacyFragment: VectorSettingsSecurityPrivacyFragment)
fun inject(vectorSettingsHelpAboutFragment: VectorSettingsHelpAboutFragment)
fun inject(userAvatarPreference: UserAvatarPreference)
fun inject(vectorSettingsNotificationsTroubleshootFragment: VectorSettingsNotificationsTroubleshootFragment)
@ -165,6 +179,8 @@ interface ScreenComponent {
fun inject(createDirectRoomActivity: CreateDirectRoomActivity)
fun inject(displayReadReceiptsBottomSheet: DisplayReadReceiptsBottomSheet)
@Component.Factory
interface Factory {
fun create(vectorComponent: VectorComponent,

View File

@ -41,6 +41,7 @@ import im.vector.riotx.features.notifications.NotificationDrawerManager
import im.vector.riotx.features.notifications.PushRuleTriggerListener
import im.vector.riotx.features.rageshake.BugReporter
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
import im.vector.riotx.features.settings.VectorPreferences
import javax.inject.Singleton
@Component(modules = [VectorModule::class])
@ -95,6 +96,8 @@ interface VectorComponent {
fun notifiableEventResolver(): NotifiableEventResolver
fun vectorPreferences(): VectorPreferences
@Component.Factory
interface Factory {
fun create(@BindsInstance context: Context): VectorComponent

View File

@ -25,41 +25,17 @@ import im.vector.riotx.core.platform.ConfigurationViewModel
import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromKeyViewModel
import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromPassphraseViewModel
import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreSharedViewModel
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsViewModel
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsViewModel_AssistedFactory
import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupSharedViewModel
import im.vector.riotx.features.crypto.verification.SasVerificationViewModel
import im.vector.riotx.features.home.*
import im.vector.riotx.features.home.HomeNavigationViewModel
import im.vector.riotx.features.home.createdirect.CreateDirectRoomNavigationViewModel
import im.vector.riotx.features.home.createdirect.CreateDirectRoomViewModel
import im.vector.riotx.features.home.createdirect.CreateDirectRoomViewModel_AssistedFactory
import im.vector.riotx.features.home.group.GroupListViewModel
import im.vector.riotx.features.home.group.GroupListViewModel_AssistedFactory
import im.vector.riotx.features.home.room.detail.RoomDetailViewModel
import im.vector.riotx.features.home.room.detail.RoomDetailViewModel_AssistedFactory
import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel
import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel_AssistedFactory
import im.vector.riotx.features.home.room.detail.timeline.action.*
import im.vector.riotx.features.home.room.list.RoomListViewModel
import im.vector.riotx.features.home.room.list.RoomListViewModel_AssistedFactory
import im.vector.riotx.features.reactions.EmojiChooserViewModel
import im.vector.riotx.features.roomdirectory.RoomDirectoryNavigationViewModel
import im.vector.riotx.features.roomdirectory.RoomDirectoryViewModel
import im.vector.riotx.features.roomdirectory.RoomDirectoryViewModel_AssistedFactory
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomViewModel
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomViewModel_AssistedFactory
import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerViewModel
import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerViewModel_AssistedFactory
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewViewModel
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewViewModel_AssistedFactory
import im.vector.riotx.features.settings.push.PushGatewaysViewModel
import im.vector.riotx.features.settings.push.PushGatewaysViewModel_AssistedFactory
import im.vector.riotx.features.workers.signout.SignOutViewModel
@Module
interface ViewModelModule {
/**
* ViewModels with @IntoMap will be injected by this factory
*/
@ -69,6 +45,7 @@ interface ViewModelModule {
/**
* Below are bindings for the androidx view models (which extend ViewModel). Will be converted to MvRx ViewModel in the future.
*/
@Binds
@IntoMap
@ViewModelKey(SignOutViewModel::class)
@ -124,62 +101,4 @@ interface ViewModelModule {
@ViewModelKey(CreateDirectRoomNavigationViewModel::class)
fun bindCreateDirectRoomNavigationViewModel(viewModel: CreateDirectRoomNavigationViewModel): ViewModel
/**
* Below are bindings for the MvRx view models (which extend VectorViewModel). Will be the only usage in the future.
*/
@Binds
fun bindHomeActivityViewModelFactory(factory: HomeActivityViewModel_AssistedFactory): HomeActivityViewModel.Factory
@Binds
fun bindTextComposerViewModelFactory(factory: TextComposerViewModel_AssistedFactory): TextComposerViewModel.Factory
@Binds
fun bindRoomDetailViewModelFactory(factory: RoomDetailViewModel_AssistedFactory): RoomDetailViewModel.Factory
@Binds
fun bindQuickReactionViewModelFactory(factory: QuickReactionViewModel_AssistedFactory): QuickReactionViewModel.Factory
@Binds
fun bindMessageActionsViewModelFactory(factory: MessageActionsViewModel_AssistedFactory): MessageActionsViewModel.Factory
@Binds
fun bindMessageMenuViewModelFactory(factory: MessageMenuViewModel_AssistedFactory): MessageMenuViewModel.Factory
@Binds
fun bindRoomListViewModelFactory(factory: RoomListViewModel_AssistedFactory): RoomListViewModel.Factory
@Binds
fun bindGroupListViewModelFactory(factory: GroupListViewModel_AssistedFactory): GroupListViewModel.Factory
@Binds
fun bindHomeDetailViewModelFactory(factory: HomeDetailViewModel_AssistedFactory): HomeDetailViewModel.Factory
@Binds
fun bindKeysBackupSettingsViewModelFactory(factory: KeysBackupSettingsViewModel_AssistedFactory): KeysBackupSettingsViewModel.Factory
@Binds
fun bindRoomDirectoryPickerViewModelFactory(factory: RoomDirectoryPickerViewModel_AssistedFactory): RoomDirectoryPickerViewModel.Factory
@Binds
fun bindRoomDirectoryViewModelFactory(factory: RoomDirectoryViewModel_AssistedFactory): RoomDirectoryViewModel.Factory
@Binds
fun bindRoomPreviewViewModelFactory(factory: RoomPreviewViewModel_AssistedFactory): RoomPreviewViewModel.Factory
@Binds
fun bindViewReactionViewModelFactory(factory: ViewReactionViewModel_AssistedFactory): ViewReactionViewModel.Factory
@Binds
fun bindViewEditHistoryViewModelFactory(factory: ViewEditHistoryViewModel_AssistedFactory): ViewEditHistoryViewModel.Factory
@Binds
fun bindCreateRoomViewModelFactory(factory: CreateRoomViewModel_AssistedFactory): CreateRoomViewModel.Factory
@Binds
fun bindCreateDirectRoomViewModelFactory(factory: CreateDirectRoomViewModel_AssistedFactory): CreateDirectRoomViewModel.Factory
@Binds
fun bindPushGatewaysViewModelFactory(factory: PushGatewaysViewModel_AssistedFactory): PushGatewaysViewModel.Factory
}

View File

@ -161,7 +161,6 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector {
override fun onDestroy() {
super.onDestroy()
unBinder?.unbind()
unBinder = null

View File

@ -16,13 +16,17 @@
package im.vector.riotx.core.resources
import android.content.Context
import im.vector.riotx.features.settings.VectorPreferences
import javax.inject.Inject
class UserPreferencesProvider @Inject constructor(private val context: Context) {
class UserPreferencesProvider @Inject constructor(private val vectorPreferences: VectorPreferences) {
fun shouldShowHiddenEvents(): Boolean {
return VectorPreferences.shouldShowHiddenEvents(context)
return vectorPreferences.shouldShowHiddenEvents()
}
fun shouldShowReadReceipts(): Boolean {
return vectorPreferences.showReadReceipts()
}
}

View File

@ -0,0 +1,40 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.core.resources
import android.content.Context
import android.os.Build
import androidx.annotation.NonNull
import javax.inject.Inject
class VersionCodeProvider @Inject constructor(private val context: Context) {
/**
* Returns the version code, read from the Manifest. It is not the same than BuildConfig.VERSION_CODE due to versionCodeOverride
*/
@NonNull
fun getVersionCode(): Long {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
packageInfo.longVersionCode
} else {
@Suppress("DEPRECATION")
packageInfo.versionCode.toLong()
}
}
}

View File

@ -0,0 +1,75 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.core.ui.views
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.LinearLayout
import android.widget.RelativeLayout
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import butterknife.ButterKnife
import im.vector.riotx.R
import im.vector.riotx.features.themes.ThemeUtils
import kotlinx.android.synthetic.main.view_jump_to_read_marker.view.*
import me.gujun.android.span.span
import me.saket.bettermovementmethod.BetterLinkMovementMethod
class JumpToReadMarkerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : RelativeLayout(context, attrs, defStyleAttr) {
interface Callback {
fun onJumpToReadMarkerClicked(readMarkerId: String)
fun onClearReadMarkerClicked()
}
var callback: Callback? = null
init {
setupView()
}
private fun setupView() {
LinearLayout.inflate(context, R.layout.view_jump_to_read_marker, this)
setBackgroundColor(ContextCompat.getColor(context, R.color.notification_accent_color))
jumpToReadMarkerLabelView.movementMethod = BetterLinkMovementMethod.getInstance()
isClickable = true
closeJumpToReadMarkerView.setOnClickListener {
visibility = View.GONE
callback?.onClearReadMarkerClicked()
}
}
fun render(show: Boolean, readMarkerId: String?) {
isVisible = show
if (readMarkerId != null) {
jumpToReadMarkerLabelView.text = span(resources.getString(R.string.room_jump_to_first_unread)) {
textDecorationLine = "underline"
onClick = { callback?.onJumpToReadMarkerClicked(readMarkerId) }
}
}
}
}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package im.vector.riotx.core.platform
package im.vector.riotx.core.ui.views
import android.content.Context
import android.graphics.Color
@ -32,10 +32,7 @@ import androidx.core.content.ContextCompat
import butterknife.BindView
import butterknife.ButterKnife
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.api.permalinks.PermalinkFactory
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent
import im.vector.riotx.R
import im.vector.riotx.core.error.ResourceLimitErrorFormatter
import im.vector.riotx.features.themes.ThemeUtils

View File

@ -0,0 +1,86 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.core.ui.views
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import im.vector.riotx.R
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import kotlinx.coroutines.*
private const val DELAY_IN_MS = 1_500L
class ReadMarkerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
interface Callback {
fun onReadMarkerDisplayed()
}
private var callback: Callback? = null
private var callbackDispatcherJob: Job? = null
fun bindView(informationData: MessageInformationData, readMarkerCallback: Callback) {
this.callback = readMarkerCallback
if (informationData.displayReadMarker) {
visibility = VISIBLE
callbackDispatcherJob = GlobalScope.launch(Dispatchers.Main) {
delay(DELAY_IN_MS)
callback?.onReadMarkerDisplayed()
}
startAnimation()
} else {
visibility = INVISIBLE
}
}
fun unbind() {
this.callbackDispatcherJob?.cancel()
this.callback = null
this.animation?.cancel()
this.visibility = INVISIBLE
}
private fun startAnimation() {
if (animation == null) {
animation = AnimationUtils.loadAnimation(context, R.anim.unread_marker_anim)
animation.startOffset = DELAY_IN_MS / 2
animation.duration = DELAY_IN_MS / 2
animation.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(animation: Animation) {
}
override fun onAnimationEnd(animation: Animation) {
visibility = INVISIBLE
}
override fun onAnimationRepeat(animation: Animation) {}
})
}
animation.start()
}
}

View File

@ -0,0 +1,80 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.core.ui.views
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import butterknife.ButterKnife
import im.vector.riotx.R
import im.vector.riotx.core.utils.DebouncedClickListener
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
import kotlinx.android.synthetic.main.view_read_receipts.view.*
private const val MAX_RECEIPT_DISPLAYED = 5
class ReadReceiptsView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
private val receiptAvatars: List<ImageView> by lazy {
listOf(receiptAvatar1, receiptAvatar2, receiptAvatar3, receiptAvatar4, receiptAvatar5)
}
init {
setupView()
}
private fun setupView() {
inflate(context, R.layout.view_read_receipts, this)
ButterKnife.bind(this)
}
fun render(readReceipts: List<ReadReceiptData>, avatarRenderer: AvatarRenderer, clickListener: OnClickListener) {
setOnClickListener(clickListener)
if (readReceipts.isNotEmpty()) {
isVisible = true
for (index in 0 until MAX_RECEIPT_DISPLAYED) {
val receiptData = readReceipts.getOrNull(index)
if (receiptData == null) {
receiptAvatars[index].visibility = View.INVISIBLE
} else {
receiptAvatars[index].visibility = View.VISIBLE
avatarRenderer.render(receiptData.avatarUrl, receiptData.userId, receiptData.displayName, receiptAvatars[index])
}
}
if (readReceipts.size > MAX_RECEIPT_DISPLAYED) {
receiptMore.visibility = View.VISIBLE
receiptMore.text = context.getString(
R.string.x_plus, readReceipts.size - MAX_RECEIPT_DISPLAYED
)
} else {
receiptMore.visibility = View.GONE
}
} else {
isVisible = false
}
}
}

View File

@ -0,0 +1,25 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.home.room.detail
import java.io.File
data class DownloadFileState(
val mimeType: String,
val file: File?,
val throwable: Throwable?
)

View File

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

View File

@ -28,7 +28,12 @@ import android.os.Parcelable
import android.text.Editable
import android.text.Spannable
import android.text.TextUtils
import android.view.*
import android.view.HapticFeedbackConstants
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.Window
import android.view.inputmethod.InputMethodManager
import android.widget.TextView
import android.widget.Toast
@ -46,7 +51,12 @@ import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.mvrx.*
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.github.piasy.biv.BigImageViewer
import com.github.piasy.biv.loader.ImageLoader
import com.google.android.material.snackbar.Snackbar
@ -60,11 +70,18 @@ import im.vector.matrix.android.api.permalinks.PermalinkFactory
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent
import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
@ -76,9 +93,21 @@ import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.core.files.addEntryToDownloadManager
import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.core.platform.NotificationAreaView
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.*
import im.vector.riotx.core.ui.views.JumpToReadMarkerView
import im.vector.riotx.core.ui.views.NotificationAreaView
import im.vector.riotx.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_DOWNLOAD_FILE
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_CAMERA
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CAMERA
import im.vector.riotx.core.utils.allGranted
import im.vector.riotx.core.utils.checkPermissions
import im.vector.riotx.core.utils.copyToClipboard
import im.vector.riotx.core.utils.openCamera
import im.vector.riotx.core.utils.shareMedia
import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter
import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy
import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter
@ -91,10 +120,20 @@ import im.vector.riotx.features.home.room.detail.composer.TextComposerActions
import im.vector.riotx.features.home.room.detail.composer.TextComposerView
import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel
import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState
import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.action.*
import im.vector.riotx.features.home.room.detail.timeline.action.ActionsHandler
import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.action.SimpleAction
import im.vector.riotx.features.home.room.detail.timeline.action.ViewEditHistoryBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener
import im.vector.riotx.features.home.room.detail.timeline.item.*
import im.vector.riotx.features.home.room.detail.timeline.item.AbsMessageItem
import im.vector.riotx.features.home.room.detail.timeline.item.MessageFileItem
import im.vector.riotx.features.home.room.detail.timeline.item.MessageImageVideoItem
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.riotx.features.html.EventHtmlRenderer
import im.vector.riotx.features.html.PillImageSpan
import im.vector.riotx.features.invite.VectorInviteView
@ -132,7 +171,8 @@ class RoomDetailFragment :
VectorBaseFragment(),
TimelineEventController.Callback,
AutocompleteUserPresenter.Callback,
VectorInviteView.Callback {
VectorInviteView.Callback,
JumpToReadMarkerView.Callback {
companion object {
@ -184,6 +224,7 @@ class RoomDetailFragment :
private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
private lateinit var scrollOnHighlightedEventCallback: ScrollOnHighlightedEventCallback
@Inject lateinit var eventHtmlRenderer: EventHtmlRenderer
@Inject lateinit var vectorPreferences: VectorPreferences
override fun getLayoutResId() = R.layout.fragment_room_detail
@ -191,6 +232,7 @@ class RoomDetailFragment :
override fun getMenuRes() = R.menu.menu_timeline
private lateinit var actionViewModel: ActionsHandler
private lateinit var layoutManager: LinearLayoutManager
@BindView(R.id.composerLayout)
lateinit var composerLayout: TextComposerView
@ -208,6 +250,7 @@ class RoomDetailFragment :
setupAttachmentButton()
setupInviteView()
setupNotificationView()
setupJumpToReadMarkerView()
roomDetailViewModel.subscribe { renderState(it) }
textComposerViewModel.subscribe { renderTextComposerState(it) }
roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) }
@ -221,8 +264,12 @@ class RoomDetailFragment :
}
roomDetailViewModel.navigateToEvent.observeEvent(this) {
//
scrollOnHighlightedEventCallback.scheduleScrollTo(it)
val scrollPosition = timelineEventController.searchPositionOfEvent(it)
if (scrollPosition == null) {
scrollOnHighlightedEventCallback.scheduleScrollTo(it)
} else {
layoutManager.scrollToPosition(scrollPosition)
}
}
roomDetailViewModel.selectSubscribe(this, RoomDetailViewState::tombstoneEventHandling, uniqueOnly("tombstoneEventHandling")) {
@ -246,6 +293,18 @@ class RoomDetailFragment :
is SendMode.REPLY -> enterSpecialMode(mode.timelineEvent, R.drawable.ic_reply, false)
}
}
roomDetailViewModel.selectSubscribe(RoomDetailViewState::syncState) { syncState ->
syncProgressBar.visibility = when (syncState) {
is SyncState.RUNNING -> if (syncState.afterPause) View.VISIBLE else View.GONE
else -> View.GONE
}
syncProgressBarWrap.visibility = syncProgressBar.visibility
}
}
private fun setupJumpToReadMarkerView() {
jumpToReadMarkerView.callback = this
}
private fun setupNotificationView() {
@ -315,17 +374,17 @@ class RoomDetailFragment :
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
val parser = Parser.builder().build()
val document = parser.parse(messageContent.formattedBody
?: messageContent.body)
?: messageContent.body)
formattedBody = eventHtmlRenderer.render(document)
}
composerLayout.composerRelatedMessageContent.text = formattedBody
?: nonFormattedBody
?: nonFormattedBody
composerLayout.composerEditText.setText(if (useText) event.getTextEditableContent() else "")
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))
avatarRenderer.render(event.senderAvatar, event.root.senderId
?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)
?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
composerLayout.expand {
@ -354,9 +413,9 @@ class RoomDetailFragment :
REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data)
REACTION_SELECT_REQUEST_CODE -> {
val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID)
?: return
?: return
val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
?: return
?: return
//TODO check if already reacted with that?
roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId))
}
@ -369,7 +428,7 @@ class RoomDetailFragment :
private fun setupRecyclerView() {
val epoxyVisibilityTracker = EpoxyVisibilityTracker()
epoxyVisibilityTracker.attach(recyclerView)
val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager)
scrollOnHighlightedEventCallback = ScrollOnHighlightedEventCallback(layoutManager, timelineEventController)
@ -389,28 +448,28 @@ class RoomDetailFragment :
recyclerView.setController(timelineEventController)
timelineEventController.callback = this
if (VectorPreferences.swipeToReplyIsEnabled(requireContext())) {
if (vectorPreferences.swipeToReplyIsEnabled()) {
val swipeCallback = RoomMessageTouchHelperCallback(requireContext(),
R.drawable.ic_reply,
object : RoomMessageTouchHelperCallback.QuickReplayHandler {
override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
(model as? AbsMessageItem)?.informationData?.let {
val eventId = it.eventId
roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId))
}
}
R.drawable.ic_reply,
object : RoomMessageTouchHelperCallback.QuickReplayHandler {
override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
(model as? AbsMessageItem)?.attributes?.informationData?.let {
val eventId = it.eventId
roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId))
}
}
override fun canSwipeModel(model: EpoxyModel<*>): Boolean {
return when (model) {
is MessageFileItem,
is MessageImageVideoItem,
is MessageTextItem -> {
return (model as AbsMessageItem).informationData.sendState == SendState.SYNCED
}
else -> false
}
}
})
override fun canSwipeModel(model: EpoxyModel<*>): Boolean {
return when (model) {
is MessageFileItem,
is MessageImageVideoItem,
is MessageTextItem -> {
return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED
}
else -> false
}
}
})
val touchHelper = ItemTouchHelper(swipeCallback)
touchHelper.attachToRecyclerView(recyclerView)
}
@ -482,7 +541,7 @@ class RoomDetailFragment :
composerLayout.sendButton.setOnClickListener {
val textMessage = composerLayout.composerEditText.text.toString()
if (textMessage.isNotBlank()) {
roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage, VectorPreferences.isMarkdownEnabled(requireContext())))
roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage, vectorPreferences.isMarkdownEnabled()))
}
}
composerLayout.composerRelatedMessageCloseButton.setOnClickListener {
@ -507,7 +566,7 @@ class RoomDetailFragment :
items.add(DialogListItem.SendFile)
// Send voice
if (VectorPreferences.isSendVoiceFeatureEnabled(this)) {
if (vectorPreferences.isSendVoiceFeatureEnabled()) {
items.add(DialogListItem.SendVoice.INSTANCE)
}
@ -516,7 +575,7 @@ class RoomDetailFragment :
//items.add(DialogListItem.SendSticker)
// Camera
//if (VectorPreferences.useNativeCamera(this)) {
//if (vectorPreferences.useNativeCamera()) {
items.add(DialogListItem.TakePhoto)
items.add(DialogListItem.TakeVideo)
//} else {
@ -574,7 +633,7 @@ class RoomDetailFragment :
val summary = state.asyncRoomSummary()
val inviter = state.asyncInviter()
if (summary?.membership == Membership.JOIN) {
timelineEventController.setTimeline(state.timeline, state.eventId)
timelineEventController.setTimeline(state.timeline, state.highlightedEventId)
inviteView.visibility = View.GONE
val uid = session.myUserId
val meMember = session.getRoom(state.roomId)?.getRoomMember(uid)
@ -597,10 +656,12 @@ class RoomDetailFragment :
composerLayout.visibility = View.GONE
notificationAreaView.render(NotificationAreaView.State.Tombstone(state.tombstoneEvent))
}
jumpToReadMarkerView.render(state.showJumpToReadMarker, summary?.readMarkerId)
}
private fun renderRoomSummary(state: RoomDetailViewState) {
state.asyncRoomSummary()?.let {
if (it.membership.isLeft()) {
Timber.w("The room has been left")
activity?.finish()
@ -638,8 +699,12 @@ class RoomDetailFragment :
private fun renderSendMessageResult(sendMessageResult: SendMessageResult) {
when (sendMessageResult) {
is SendMessageResult.MessageSent,
is SendMessageResult.MessageSent -> {
// Clear composer
composerLayout.composerEditText.text = null
}
is SendMessageResult.SlashCommandHandled -> {
sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) }
// Clear composer
composerLayout.composerEditText.text = null
}
@ -669,7 +734,7 @@ class RoomDetailFragment :
.show()
}
// TimelineEventController.Callback ************************************************************
// TimelineEventController.Callback ************************************************************
override fun onUrlClicked(url: String): Boolean {
return permalinkHandler.launch(requireActivity(), url, object : NavigateToRoomInterceptor {
@ -681,7 +746,7 @@ class RoomDetailFragment :
showSnackWithMessage(getString(R.string.navigate_to_room_when_already_in_the_room))
} else {
// Highlight and scroll to this event
roomDetailViewModel.process(RoomDetailActions.NavigateToEvent(eventId, timelineEventController.searchPositionOfEvent(eventId)))
roomDetailViewModel.process(RoomDetailActions.NavigateToEvent(eventId, true))
}
return true
}
@ -701,7 +766,11 @@ class RoomDetailFragment :
}
override fun onEventVisible(event: TimelineEvent) {
roomDetailViewModel.process(RoomDetailActions.EventDisplayed(event))
roomDetailViewModel.process(RoomDetailActions.TimelineEventTurnsVisible(event))
}
override fun onEventInvisible(event: TimelineEvent) {
roomDetailViewModel.process(RoomDetailActions.TimelineEventTurnsInvisible(event))
}
override fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View) {
@ -816,7 +885,20 @@ class RoomDetailFragment :
})
}
// AutocompleteUserPresenter.Callback
override fun onReadReceiptsClicked(readReceipts: List<ReadReceiptData>) {
DisplayReadReceiptsBottomSheet.newInstance(readReceipts)
.show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS")
}
override fun onReadMarkerLongDisplayed(informationData: MessageInformationData) {
val firstVisibleItem = layoutManager.findFirstVisibleItemPosition()
val eventId = timelineEventController.searchEventIdAtPosition(firstVisibleItem)
if (eventId != null) {
roomDetailViewModel.process(RoomDetailActions.SetReadMarkerAction(eventId))
}
}
// AutocompleteUserPresenter.Callback
override fun onQueryUsers(query: CharSequence?) {
textComposerViewModel.process(TextComposerActions.QueryUsers(query))
@ -959,7 +1041,7 @@ class RoomDetailFragment :
// vibrate = true
}
// if (vibrate && VectorPreferences.vibrateWhenMentioning(context)) {
// if (vibrate && vectorPreferences.vibrateWhenMentioning()) {
// val v= context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
// if (v?.hasVibrator() == true) {
// v.vibrate(100)
@ -981,7 +1063,7 @@ class RoomDetailFragment :
snack.show()
}
// VectorInviteView.Callback
// VectorInviteView.Callback
override fun onAcceptInvite() {
notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId)
@ -992,4 +1074,16 @@ class RoomDetailFragment :
notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId)
roomDetailViewModel.process(RoomDetailActions.RejectInvite)
}
// JumpToReadMarkerView.Callback
override fun onJumpToReadMarkerClicked(readMarkerId: String) {
roomDetailViewModel.process(RoomDetailActions.NavigateToEvent(readMarkerId, false))
}
override fun onClearReadMarkerClicked() {
roomDetailViewModel.process(RoomDetailActions.MarkAllAsRead)
}
}

View File

@ -38,11 +38,13 @@ import im.vector.matrix.android.api.session.events.model.isTextMessage
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.file.FileService
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.message.getFileUrl
import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.matrix.rx.rx
@ -56,6 +58,9 @@ import im.vector.riotx.core.utils.subscribeLogError
import im.vector.riotx.features.command.CommandParser
import im.vector.riotx.features.command.ParsedCommand
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
import im.vector.riotx.features.settings.VectorPreferences
import io.reactivex.Observable
import io.reactivex.functions.Function3
import io.reactivex.rxkotlin.subscribeBy
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
@ -65,20 +70,23 @@ import java.util.concurrent.TimeUnit
class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: RoomDetailViewState,
userPreferencesProvider: UserPreferencesProvider,
private val userPreferencesProvider: UserPreferencesProvider,
private val vectorPreferences: VectorPreferences,
private val session: Session
) : VectorViewModel<RoomDetailViewState>(initialState) {
private val room = session.getRoom(initialState.roomId)!!
private val roomId = initialState.roomId
private val eventId = initialState.eventId
private val displayedEventsObservable = BehaviorRelay.create<RoomDetailActions.EventDisplayed>()
private val allowedTypes = if (userPreferencesProvider.shouldShowHiddenEvents()) {
TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES
private val invisibleEventsObservable = BehaviorRelay.create<RoomDetailActions.TimelineEventTurnsInvisible>()
private val visibleEventsObservable = BehaviorRelay.create<RoomDetailActions.TimelineEventTurnsVisible>()
private val timelineSettings = if (userPreferencesProvider.shouldShowHiddenEvents()) {
TimelineSettings(30, false, true, TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES, userPreferencesProvider.shouldShowReadReceipts())
} else {
TimelineDisplayableEvents.DISPLAYABLE_TYPES
TimelineSettings(30, true, true, TimelineDisplayableEvents.DISPLAYABLE_TYPES, userPreferencesProvider.shouldShowReadReceipts())
}
private var timeline = room.createTimeline(eventId, allowedTypes)
private var timeline = room.createTimeline(eventId, timelineSettings)
// Slot to keep a pending action during permission request
var pendingAction: RoomDetailActions? = null
@ -101,9 +109,11 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
init {
observeSyncState()
observeRoomSummary()
observeEventDisplayedActions()
observeSummaryState()
observeJumpToReadMarkerViewVisibility()
room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear()
timeline.start()
setState { copy(timeline = this@RoomDetailViewModel.timeline) }
@ -111,33 +121,40 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
fun process(action: RoomDetailActions) {
when (action) {
is RoomDetailActions.SendMessage -> handleSendMessage(action)
is RoomDetailActions.SendMedia -> handleSendMedia(action)
is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action)
is RoomDetailActions.LoadMoreTimelineEvents -> handleLoadMore(action)
is RoomDetailActions.SendReaction -> handleSendReaction(action)
is RoomDetailActions.AcceptInvite -> handleAcceptInvite()
is RoomDetailActions.RejectInvite -> handleRejectInvite()
is RoomDetailActions.RedactAction -> handleRedactEvent(action)
is RoomDetailActions.UndoReaction -> handleUndoReact(action)
is RoomDetailActions.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
is RoomDetailActions.EnterEditMode -> handleEditAction(action)
is RoomDetailActions.EnterQuoteMode -> handleQuoteAction(action)
is RoomDetailActions.EnterReplyMode -> handleReplyAction(action)
is RoomDetailActions.DownloadFile -> handleDownloadFile(action)
is RoomDetailActions.NavigateToEvent -> handleNavigateToEvent(action)
is RoomDetailActions.HandleTombstoneEvent -> handleTombstoneEvent(action)
is RoomDetailActions.ResendMessage -> handleResendEvent(action)
is RoomDetailActions.RemoveFailedEcho -> handleRemove(action)
is RoomDetailActions.ClearSendQueue -> handleClearSendQueue()
is RoomDetailActions.ResendAll -> handleResendAll()
else -> Timber.e("Unhandled Action: $action")
is RoomDetailActions.SendMessage -> handleSendMessage(action)
is RoomDetailActions.SendMedia -> handleSendMedia(action)
is RoomDetailActions.TimelineEventTurnsVisible -> handleEventVisible(action)
is RoomDetailActions.TimelineEventTurnsInvisible -> handleEventInvisible(action)
is RoomDetailActions.LoadMoreTimelineEvents -> handleLoadMore(action)
is RoomDetailActions.SendReaction -> handleSendReaction(action)
is RoomDetailActions.AcceptInvite -> handleAcceptInvite()
is RoomDetailActions.RejectInvite -> handleRejectInvite()
is RoomDetailActions.RedactAction -> handleRedactEvent(action)
is RoomDetailActions.UndoReaction -> handleUndoReact(action)
is RoomDetailActions.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
is RoomDetailActions.EnterEditMode -> handleEditAction(action)
is RoomDetailActions.EnterQuoteMode -> handleQuoteAction(action)
is RoomDetailActions.EnterReplyMode -> handleReplyAction(action)
is RoomDetailActions.DownloadFile -> handleDownloadFile(action)
is RoomDetailActions.NavigateToEvent -> handleNavigateToEvent(action)
is RoomDetailActions.HandleTombstoneEvent -> handleTombstoneEvent(action)
is RoomDetailActions.ResendMessage -> handleResendEvent(action)
is RoomDetailActions.RemoveFailedEcho -> handleRemove(action)
is RoomDetailActions.ClearSendQueue -> handleClearSendQueue()
is RoomDetailActions.ResendAll -> handleResendAll()
is RoomDetailActions.SetReadMarkerAction -> handleSetReadMarkerAction(action)
is RoomDetailActions.MarkAllAsRead -> handleMarkAllAsRead()
else -> Timber.e("Unhandled Action: $action")
}
}
private fun handleEventInvisible(action: RoomDetailActions.TimelineEventTurnsInvisible) {
invisibleEventsObservable.accept(action)
}
private fun handleTombstoneEvent(action: RoomDetailActions.HandleTombstoneEvent) {
val tombstoneContent = action.event.getClearContent().toModel<RoomTombstoneContent>()
?: return
?: return
val roomId = tombstoneContent.replacementRoom ?: ""
val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN
@ -243,8 +260,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented)
}
is ParsedCommand.SetMarkdown -> {
// TODO
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented)
vectorPreferences.setMarkdownEnabled(slashCommandResult.enable)
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled(
if (slashCommandResult.enable) R.string.markdown_has_been_enabled else R.string.markdown_has_been_disabled))
}
is ParsedCommand.UnbanUser -> {
// TODO
@ -268,7 +286,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
is ParsedCommand.SendEmote -> {
room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE)
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled)
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
}
is ParsedCommand.ChangeTopic -> {
handleChangeTopicSlashCommand(slashCommandResult)
@ -283,7 +301,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
//is original event a reply?
val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel<MessageContent>()?.relatesTo?.inReplyTo?.eventId
?: state.sendMode.timelineEvent.root.content.toModel<EncryptedEventContent>()?.relatesTo?.inReplyTo?.eventId
?: state.sendMode.timelineEvent.root.content.toModel<EncryptedEventContent>()?.relatesTo?.inReplyTo?.eventId
if (inReplyTo != null) {
//TODO check if same content?
room.getTimeLineEvent(inReplyTo)?.let {
@ -292,12 +310,12 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} else {
val messageContent: MessageContent? =
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
val existingBody = messageContent?.body ?: ""
if (existingBody != action.text) {
room.editTextMessage(state.sendMode.timelineEvent.root.eventId
?: "", messageContent?.type
?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown)
?: "", messageContent?.type
?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown)
} else {
Timber.w("Same message content, do not send edition")
}
@ -312,7 +330,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
is SendMode.QUOTE -> {
val messageContent: MessageContent? =
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
val textMsg = messageContent?.body
val finalText = legacyRiotQuoteText(textMsg, action.text)
@ -348,8 +366,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
}
}
// Handle slash command
}
private fun legacyRiotQuoteText(quotedText: String?, myText: String): String {
@ -371,7 +387,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) {
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled)
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
room.updateTopic(changeTopic.topic, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
@ -385,7 +401,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
private fun handleInviteSlashCommand(invite: ParsedCommand.Invite) {
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled)
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
room.invite(invite.userId, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
@ -440,14 +456,14 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
room.sendMedias(attachments)
}
private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) {
private fun handleEventVisible(action: RoomDetailActions.TimelineEventTurnsVisible) {
if (action.event.root.sendState.isSent()) { //ignore pending/local events
displayedEventsObservable.accept(action)
visibleEventsObservable.accept(action)
}
//We need to update this with the related m.replace also (to move read receipt)
action.event.annotations?.editSummary?.sourceEvents?.forEach {
room.getTimeLineEvent(it)?.let { event ->
displayedEventsObservable.accept(RoomDetailActions.EventDisplayed(event))
visibleEventsObservable.accept(RoomDetailActions.TimelineEventTurnsVisible(event))
}
}
}
@ -490,11 +506,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
}
data class DownloadFileState(
val mimeType: String,
val file: File?,
val throwable: Throwable?
)
private fun handleDownloadFile(action: RoomDetailActions.DownloadFile) {
session.downloadFile(
@ -526,53 +537,15 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private fun handleNavigateToEvent(action: RoomDetailActions.NavigateToEvent) {
val targetEventId = action.eventId
if (action.position != null) {
// Event is already in RAM
withState {
if (it.eventId == targetEventId) {
// ensure another click on the same permalink will also do a scroll
setState {
copy(
eventId = null
)
}
}
setState {
copy(
eventId = targetEventId
)
}
}
_navigateToEvent.postLiveEvent(targetEventId)
} else {
// change timeline
timeline.dispose()
timeline = room.createTimeline(targetEventId, allowedTypes)
timeline.start()
withState {
if (it.eventId == targetEventId) {
// ensure another click on the same permalink will also do a scroll
setState {
copy(
eventId = null
)
}
}
setState {
copy(
eventId = targetEventId,
timeline = this@RoomDetailViewModel.timeline
)
}
}
_navigateToEvent.postLiveEvent(targetEventId)
val indexOfEvent = timeline.getIndexOfEvent(targetEventId)
if (indexOfEvent == null) {
// Event is not already in RAM
timeline.restartWithEventId(targetEventId)
}
if (action.highlight) {
setState { copy(highlightedEventId = targetEventId) }
}
_navigateToEvent.postLiveEvent(targetEventId)
}
private fun handleResendEvent(action: RoomDetailActions.ResendMessage) {
@ -618,7 +591,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private fun observeEventDisplayedActions() {
// We are buffering scroll events for one second
// and keep the most recent one to set the read receipt on.
displayedEventsObservable
visibleEventsObservable
.buffer(1, TimeUnit.SECONDS)
.filter { it.isNotEmpty() }
.subscribeBy(onNext = { actions ->
@ -630,6 +603,68 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
.disposeOnClear()
}
private fun handleSetReadMarkerAction(action: RoomDetailActions.SetReadMarkerAction) = withState { state ->
var readMarkerId = action.eventId
if (readMarkerId == state.asyncRoomSummary()?.readMarkerId) {
val indexOfEvent = timeline.getIndexOfEvent(action.eventId)
// force to set the read marker on the next event
if (indexOfEvent != null) {
timeline.getTimelineEventAtIndex(indexOfEvent - 1)?.root?.eventId?.also { eventIdOfNext ->
readMarkerId = eventIdOfNext
}
}
}
room.setReadMarker(readMarkerId, callback = object : MatrixCallback<Unit> {})
}
private fun handleMarkAllAsRead() {
room.markAllAsRead(object : MatrixCallback<Any> {})
}
private fun observeSyncState() {
session.rx()
.liveSyncState()
.subscribe { syncState ->
setState {
copy(syncState = syncState)
}
}
.disposeOnClear()
}
private fun observeJumpToReadMarkerViewVisibility() {
Observable
.combineLatest(
room.rx().liveRoomSummary(),
visibleEventsObservable.distinctUntilChanged(),
isEventVisibleObservable { it.hasReadMarker }.startWith(false),
Function3<RoomSummary, RoomDetailActions.TimelineEventTurnsVisible, Boolean, Boolean> { roomSummary, currentVisibleEvent, isReadMarkerViewVisible ->
val readMarkerId = roomSummary.readMarkerId
if (readMarkerId == null || isReadMarkerViewVisible || !timeline.isLive) {
false
} else {
val readMarkerPosition = timeline.getIndexOfEvent(readMarkerId)
?: Int.MAX_VALUE
val currentVisibleEventPosition = timeline.getIndexOfEvent(currentVisibleEvent.event.root.eventId)
?: Int.MIN_VALUE
readMarkerPosition > currentVisibleEventPosition
}
}
)
.distinctUntilChanged()
.subscribe {
setState { copy(showJumpToReadMarker = it) }
}
.disposeOnClear()
}
private fun isEventVisibleObservable(filterEvent: (TimelineEvent) -> Boolean): Observable<Boolean> {
return Observable.merge(
visibleEventsObservable.filter { filterEvent(it.event) }.map { true },
invisibleEventsObservable.filter { filterEvent(it.event) }.map { false }
)
}
private fun observeRoomSummary() {
room.rx().liveRoomSummary()
.execute { async ->

View File

@ -21,9 +21,9 @@ import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent
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.sync.SyncState
import im.vector.matrix.android.api.session.user.model.User
/**
@ -50,7 +50,10 @@ data class RoomDetailViewState(
val sendMode: SendMode = SendMode.REGULAR,
val isEncrypted: Boolean = false,
val tombstoneEvent: Event? = null,
val tombstoneEventHandling: Async<String> = Uninitialized
val tombstoneEventHandling: Async<String> = Uninitialized,
val syncState: SyncState = SyncState.IDLE,
val showJumpToReadMarker: Boolean = false,
val highlightedEventId: String? = null
) : MvRxState {
constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId)

View File

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

View File

@ -16,13 +16,14 @@
package im.vector.riotx.features.home.room.detail
import androidx.annotation.StringRes
import im.vector.riotx.features.command.Command
sealed class SendMessageResult {
object MessageSent : SendMessageResult()
class SlashCommandError(val command: Command) : SendMessageResult()
class SlashCommandUnknown(val command: String) : SendMessageResult()
object SlashCommandHandled : SendMessageResult()
data class SlashCommandHandled(@StringRes val messageRes: Int? = null) : SendMessageResult()
object SlashCommandResultOk : SendMessageResult()
class SlashCommandResultError(val throwable: Throwable) : SendMessageResult()
// TODO Remove

View File

@ -0,0 +1,55 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.home.room.detail.readreceipts
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.airbnb.epoxy.EpoxyModelWithHolder
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.features.home.AvatarRenderer
@EpoxyModelClass(layout = R.layout.item_display_read_receipt)
abstract class DisplayReadReceiptItem : EpoxyModelWithHolder<DisplayReadReceiptItem.Holder>() {
@EpoxyAttribute var name: String? = null
@EpoxyAttribute var userId: String = ""
@EpoxyAttribute var avatarUrl: String? = null
@EpoxyAttribute var timestamp: CharSequence? = null
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
override fun bind(holder: Holder) {
avatarRenderer.render(avatarUrl, userId, name, holder.avatarView)
holder.displayNameView.text = name ?: userId
timestamp?.let {
holder.timestampView.text = it
holder.timestampView.isVisible = true
} ?: run {
holder.timestampView.isVisible = false
}
}
class Holder : VectorEpoxyHolder() {
val avatarView by bind<ImageView>(R.id.readReceiptAvatar)
val displayNameView by bind<TextView>(R.id.readReceiptName)
val timestampView by bind<TextView>(R.id.readReceiptDate)
}
}

View File

@ -0,0 +1,91 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.home.room.detail.readreceipts
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.recyclerview.widget.DividerItemDecoration
import butterknife.BindView
import butterknife.ButterKnife
import com.airbnb.epoxy.EpoxyRecyclerView
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.args
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.features.home.room.detail.timeline.action.VectorBaseBottomSheetDialogFragment
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.*
import javax.inject.Inject
@Parcelize
data class DisplayReadReceiptArgs(
val readReceipts: List<ReadReceiptData>
) : Parcelable
/**
* Bottom sheet displaying list of read receipts for a given event ordered by descending timestamp
*/
class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() {
@Inject lateinit var epoxyController: DisplayReadReceiptsController
@BindView(R.id.bottom_sheet_display_reactions_list)
lateinit var epoxyRecyclerView: EpoxyRecyclerView
private val displayReadReceiptArgs: DisplayReadReceiptArgs by args()
override fun injectWith(screenComponent: ScreenComponent) {
screenComponent.inject(this)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.bottom_sheet_epoxylist_with_title, container, false)
ButterKnife.bind(this, view)
return view
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
epoxyRecyclerView.setController(epoxyController)
val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context,
LinearLayout.VERTICAL)
epoxyRecyclerView.addItemDecoration(dividerItemDecoration)
bottomSheetTitle.text = getString(R.string.read_receipts_list)
epoxyController.setData(displayReadReceiptArgs.readReceipts)
}
override fun invalidate() {
// we are not using state for this one as it's static
}
companion object {
fun newInstance(readReceipts: List<ReadReceiptData>): DisplayReadReceiptsBottomSheet {
val args = Bundle()
val parcelableArgs = DisplayReadReceiptArgs(
readReceipts
)
args.putParcelable(MvRx.KEY_ARG, parcelableArgs)
return DisplayReadReceiptsBottomSheet().apply { arguments = args }
}
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.home.room.detail.readreceipts
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.session.Session
import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
import javax.inject.Inject
/**
* Epoxy controller for read receipt event list
*/
class DisplayReadReceiptsController @Inject constructor(private val dateFormatter: VectorDateFormatter,
private val session: Session,
private val avatarRender: AvatarRenderer)
: TypedEpoxyController<List<ReadReceiptData>>() {
override fun buildModels(readReceipts: List<ReadReceiptData>) {
readReceipts.forEach {
val timestamp = dateFormatter.formatRelativeDateTime(it.timestamp)
DisplayReadReceiptItem_()
.id(it.userId)
.userId(it.userId)
.avatarUrl(it.avatarUrl)
.name(it.displayName)
.avatarRenderer(avatarRender)
.timestamp(timestamp)
.addIf(session.myUserId != it.userId, this)
}
}
}

View File

@ -27,6 +27,7 @@ import com.airbnb.epoxy.EpoxyModel
import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.core.epoxy.LoadingItem_
import im.vector.riotx.core.extensions.localDateTime
import im.vector.riotx.core.resources.UserPreferencesProvider
@ -37,21 +38,22 @@ import im.vector.riotx.features.home.room.detail.timeline.item.DaySeparatorItem
import im.vector.riotx.features.home.room.detail.timeline.item.DaySeparatorItem_
import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.riotx.features.media.ImageContentRenderer
import im.vector.riotx.features.media.VideoContentRenderer
import org.threeten.bp.LocalDateTime
import javax.inject.Inject
class TimelineEventController @Inject constructor(private val dateFormatter: TimelineDateFormatter,
class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter,
private val timelineItemFactory: TimelineItemFactory,
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
private val avatarRenderer: AvatarRenderer,
@TimelineEventControllerHandler
private val backgroundHandler: Handler,
userPreferencesProvider: UserPreferencesProvider
private val backgroundHandler: Handler
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener {
interface Callback : ReactionPillCallback, AvatarCallback, BaseCallback, UrlClickCallback {
interface Callback : BaseCallback, ReactionPillCallback, AvatarCallback, UrlClickCallback, ReadReceiptsCallback {
fun onEventInvisible(event: TimelineEvent)
fun onEventVisible(event: TimelineEvent)
fun onRoomCreateLinkClicked(url: String)
fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View)
@ -77,6 +79,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
fun onMemberNameClicked(informationData: MessageInformationData)
}
interface ReadReceiptsCallback {
fun onReadReceiptsClicked(readReceipts: List<ReadReceiptData>)
fun onReadMarkerLongDisplayed(informationData: MessageInformationData)
}
interface UrlClickCallback {
fun onUrlClicked(url: String): Boolean
fun onUrlLongClicked(url: String): Boolean
@ -134,8 +141,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
}
}
private val showHiddenEvents = userPreferencesProvider.shouldShowHiddenEvents()
init {
requestModelBuild()
}
@ -158,7 +163,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
synchronized(modelCache) {
for (i in 0 until modelCache.size) {
if (modelCache[i]?.eventId == eventIdToHighlight
|| modelCache[i]?.eventId == this.eventIdToHighlight) {
|| modelCache[i]?.eventId == this.eventIdToHighlight) {
modelCache[i] = null
}
}
@ -219,8 +224,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
// Should be build if not cached or if cached but contains mergedHeader or formattedDay
// We then are sure we always have items up to date.
if (modelCache[position] == null
|| modelCache[position]?.mergedHeaderModel != null
|| modelCache[position]?.formattedDayModel != null) {
|| modelCache[position]?.mergedHeaderModel != null
|| modelCache[position]?.formattedDayModel != null) {
modelCache[position] = buildItemModels(position, currentSnapshot)
}
}
@ -241,7 +246,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
private fun buildItemModels(currentPosition: Int, items: List<TimelineEvent>): CacheItemData {
val event = items[currentPosition]
val nextEvent = items.nextDisplayableEvent(currentPosition, showHiddenEvents)
val nextEvent = items.nextOrNull(currentPosition)
val date = event.root.localDateTime()
val nextDate = nextEvent?.root?.localDateTime()
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
@ -293,7 +298,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
// We try to find if one of the item id were used as mergeItemCollapseStates key
// => handle case where paginating from mergeable events and we get more
val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull()
val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey) ?: true
val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey)
?: true
val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState }
if (isCollapsed) {
collapsedEventIds.addAll(mergedEventIds)
@ -320,24 +326,50 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
return shouldAdd
}
fun searchPositionOfEvent(eventId: String): Int? {
synchronized(modelCache) {
// Search in the cache
modelCache.forEachIndexed { idx, cacheItemData ->
if (cacheItemData?.eventId == eventId) {
return idx
}
fun searchPositionOfEvent(eventId: String): Int? = synchronized(modelCache) {
// Search in the cache
var realPosition = 0
for (i in 0 until modelCache.size) {
val itemCache = modelCache[i]
if (itemCache?.eventId == eventId) {
return realPosition
}
if (itemCache?.eventModel != null) {
realPosition++
}
if (itemCache?.mergedHeaderModel != null) {
realPosition++
}
if (itemCache?.formattedDayModel != null) {
realPosition++
}
return null
}
return null
}
}
private data class CacheItemData(
val localId: Long,
val eventId: String?,
val eventModel: EpoxyModel<*>? = null,
val mergedHeaderModel: MergedHeaderItem? = null,
val formattedDayModel: DaySeparatorItem? = null
)
fun searchEventIdAtPosition(position: Int): String? = synchronized(modelCache) {
var offsetValue = 0
for (i in 0 until position) {
val itemCache = modelCache[i]
if (itemCache?.eventModel == null) {
offsetValue--
}
if (itemCache?.mergedHeaderModel != null) {
offsetValue++
}
if (itemCache?.formattedDayModel != null) {
offsetValue++
}
}
return modelCache.getOrNull(position - offsetValue)?.eventId
}
private data class CacheItemData(
val localId: Long,
val eventId: String?,
val eventModel: EpoxyModel<*>? = null,
val mergedHeaderModel: MergedHeaderItem? = null,
val formattedDayModel: DaySeparatorItem? = null
)
}

View File

@ -49,7 +49,7 @@ class ViewEditHistoryBottomSheet : VectorBaseBottomSheetDialogFragment() {
lateinit var epoxyRecyclerView: EpoxyRecyclerView
private val epoxyController by lazy {
ViewEditHistoryEpoxyController(requireContext(), viewModel.timelineDateFormatter, eventHtmlRenderer)
ViewEditHistoryEpoxyController(requireContext(), viewModel.dateFormatter, eventHtmlRenderer)
}
override fun injectWith(screenComponent: ScreenComponent) {

View File

@ -33,7 +33,7 @@ import im.vector.riotx.core.ui.list.genericFooterItem
import im.vector.riotx.core.ui.list.genericItem
import im.vector.riotx.core.ui.list.genericItemHeader
import im.vector.riotx.core.ui.list.genericLoaderItem
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFormatter
import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.features.html.EventHtmlRenderer
import me.gujun.android.span.span
import name.fraser.neil.plaintext.diff_match_patch
@ -44,7 +44,7 @@ import java.util.*
* Epoxy controller for reaction event list
*/
class ViewEditHistoryEpoxyController(private val context: Context,
val timelineDateFormatter: TimelineDateFormatter,
val dateFormatter: VectorDateFormatter,
val eventHtmlRenderer: EventHtmlRenderer) : TypedEpoxyController<ViewEditHistoryViewState>() {
override fun buildModels(state: ViewEditHistoryViewState) {
@ -84,7 +84,7 @@ class ViewEditHistoryEpoxyController(private val context: Context,
if (lastDate?.get(Calendar.DAY_OF_YEAR) != evDate.get(Calendar.DAY_OF_YEAR)) {
//need to display header with day
val dateString = if (DateUtils.isToday(evDate.timeInMillis)) context.getString(R.string.today)
else timelineDateFormatter.formatMessageDay(timelineEvent.localDateTime())
else dateFormatter.formatMessageDay(timelineEvent.localDateTime())
genericItemHeader {
id(evDate.hashCode())
text(dateString)
@ -136,7 +136,7 @@ class ViewEditHistoryEpoxyController(private val context: Context,
}
genericItem {
id(timelineEvent.eventId)
title(timelineDateFormatter.formatMessageHour(timelineEvent.localDateTime()))
title(dateFormatter.formatMessageHour(timelineEvent.localDateTime()))
description(spannedDiff ?: body)
}
}

View File

@ -27,7 +27,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.isReply
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFormatter
import im.vector.riotx.core.date.VectorDateFormatter
import timber.log.Timber
import java.util.*
@ -46,7 +46,7 @@ data class ViewEditHistoryViewState(
class ViewEditHistoryViewModel @AssistedInject constructor(@Assisted
initialState: ViewEditHistoryViewState,
val session: Session,
val timelineDateFormatter: TimelineDateFormatter
val dateFormatter: VectorDateFormatter
) : VectorViewModel<ViewEditHistoryViewState>(initialState) {
private val roomId = initialState.roomId

View File

@ -16,16 +16,20 @@
package im.vector.riotx.features.home.room.detail.timeline.action
import com.airbnb.mvrx.*
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.ReactionAggregatedSummary
import im.vector.matrix.rx.RxRoom
import im.vector.riotx.core.extensions.localDateTime
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.utils.isSingleEmoji
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFormatter
import im.vector.riotx.core.date.VectorDateFormatter
import io.reactivex.Observable
import io.reactivex.Single
@ -54,13 +58,13 @@ data class ReactionInfo(
class ViewReactionViewModel @AssistedInject constructor(@Assisted
initialState: DisplayReactionsViewState,
private val session: Session,
private val timelineDateFormatter: TimelineDateFormatter
private val dateFormatter: VectorDateFormatter
) : VectorViewModel<DisplayReactionsViewState>(initialState) {
private val roomId = initialState.roomId
private val eventId = initialState.eventId
private val room = session.getRoom(roomId)
?: throw IllegalStateException("Shouldn't use this ViewModel without a room")
?: throw IllegalStateException("Shouldn't use this ViewModel without a room")
@AssistedInject.Factory
interface Factory {
@ -100,14 +104,14 @@ class ViewReactionViewModel @AssistedInject constructor(@Assisted
.fromIterable(summary.sourceEvents)
.map {
val event = room.getTimeLineEvent(it)
?: throw RuntimeException("Your eventId is not valid")
val localDate = event.root.localDateTime()
?: throw RuntimeException("Your eventId is not valid")
ReactionInfo(
event.root.eventId!!,
summary.key,
event.root.senderId ?: "",
event.getDisambiguatedDisplayName(),
timelineDateFormatter.formatMessageHour(localDate)
dateFormatter.formatRelativeDateTime(event.root.originServerTs)
)
}
}.toList()

View File

@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.detail.timeline.action
import android.content.Context
import android.graphics.Typeface
import android.text.format.DateUtils
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete

View File

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

View File

@ -1,71 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.home.room.detail.timeline.factory
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent
import im.vector.riotx.R
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.helper.senderAvatar
import im.vector.riotx.features.home.room.detail.timeline.helper.senderName
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem
import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem_
import javax.inject.Inject
class EncryptionItemFactory @Inject constructor(private val stringProvider: StringProvider,
private val avatarRenderer: AvatarRenderer) {
fun create(event: TimelineEvent,
highlight: Boolean,
callback: TimelineEventController.BaseCallback?): NoticeItem? {
val text = buildNoticeText(event.root, event.senderName) ?: return null
val informationData = MessageInformationData(
eventId = event.root.eventId ?: "?",
senderId = event.root.senderId ?: "",
sendState = event.root.sendState,
avatarUrl = event.senderAvatar(),
memberName = event.senderName(),
showInformation = false
)
return NoticeItem_()
.avatarRenderer(avatarRenderer)
.noticeText(text)
.informationData(informationData)
.highlighted(highlight)
.baseCallback(callback)
}
private fun buildNoticeText(event: Event, senderName: String?): CharSequence? {
return when {
EventType.ENCRYPTION == event.getClearType() -> {
val content = event.content.toModel<EncryptionEventContent>() ?: return null
stringProvider.getString(R.string.notice_end_to_end, senderName, content.algorithm)
}
else -> null
}
}
}

View File

@ -28,8 +28,15 @@ import im.vector.matrix.android.api.permalinks.MatrixLinkify
import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan
import im.vector.matrix.android.api.session.events.model.RelationType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageEmoteContent
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
import im.vector.matrix.android.api.session.room.model.message.MessageNoticeContent
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
import im.vector.matrix.android.api.session.room.model.message.getFileUrl
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
@ -40,15 +47,14 @@ import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.linkify.VectorLinkify
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.resources.UserPreferencesProvider
import im.vector.riotx.core.utils.DebouncedClickListener
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
import im.vector.riotx.features.home.room.detail.timeline.helper.senderAvatar
import im.vector.riotx.features.home.room.detail.timeline.item.*
import im.vector.riotx.features.home.room.detail.timeline.util.MessageInformationDataFactory
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
import im.vector.riotx.features.html.EventHtmlRenderer
import im.vector.riotx.features.media.ImageContentRenderer
import im.vector.riotx.features.media.VideoContentRenderer
@ -56,16 +62,15 @@ import me.gujun.android.span.span
import javax.inject.Inject
class MessageItemFactory @Inject constructor(
private val avatarRenderer: AvatarRenderer,
private val colorProvider: ColorProvider,
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
private val htmlRenderer: Lazy<EventHtmlRenderer>,
private val stringProvider: StringProvider,
private val emojiCompatFontProvider: EmojiCompatFontProvider,
private val imageContentRenderer: ImageContentRenderer,
private val messageInformationDataFactory: MessageInformationDataFactory,
private val messageItemAttributesFactory: MessageItemAttributesFactory,
private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder,
private val userPreferencesProvider: UserPreferencesProvider) {
private val noticeItemFactory: NoticeItemFactory) {
fun create(event: TimelineEvent,
@ -79,7 +84,8 @@ class MessageItemFactory @Inject constructor(
if (event.root.isRedacted()) {
//message is redacted
return buildRedactedItem(informationData, highlight, callback)
val attributes = messageItemAttributesFactory.create(null, informationData, callback)
return buildRedactedItem(attributes, highlight)
}
val messageContent: MessageContent =
@ -90,46 +96,29 @@ class MessageItemFactory @Inject constructor(
if (messageContent.relatesTo?.type == RelationType.REPLACE
|| event.isEncrypted() && event.root.content.toModel<EncryptedEventContent>()?.relatesTo?.type == RelationType.REPLACE
) {
// ignore replace event, the targeted id is already edited
if (userPreferencesProvider.shouldShowHiddenEvents()) {
//These are just for debug to display hidden event, they should be filtered out in normal mode
val informationData = MessageInformationData(
eventId = event.root.eventId ?: "?",
senderId = event.root.senderId ?: "",
sendState = event.root.sendState,
time = "",
avatarUrl = event.senderAvatar(),
memberName = "",
showInformation = false
)
return NoticeItem_()
.avatarRenderer(avatarRenderer)
.informationData(informationData)
.noticeText("{ \"type\": ${event.root.getClearType()} }")
.highlighted(highlight)
.baseCallback(callback)
} else {
return BlankItem_()
}
// This is an edit event, we should it when debugging as a notice event
return noticeItemFactory.create(event, highlight, callback)
}
val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback)
// val all = event.root.toContent()
// val ev = all.toModel<Event>()
return when (messageContent) {
is MessageEmoteContent -> buildEmoteMessageItem(messageContent,
informationData,
highlight,
callback)
is MessageTextContent -> buildTextMessageItem(event.root.sendState,
messageContent,
callback,
attributes)
is MessageTextContent -> buildTextMessageItem(messageContent,
informationData,
highlight,
callback
)
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback)
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback)
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback)
is MessageFileContent -> buildFileMessageItem(messageContent, informationData, highlight, callback)
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, callback)
callback,
attributes)
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageFileContent -> buildFileMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, callback, attributes)
else -> buildNotHandledMessageItem(messageContent, highlight)
}
}
@ -137,61 +126,33 @@ class MessageItemFactory @Inject constructor(
private fun buildAudioMessageItem(messageContent: MessageAudioContent,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?): MessageFileItem? {
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageFileItem? {
return MessageFileItem_()
.avatarRenderer(avatarRenderer)
.colorProvider(colorProvider)
.informationData(informationData)
.attributes(attributes)
.highlighted(highlight)
.avatarCallback(callback)
.filename(messageContent.body)
.iconRes(R.drawable.filetype_audio)
.reactionPillCallback(callback)
.emojiTypeFace(emojiCompatFontProvider.typeface)
.cellClickListener(
DebouncedClickListener(View.OnClickListener { view: View ->
callback?.onEventCellClicked(informationData, messageContent, view)
}))
.clickListener(
DebouncedClickListener(View.OnClickListener {
callback?.onAudioMessageClicked(messageContent)
}))
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false
}
}
private fun buildFileMessageItem(messageContent: MessageFileContent,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?): MessageFileItem? {
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageFileItem? {
return MessageFileItem_()
.avatarRenderer(avatarRenderer)
.colorProvider(colorProvider)
.informationData(informationData)
.attributes(attributes)
.highlighted(highlight)
.avatarCallback(callback)
.filename(messageContent.body)
.reactionPillCallback(callback)
.emojiTypeFace(emojiCompatFontProvider.typeface)
.iconRes(R.drawable.filetype_attachment)
.cellClickListener(
DebouncedClickListener(View.OnClickListener { view ->
callback?.onEventCellClicked(informationData, messageContent, view)
}))
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false
}
.clickListener(
DebouncedClickListener(View.OnClickListener { _ ->
callback?.onFileMessageClicked(informationData.eventId, messageContent)
}))
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false
}
}
private fun buildNotHandledMessageItem(messageContent: MessageContent, highlight: Boolean): DefaultItem? {
@ -204,11 +165,11 @@ class MessageItemFactory @Inject constructor(
private fun buildImageMessageItem(messageContent: MessageImageContent,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?): MessageImageVideoItem? {
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageImageVideoItem? {
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
val data = ImageContentRenderer.Data(
eventId = informationData.eventId,
filename = messageContent.body,
url = messageContent.getFileUrl(),
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
@ -220,39 +181,26 @@ class MessageItemFactory @Inject constructor(
rotation = messageContent.info?.rotation
)
return MessageImageVideoItem_()
.avatarRenderer(avatarRenderer)
.colorProvider(colorProvider)
.attributes(attributes)
.imageContentRenderer(imageContentRenderer)
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.playable(messageContent.info?.mimeType == "image/gif")
.informationData(informationData)
.highlighted(highlight)
.avatarCallback(callback)
.mediaData(data)
.reactionPillCallback(callback)
.emojiTypeFace(emojiCompatFontProvider.typeface)
.clickListener(
DebouncedClickListener(View.OnClickListener { view ->
callback?.onImageMessageClicked(messageContent, data, view)
}))
.cellClickListener(
DebouncedClickListener(View.OnClickListener { view ->
callback?.onEventCellClicked(informationData, messageContent, view)
}))
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false
}
}
private fun buildVideoMessageItem(messageContent: MessageVideoContent,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?): MessageImageVideoItem? {
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageImageVideoItem? {
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
val thumbnailData = ImageContentRenderer.Data(
eventId = informationData.eventId,
filename = messageContent.body,
url = messageContent.videoInfo?.thumbnailFile?.url
?: messageContent.videoInfo?.thumbnailUrl,
@ -272,33 +220,20 @@ class MessageItemFactory @Inject constructor(
)
return MessageImageVideoItem_()
.attributes(attributes)
.imageContentRenderer(imageContentRenderer)
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.avatarRenderer(avatarRenderer)
.colorProvider(colorProvider)
.playable(true)
.informationData(informationData)
.highlighted(highlight)
.avatarCallback(callback)
.mediaData(thumbnailData)
.reactionPillCallback(callback)
.emojiTypeFace(emojiCompatFontProvider.typeface)
.cellClickListener(
DebouncedClickListener(View.OnClickListener { view ->
callback?.onEventCellClicked(informationData, messageContent, view)
}))
.clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) }
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false
}
}
private fun buildTextMessageItem(sendState: SendState,
messageContent: MessageTextContent,
private fun buildTextMessageItem(messageContent: MessageTextContent,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?): MessageTextItem? {
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageTextItem? {
val bodyToUse = messageContent.formattedBody?.let {
htmlRenderer.get().render(it.trim())
@ -315,23 +250,10 @@ class MessageItemFactory @Inject constructor(
message(linkifiedBody)
}
}
.avatarRenderer(avatarRenderer)
.informationData(informationData)
.colorProvider(colorProvider)
.attributes(attributes)
.highlighted(highlight)
.avatarCallback(callback)
.urlClickCallback(callback)
.reactionPillCallback(callback)
.emojiTypeFace(emojiCompatFontProvider.typeface)
//click on the text
.cellClickListener(
DebouncedClickListener(View.OnClickListener { view ->
callback?.onEventCellClicked(informationData, messageContent, view)
}))
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false
}
//click on the text
}
private fun annotateWithEdited(linkifiedBody: CharSequence,
@ -369,7 +291,8 @@ class MessageItemFactory @Inject constructor(
private fun buildNoticeMessageItem(messageContent: MessageNoticeContent,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?): MessageTextItem? {
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageTextItem? {
val message = messageContent.body.let {
val formattedBody = span {
@ -380,33 +303,17 @@ class MessageItemFactory @Inject constructor(
linkifyBody(formattedBody, callback)
}
return MessageTextItem_()
.avatarRenderer(avatarRenderer)
.attributes(attributes)
.message(message)
.colorProvider(colorProvider)
.informationData(informationData)
.highlighted(highlight)
.avatarCallback(callback)
.reactionPillCallback(callback)
.urlClickCallback(callback)
.emojiTypeFace(emojiCompatFontProvider.typeface)
.memberClickListener(
DebouncedClickListener(View.OnClickListener { view ->
callback?.onMemberNameClicked(informationData)
}))
.cellClickListener(
DebouncedClickListener(View.OnClickListener { view ->
callback?.onEventCellClicked(informationData, messageContent, view)
}))
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false
}
}
private fun buildEmoteMessageItem(messageContent: MessageEmoteContent,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?): MessageTextItem? {
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageTextItem? {
val message = messageContent.body.let {
val formattedBody = "* ${informationData.memberName} $it"
@ -421,41 +328,16 @@ class MessageItemFactory @Inject constructor(
message(message)
}
}
.avatarRenderer(avatarRenderer)
.colorProvider(colorProvider)
.informationData(informationData)
.attributes(attributes)
.highlighted(highlight)
.avatarCallback(callback)
.reactionPillCallback(callback)
.urlClickCallback(callback)
.emojiTypeFace(emojiCompatFontProvider.typeface)
.cellClickListener(
DebouncedClickListener(View.OnClickListener { view ->
callback?.onEventCellClicked(informationData, messageContent, view)
}))
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false
}
}
private fun buildRedactedItem(informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?): RedactedMessageItem? {
private fun buildRedactedItem(attributes: AbsMessageItem.Attributes,
highlight: Boolean): RedactedMessageItem? {
return RedactedMessageItem_()
.avatarRenderer(avatarRenderer)
.colorProvider(colorProvider)
.informationData(informationData)
.attributes(attributes)
.highlighted(highlight)
.avatarCallback(callback)
.cellClickListener(
DebouncedClickListener(View.OnClickListener { view ->
callback?.onEventCellClicked(informationData, null, view)
}))
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, null, view)
?: false
}
}
private fun linkifyBody(body: CharSequence, callback: TimelineEventController.Callback?): CharSequence {

View File

@ -20,28 +20,20 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter
import im.vector.riotx.features.home.room.detail.timeline.helper.senderAvatar
import im.vector.riotx.features.home.room.detail.timeline.helper.senderName
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory
import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem
import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem_
import javax.inject.Inject
class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEventFormatter,
private val avatarRenderer: AvatarRenderer) {
private val avatarRenderer: AvatarRenderer,
private val informationDataFactory: MessageInformationDataFactory) {
fun create(event: TimelineEvent,
highlight: Boolean,
callback: TimelineEventController.Callback?): NoticeItem? {
val formattedText = eventFormatter.format(event) ?: return null
val informationData = MessageInformationData(
eventId = event.root.eventId ?: "?",
senderId = event.root.senderId ?: "",
sendState = event.root.sendState,
avatarUrl = event.senderAvatar(),
memberName = event.senderName(),
showInformation = false
)
val informationData = informationDataFactory.create(event, null)
return NoticeItem_()
.avatarRenderer(avatarRenderer)
@ -49,6 +41,7 @@ class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEv
.highlighted(highlight)
.informationData(informationData)
.baseCallback(callback)
.readReceiptsCallback(callback)
}

View File

@ -20,26 +20,21 @@ import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotx.core.epoxy.EmptyItem_
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.helper.senderAvatar
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem_
import timber.log.Timber
import javax.inject.Inject
class TimelineItemFactory @Inject constructor(private val messageItemFactory: MessageItemFactory,
private val encryptionItemFactory: EncryptionItemFactory,
private val encryptedItemFactory: EncryptedItemFactory,
private val noticeItemFactory: NoticeItemFactory,
private val defaultItemFactory: DefaultItemFactory,
private val roomCreateItemFactory: RoomCreateItemFactory,
private val avatarRenderer: AvatarRenderer) {
private val roomCreateItemFactory: RoomCreateItemFactory) {
fun create(event: TimelineEvent,
nextEvent: TimelineEvent?,
eventIdToHighlight: String?,
callback: TimelineEventController.Callback?): VectorEpoxyModel<*> {
val highlight = event.root.eventId == eventIdToHighlight
val computedModel = try {
@ -53,11 +48,13 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
EventType.STATE_HISTORY_VISIBILITY,
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
EventType.CALL_ANSWER -> noticeItemFactory.create(event, highlight, callback)
EventType.CALL_ANSWER,
EventType.REACTION,
EventType.REDACTION,
EventType.ENCRYPTION -> noticeItemFactory.create(event, highlight, callback)
// State room create
EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback)
// Crypto
EventType.ENCRYPTION -> encryptionItemFactory.create(event, highlight, callback)
EventType.ENCRYPTED -> {
if (event.root.isRedacted()) {
// Redacted event, let the MessageItemFactory handle it
@ -70,24 +67,9 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
// Unhandled event types (yet)
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
EventType.STICKER -> defaultItemFactory.create(event, highlight)
else -> {
//These are just for debug to display hidden event, they should be filtered out in normal mode
val informationData = MessageInformationData(
eventId = event.root.eventId ?: "?",
senderId = event.root.senderId ?: "",
sendState = event.root.sendState,
time = "",
avatarUrl = event.senderAvatar(),
memberName = "",
showInformation = false
)
NoticeItem_()
.avatarRenderer(avatarRenderer)
.informationData(informationData)
.noticeText("{ \"type\": ${event.root.getClearType()} }")
.highlighted(highlight)
.baseCallback(callback)
Timber.v("Type ${event.root.getClearType()} not handled")
null
}
}
} catch (e: Exception) {

View File

@ -22,8 +22,8 @@ import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.*
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent
import im.vector.riotx.R
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.home.room.detail.timeline.helper.senderName
@ -42,6 +42,10 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
EventType.ENCRYPTION -> formatEncryptionEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
EventType.MESSAGE,
EventType.REACTION,
EventType.REDACTION -> formatDebug(timelineEvent.root)
else -> {
Timber.v("Type $type not handled by this formatter")
null
@ -58,6 +62,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
EventType.CALL_ANSWER -> formatCallEvent(event, senderName)
EventType.ENCRYPTION -> formatEncryptionEvent(event, senderName)
EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(event, senderName)
else -> {
Timber.v("Type $type not handled by this formatter")
@ -66,6 +71,10 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
}
}
private fun formatDebug(event: Event): CharSequence? {
return "{ \"type\": ${event.getClearType()} }"
}
private fun formatRoomNameEvent(event: Event, senderName: String?): CharSequence? {
val content = event.getClearContent().toModel<RoomNameContent>() ?: return null
return if (!TextUtils.isEmpty(content.name)) {
@ -203,4 +212,9 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
}
}
private fun formatEncryptionEvent(event: Event, senderName: String?): CharSequence? {
val eventContent: EncryptionEventContent = event.getClearContent().toModel() ?: return null
return stringProvider.getString(R.string.notice_end_to_end, senderName, eventContent.algorithm)
}
}

View File

@ -1,21 +1,24 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.home.room.detail.timeline.util
package im.vector.riotx.features.home.room.detail.timeline.helper
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited
@ -23,16 +26,18 @@ import im.vector.riotx.core.extensions.localDateTime
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.utils.isSingleEmoji
import im.vector.riotx.features.home.getColorFromUserId
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFormatter
import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.home.room.detail.timeline.item.ReactionInfoData
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
import me.gujun.android.span.span
import javax.inject.Inject
/**
* This class compute if data of an event (such has avatar, display name, ...) should be displayed, depending on the previous event in the timeline
*/
class MessageInformationDataFactory @Inject constructor(private val timelineDateFormatter: TimelineDateFormatter,
class MessageInformationDataFactory @Inject constructor(private val session: Session,
private val dateFormatter: VectorDateFormatter,
private val colorProvider: ColorProvider) {
fun create(event: TimelineEvent, nextEvent: TimelineEvent?): MessageInformationData {
@ -43,23 +48,25 @@ class MessageInformationDataFactory @Inject constructor(private val timelineDate
val nextDate = nextEvent?.root?.localDateTime()
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
val isNextMessageReceivedMoreThanOneHourAgo = nextDate?.isBefore(date.minusMinutes(60))
?: false
?: false
val showInformation =
addDaySeparator
|| event.senderAvatar != nextEvent?.senderAvatar
|| event.getDisambiguatedDisplayName() != nextEvent?.getDisambiguatedDisplayName()
|| (nextEvent?.root?.getClearType() != EventType.MESSAGE && nextEvent?.root?.getClearType() != EventType.ENCRYPTED)
|| isNextMessageReceivedMoreThanOneHourAgo
|| event.senderAvatar != nextEvent?.senderAvatar
|| event.getDisambiguatedDisplayName() != nextEvent?.getDisambiguatedDisplayName()
|| (nextEvent.root.getClearType() != EventType.MESSAGE && nextEvent.root.getClearType() != EventType.ENCRYPTED)
|| isNextMessageReceivedMoreThanOneHourAgo
val time = timelineDateFormatter.formatMessageHour(date)
val time = dateFormatter.formatMessageHour(date)
val avatarUrl = event.senderAvatar
val memberName = event.getDisambiguatedDisplayName()
val formattedMemberName = span(memberName) {
textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId
?: ""))
textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId ?: ""))
}
val displayReadMarker = event.hasReadMarker
&& event.readReceipts.find { it.user.userId == session.myUserId } == null
return MessageInformationData(
eventId = eventId,
senderId = event.root.senderId ?: "",
@ -74,7 +81,17 @@ class MessageInformationDataFactory @Inject constructor(private val timelineDate
ReactionInfoData(it.key, it.count, it.addedByMe, it.localEchoEvents.isEmpty())
},
hasBeenEdited = event.hasBeenEdited(),
hasPendingEdits = event.annotations?.editSummary?.localEchos?.any() ?: false
hasPendingEdits = event.annotations?.editSummary?.localEchos?.any() ?: false,
readReceipts = event.readReceipts
.asSequence()
.filter {
it.user.userId != session.myUserId
}
.map {
ReadReceiptData(it.user.userId, it.user.avatarUrl, it.user.displayName, it.originServerTs)
}
.toList(),
displayReadMarker = displayReadMarker
)
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -24,7 +24,10 @@ import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.platform.CheckableView
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.utils.DimensionUtils.dpToPx
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
/**
* Children must override getViewType()

View File

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

View File

@ -32,7 +32,9 @@ data class MessageInformationData(
/*List of reactions (emoji,count,isSelected)*/
val orderedReactionList: List<ReactionInfoData>? = null,
val hasBeenEdited: Boolean = false,
val hasPendingEdits: Boolean = false
val hasPendingEdits: Boolean = false,
val readReceipts: List<ReadReceiptData> = emptyList(),
val displayReadMarker: Boolean = false
) : Parcelable
@ -43,3 +45,11 @@ data class ReactionInfoData(
val addedByMe: Boolean,
val synced: Boolean
) : Parcelable
@Parcelize
data class ReadReceiptData(
val userId: String,
val avatarUrl: String?,
val displayName: String?,
val timestamp: Long
) : Parcelable

View File

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

View File

@ -22,6 +22,9 @@ import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.ui.views.ReadMarkerView
import im.vector.riotx.core.ui.views.ReadReceiptsView
import im.vector.riotx.core.utils.DebouncedClickListener
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
@ -44,6 +47,19 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
return@OnLongClickListener baseCallback?.onEventLongClicked(informationData, null, it) == true
}
@EpoxyAttribute
var readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null
private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener {
readReceiptsCallback?.onReadReceiptsClicked(informationData.readReceipts)
})
private val _readMarkerCallback = object : ReadMarkerView.Callback {
override fun onReadMarkerDisplayed() {
readReceiptsCallback?.onReadMarkerLongDisplayed(informationData)
}
}
override fun bind(holder: Holder) {
super.bind(holder)
holder.noticeTextView.text = noticeText
@ -51,10 +67,17 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
informationData.avatarUrl,
informationData.senderId,
informationData.memberName?.toString()
?: informationData.senderId,
?: informationData.senderId,
holder.avatarImageView
)
holder.view.setOnLongClickListener(longClickListener)
holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener)
holder.readMarkerView.bindView(informationData, _readMarkerCallback)
}
override fun unbind(holder: Holder) {
holder.readMarkerView.unbind()
super.unbind(holder)
}
override fun getViewType() = STUB_ID
@ -62,6 +85,8 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
class Holder : BaseHolder(STUB_ID) {
val avatarImageView by bind<ImageView>(R.id.itemNoticeAvatarView)
val noticeTextView by bind<TextView>(R.id.itemNoticeTextView)
val readReceiptsView by bind<ReadReceiptsView>(R.id.readReceiptsView)
val readMarkerView by bind<ReadMarkerView>(R.id.readMarkerView)
}
companion object {

View File

@ -121,7 +121,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
when (newState) {
RecyclerView.SCROLL_STATE_IDLE -> {
createChatFabMenu.postDelayed(showFabRunnable, 1000)
createChatFabMenu.postDelayed(showFabRunnable, 250)
}
RecyclerView.SCROLL_STATE_DRAGGING,
RecyclerView.SCROLL_STATE_SETTLING -> {

View File

@ -29,13 +29,13 @@ import im.vector.riotx.core.resources.DateProvider
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFormatter
import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.features.home.room.detail.timeline.helper.senderName
import me.gujun.android.span.span
import javax.inject.Inject
class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatter: NoticeEventFormatter,
private val timelineDateFormatter: TimelineDateFormatter,
private val dateFormatter: VectorDateFormatter,
private val colorProvider: ColorProvider,
private val stringProvider: StringProvider,
private val avatarRenderer: AvatarRenderer) {
@ -94,7 +94,7 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte
val currentDate = DateProvider.currentLocalDateTime()
val isSameDay = date.toLocalDate() == currentDate.toLocalDate()
latestFormattedEvent = if (latestEvent.root.isEncrypted()
&& latestEvent.root.mxDecryptionResult == null) {
&& latestEvent.root.mxDecryptionResult == null) {
stringProvider.getString(R.string.encrypted_message)
} else if (latestEvent.root.getClearType() == EventType.MESSAGE) {
val senderName = latestEvent.senderName() ?: latestEvent.root.senderId
@ -117,10 +117,9 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte
}
}
latestEventTime = if (isSameDay) {
timelineDateFormatter.formatMessageHour(date)
dateFormatter.formatMessageHour(date)
} else {
//TODO: change this
timelineDateFormatter.formatMessageDay(date)
dateFormatter.formatMessageDay(date)
}
}
return RoomSummaryItem_()

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