forked from GitHub-Mirror/riotX-android
Merge pull request #137 from vector-im/feature/aggregations_relations
Feature/aggregations relations
This commit is contained in:
commit
e27367e3f2
@ -20,6 +20,7 @@ import com.zhuinden.monarchy.Monarchy
|
|||||||
import im.vector.matrix.android.InstrumentedTest
|
import im.vector.matrix.android.InstrumentedTest
|
||||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
|
import im.vector.matrix.android.internal.session.room.EventRelationExtractor
|
||||||
import im.vector.matrix.android.internal.session.room.members.SenderRoomMemberExtractor
|
import im.vector.matrix.android.internal.session.room.members.SenderRoomMemberExtractor
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline
|
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory
|
import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory
|
||||||
@ -58,8 +59,16 @@ internal class TimelineTest : InstrumentedTest {
|
|||||||
val paginationTask = FakePaginationTask(tokenChunkEventPersistor)
|
val paginationTask = FakePaginationTask(tokenChunkEventPersistor)
|
||||||
val getContextOfEventTask = FakeGetContextOfEventTask(tokenChunkEventPersistor)
|
val getContextOfEventTask = FakeGetContextOfEventTask(tokenChunkEventPersistor)
|
||||||
val roomMemberExtractor = SenderRoomMemberExtractor(ROOM_ID)
|
val roomMemberExtractor = SenderRoomMemberExtractor(ROOM_ID)
|
||||||
val timelineEventFactory = TimelineEventFactory(roomMemberExtractor)
|
val timelineEventFactory = TimelineEventFactory(roomMemberExtractor, EventRelationExtractor())
|
||||||
return DefaultTimeline(ROOM_ID, initialEventId, monarchy.realmConfiguration, taskExecutor, getContextOfEventTask, timelineEventFactory, paginationTask, null)
|
return DefaultTimeline(
|
||||||
|
ROOM_ID,
|
||||||
|
initialEventId,
|
||||||
|
monarchy.realmConfiguration,
|
||||||
|
taskExecutor,
|
||||||
|
getContextOfEventTask,
|
||||||
|
timelineEventFactory,
|
||||||
|
paginationTask,
|
||||||
|
null)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* 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.events.model
|
||||||
|
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <code>
|
||||||
|
* {
|
||||||
|
* "chunk": [
|
||||||
|
* {
|
||||||
|
* "type": "m.reaction",
|
||||||
|
* "key": "👍",
|
||||||
|
* "count": 3
|
||||||
|
* }
|
||||||
|
* ],
|
||||||
|
* "limited": false,
|
||||||
|
* "count": 1
|
||||||
|
* },
|
||||||
|
* </code>
|
||||||
|
*/
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class AggregatedAnnotation (
|
||||||
|
override val limited: Boolean? = false,
|
||||||
|
override val count: Int? = 0,
|
||||||
|
val chunk: List<RelationChunkInfo>? = null
|
||||||
|
|
||||||
|
) : UnsignedRelationInfo
|
@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* 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.events.model
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <code>
|
||||||
|
* {
|
||||||
|
* "m.annotation": {
|
||||||
|
* "chunk": [
|
||||||
|
* {
|
||||||
|
* "type": "m.reaction",
|
||||||
|
* "key": "👍",
|
||||||
|
* "count": 3
|
||||||
|
* }
|
||||||
|
* ],
|
||||||
|
* "limited": false,
|
||||||
|
* "count": 1
|
||||||
|
* },
|
||||||
|
* "m.reference": {
|
||||||
|
* "chunk": [
|
||||||
|
* {
|
||||||
|
* "type": "m.room.message",
|
||||||
|
* "event_id": "$some_event_id"
|
||||||
|
* }
|
||||||
|
* ],
|
||||||
|
* "limited": false,
|
||||||
|
* "count": 1
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* </code>
|
||||||
|
*/
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class AggregatedRelations(
|
||||||
|
@Json(name = "m.annotation") val annotations: AggregatedAnnotation? = null,
|
||||||
|
@Json(name = "m.reference") val references: DefaultUnsignedRelationInfo? = null
|
||||||
|
)
|
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* 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.events.model
|
||||||
|
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class DefaultUnsignedRelationInfo(
|
||||||
|
override val limited: Boolean? = false,
|
||||||
|
override val count: Int? = 0,
|
||||||
|
val chunk: List<Map<String, Any>>? = null
|
||||||
|
|
||||||
|
) : UnsignedRelationInfo
|
@ -65,6 +65,11 @@ object EventType {
|
|||||||
const val CALL_ANSWER = "m.call.answer"
|
const val CALL_ANSWER = "m.call.answer"
|
||||||
const val CALL_HANGUP = "m.call.hangup"
|
const val CALL_HANGUP = "m.call.hangup"
|
||||||
|
|
||||||
|
// Relation Events
|
||||||
|
|
||||||
|
const val REACTION = "m.reaction"
|
||||||
|
|
||||||
|
|
||||||
private val STATE_EVENTS = listOf(
|
private val STATE_EVENTS = listOf(
|
||||||
STATE_ROOM_NAME,
|
STATE_ROOM_NAME,
|
||||||
STATE_ROOM_TOPIC,
|
STATE_ROOM_TOPIC,
|
||||||
|
@ -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.api.session.events.model
|
||||||
|
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <code>
|
||||||
|
* {
|
||||||
|
* "type": "m.reaction",
|
||||||
|
* "key": "👍",
|
||||||
|
* "count": 3
|
||||||
|
* }
|
||||||
|
* </code>
|
||||||
|
*/
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class RelationChunkInfo(
|
||||||
|
val type: String,
|
||||||
|
val key: String,
|
||||||
|
val count: Int
|
||||||
|
)
|
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package im.vector.matrix.android.api.session.events.model
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constants defining known event relation types from Matrix specifications.
|
||||||
|
*/
|
||||||
|
object RelationType {
|
||||||
|
|
||||||
|
/** Lets you define an event which annotates an existing event.*/
|
||||||
|
const val ANNOTATION = "m.annotation"
|
||||||
|
/** Lets you define an event which replaces an existing event.*/
|
||||||
|
const val REPLACE = "m.replace"
|
||||||
|
/** ets you define an event which references an existing event.*/
|
||||||
|
const val REFERENCE = "m.reference"
|
||||||
|
|
||||||
|
}
|
@ -24,5 +24,6 @@ data class UnsignedData(
|
|||||||
@Json(name = "age") val age: Long?,
|
@Json(name = "age") val age: Long?,
|
||||||
@Json(name = "redacted_because") val redactedEvent: Event? = null,
|
@Json(name = "redacted_because") val redactedEvent: Event? = null,
|
||||||
@Json(name = "transaction_id") val transactionId: String? = null,
|
@Json(name = "transaction_id") val transactionId: String? = null,
|
||||||
@Json(name = "prev_content") val prevContent: Map<String, Any>? = null
|
@Json(name = "prev_content") val prevContent: Map<String, Any>? = null,
|
||||||
|
@Json(name = "m.relations") val relations: AggregatedRelations?
|
||||||
)
|
)
|
@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* 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.events.model
|
||||||
|
|
||||||
|
|
||||||
|
interface UnsignedRelationInfo {
|
||||||
|
val limited : Boolean?
|
||||||
|
val count: Int?
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package im.vector.matrix.android.api.session.room.model
|
||||||
|
|
||||||
|
|
||||||
|
data class EventAnnotationsSummary(
|
||||||
|
var eventId: String,
|
||||||
|
var reactionsSummary: List<ReactionAggregatedSummary>
|
||||||
|
)
|
@ -0,0 +1,9 @@
|
|||||||
|
package im.vector.matrix.android.api.session.room.model
|
||||||
|
|
||||||
|
data class ReactionAggregatedSummary(
|
||||||
|
val key: String, // "👍"
|
||||||
|
val count: Int, // 8
|
||||||
|
val addedByMe: Boolean, // true
|
||||||
|
val firstTimestamp: Long, // unix timestamp
|
||||||
|
val sourceEvents: List<String>
|
||||||
|
)
|
@ -0,0 +1,9 @@
|
|||||||
|
package im.vector.matrix.android.api.session.room.model.annotation
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class ReactionContent(
|
||||||
|
@Json(name = "m.relates_to") val relatesTo: ReactionInfo? = null
|
||||||
|
)
|
@ -0,0 +1,11 @@
|
|||||||
|
package im.vector.matrix.android.api.session.room.model.annotation
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class ReactionInfo(
|
||||||
|
@Json(name = "rel_type") override val type: String,
|
||||||
|
@Json(name = "event_id") override val eventId: String,
|
||||||
|
val key: String
|
||||||
|
) : RelationContent
|
@ -0,0 +1,6 @@
|
|||||||
|
package im.vector.matrix.android.api.session.room.model.annotation
|
||||||
|
|
||||||
|
interface RelationContent {
|
||||||
|
val type: String
|
||||||
|
val eventId: String
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
package im.vector.matrix.android.api.session.room.model.annotation
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
|
||||||
|
data class RelationDefaultContent(
|
||||||
|
@Json(name = "rel_type") override val type: String,
|
||||||
|
@Json(name = "event_id") override val eventId: String
|
||||||
|
) : RelationContent
|
@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session.room.timeline
|
|||||||
|
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
|
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomMember
|
import im.vector.matrix.android.api.session.room.model.RoomMember
|
||||||
import im.vector.matrix.android.api.session.room.send.SendState
|
import im.vector.matrix.android.api.session.room.send.SendState
|
||||||
|
|
||||||
@ -32,7 +33,8 @@ data class TimelineEvent(
|
|||||||
val displayIndex: Int,
|
val displayIndex: Int,
|
||||||
val senderName: String?,
|
val senderName: String?,
|
||||||
val senderAvatar: String?,
|
val senderAvatar: String?,
|
||||||
val sendState: SendState
|
val sendState: SendState,
|
||||||
|
val annotations: EventAnnotationsSummary? = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val metadata = HashMap<String, Any>()
|
val metadata = HashMap<String, Any>()
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
package im.vector.matrix.android.internal.database.mapper
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
|
||||||
|
import im.vector.matrix.android.api.session.room.model.ReactionAggregatedSummary
|
||||||
|
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
|
||||||
|
|
||||||
|
internal object EventAnnotationsSummaryMapper {
|
||||||
|
fun map(annotationsSummary: EventAnnotationsSummaryEntity): EventAnnotationsSummary {
|
||||||
|
return EventAnnotationsSummary(
|
||||||
|
eventId = annotationsSummary.eventId,
|
||||||
|
reactionsSummary = annotationsSummary.reactionsSummary.toList().map {
|
||||||
|
ReactionAggregatedSummary(
|
||||||
|
it.key,
|
||||||
|
it.count,
|
||||||
|
it.addedByMe,
|
||||||
|
it.firstTimestamp,
|
||||||
|
it.sourceEvents.toList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun EventAnnotationsSummaryEntity.asDomain(): EventAnnotationsSummary {
|
||||||
|
return EventAnnotationsSummaryMapper.map(this)
|
||||||
|
}
|
@ -19,12 +19,15 @@ 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.events.model.Event
|
||||||
import im.vector.matrix.android.api.session.events.model.UnsignedData
|
import im.vector.matrix.android.api.session.events.model.UnsignedData
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
|
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||||
|
|
||||||
|
|
||||||
internal object EventMapper {
|
internal object EventMapper {
|
||||||
|
|
||||||
|
|
||||||
fun map(event: Event, roomId: String): EventEntity {
|
fun map(event: Event, roomId: String): EventEntity {
|
||||||
|
val uds = if (event.unsignedData == null) null
|
||||||
|
else MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(event.unsignedData)
|
||||||
val eventEntity = EventEntity()
|
val eventEntity = EventEntity()
|
||||||
eventEntity.eventId = event.eventId ?: ""
|
eventEntity.eventId = event.eventId ?: ""
|
||||||
eventEntity.roomId = event.roomId ?: roomId
|
eventEntity.roomId = event.roomId ?: roomId
|
||||||
@ -37,10 +40,14 @@ internal object EventMapper {
|
|||||||
eventEntity.originServerTs = event.originServerTs
|
eventEntity.originServerTs = event.originServerTs
|
||||||
eventEntity.redacts = event.redacts
|
eventEntity.redacts = event.redacts
|
||||||
eventEntity.age = event.unsignedData?.age ?: event.originServerTs
|
eventEntity.age = event.unsignedData?.age ?: event.originServerTs
|
||||||
|
eventEntity.unsignedData = uds
|
||||||
return eventEntity
|
return eventEntity
|
||||||
}
|
}
|
||||||
|
|
||||||
fun map(eventEntity: EventEntity): Event {
|
fun map(eventEntity: EventEntity): Event {
|
||||||
|
//TODO proxy the event to only parse unsigned data when accessed?
|
||||||
|
var ud = if (eventEntity.unsignedData.isNullOrBlank()) null
|
||||||
|
else MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).fromJson(eventEntity.unsignedData)
|
||||||
return Event(
|
return Event(
|
||||||
type = eventEntity.type,
|
type = eventEntity.type,
|
||||||
eventId = eventEntity.eventId,
|
eventId = eventEntity.eventId,
|
||||||
@ -50,7 +57,7 @@ internal object EventMapper {
|
|||||||
sender = eventEntity.sender,
|
sender = eventEntity.sender,
|
||||||
stateKey = eventEntity.stateKey,
|
stateKey = eventEntity.stateKey,
|
||||||
roomId = eventEntity.roomId,
|
roomId = eventEntity.roomId,
|
||||||
unsignedData = UnsignedData(eventEntity.age),
|
unsignedData = ud,
|
||||||
redacts = eventEntity.redacts
|
redacts = eventEntity.redacts
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package im.vector.matrix.android.internal.database.model
|
||||||
|
|
||||||
|
import io.realm.RealmList
|
||||||
|
import io.realm.RealmObject
|
||||||
|
import io.realm.annotations.PrimaryKey
|
||||||
|
|
||||||
|
|
||||||
|
internal open class EventAnnotationsSummaryEntity(
|
||||||
|
@PrimaryKey
|
||||||
|
var eventId: String = "",
|
||||||
|
var roomId: String? = null,
|
||||||
|
var reactionsSummary: RealmList<ReactionAggregatedSummaryEntity> = RealmList()
|
||||||
|
) : RealmObject() {
|
||||||
|
|
||||||
|
companion object
|
||||||
|
|
||||||
|
}
|
@ -36,6 +36,7 @@ internal open class EventEntity(@PrimaryKey var localId: String = UUID.randomUUI
|
|||||||
var originServerTs: Long? = null,
|
var originServerTs: Long? = null,
|
||||||
@Index var sender: String? = null,
|
@Index var sender: String? = null,
|
||||||
var age: Long? = 0,
|
var age: Long? = 0,
|
||||||
|
var unsignedData: String? = null,
|
||||||
var redacts: String? = null,
|
var redacts: String? = null,
|
||||||
@Index var stateIndex: Int = 0,
|
@Index var stateIndex: Int = 0,
|
||||||
@Index var displayIndex: Int = 0,
|
@Index var displayIndex: Int = 0,
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
package im.vector.matrix.android.internal.database.model
|
||||||
|
|
||||||
|
import io.realm.RealmList
|
||||||
|
import io.realm.RealmObject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aggregated Summary of a reaction.
|
||||||
|
*/
|
||||||
|
internal open class ReactionAggregatedSummaryEntity(
|
||||||
|
// The reaction String 😀
|
||||||
|
var key: String = "",
|
||||||
|
// Number of time this reaction was selected
|
||||||
|
var count: Int = 0,
|
||||||
|
// Did the current user sent this reaction
|
||||||
|
var addedByMe: Boolean = false,
|
||||||
|
// The first time this reaction was added (for ordering purpose)
|
||||||
|
var firstTimestamp: Long = 0,
|
||||||
|
// The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk)
|
||||||
|
var sourceEvents: RealmList<String> = RealmList()
|
||||||
|
) : RealmObject() {
|
||||||
|
|
||||||
|
companion object
|
||||||
|
|
||||||
|
}
|
@ -33,6 +33,8 @@ import io.realm.annotations.RealmModule
|
|||||||
RoomSummaryEntity::class,
|
RoomSummaryEntity::class,
|
||||||
RoomTagEntity::class,
|
RoomTagEntity::class,
|
||||||
SyncEntity::class,
|
SyncEntity::class,
|
||||||
UserEntity::class
|
UserEntity::class,
|
||||||
|
EventAnnotationsSummaryEntity::class,
|
||||||
|
ReactionAggregatedSummaryEntity::class
|
||||||
])
|
])
|
||||||
internal class SessionRealmModule
|
internal class SessionRealmModule
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
package im.vector.matrix.android.internal.database.query
|
||||||
|
|
||||||
|
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntityFields
|
||||||
|
import io.realm.Realm
|
||||||
|
import io.realm.RealmQuery
|
||||||
|
import io.realm.kotlin.where
|
||||||
|
|
||||||
|
internal fun EventAnnotationsSummaryEntity.Companion.where(realm: Realm, eventId: String): RealmQuery<EventAnnotationsSummaryEntity> {
|
||||||
|
val query = realm.where<EventAnnotationsSummaryEntity>()
|
||||||
|
query.equalTo(EventAnnotationsSummaryEntityFields.EVENT_ID, eventId)
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun EventAnnotationsSummaryEntity.Companion.whereInRoom(realm: Realm, roomId: String?): RealmQuery<EventAnnotationsSummaryEntity> {
|
||||||
|
val query = realm.where<EventAnnotationsSummaryEntity>()
|
||||||
|
if (roomId != null) {
|
||||||
|
query.equalTo(EventAnnotationsSummaryEntityFields.ROOM_ID, roomId)
|
||||||
|
}
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
internal fun EventAnnotationsSummaryEntity.Companion.create(realm: Realm, eventId: String): EventAnnotationsSummaryEntity {
|
||||||
|
val obj = realm.createObject(EventAnnotationsSummaryEntity::class.java, eventId)
|
||||||
|
return obj
|
||||||
|
}
|
@ -17,6 +17,8 @@
|
|||||||
package im.vector.matrix.android.internal.di
|
package im.vector.matrix.android.internal.di
|
||||||
|
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
|
import im.vector.matrix.android.api.session.room.model.annotation.RelationContent
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent
|
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.MessageContent
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageDefaultContent
|
import im.vector.matrix.android.api.session.room.model.message.MessageDefaultContent
|
||||||
|
@ -34,6 +34,7 @@ import im.vector.matrix.android.internal.session.filter.*
|
|||||||
import im.vector.matrix.android.internal.session.group.DefaultGroupService
|
import im.vector.matrix.android.internal.session.group.DefaultGroupService
|
||||||
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
|
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
|
||||||
import im.vector.matrix.android.internal.session.room.DefaultRoomService
|
import im.vector.matrix.android.internal.session.room.DefaultRoomService
|
||||||
|
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
|
||||||
import im.vector.matrix.android.internal.session.room.RoomAvatarResolver
|
import im.vector.matrix.android.internal.session.room.RoomAvatarResolver
|
||||||
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
|
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
|
||||||
import im.vector.matrix.android.internal.session.room.members.RoomDisplayNameResolver
|
import im.vector.matrix.android.internal.session.room.members.RoomDisplayNameResolver
|
||||||
@ -102,6 +103,10 @@ internal class SessionModule(private val sessionParams: SessionParams) {
|
|||||||
RoomSummaryUpdater(get(), get(), get())
|
RoomSummaryUpdater(get(), get(), get())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scope(DefaultSession.SCOPE) {
|
||||||
|
EventRelationsAggregationUpdater(get())
|
||||||
|
}
|
||||||
|
|
||||||
scope(DefaultSession.SCOPE) {
|
scope(DefaultSession.SCOPE) {
|
||||||
DefaultRoomService(get(), get(), get(), get()) as RoomService
|
DefaultRoomService(get(), get(), get(), get()) as RoomService
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
package im.vector.matrix.android.internal.session.room
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
|
||||||
|
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||||
|
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
|
import io.realm.Realm
|
||||||
|
|
||||||
|
|
||||||
|
internal class EventRelationExtractor {
|
||||||
|
|
||||||
|
fun extractFrom(event: EventEntity, realm: Realm = event.realm): EventAnnotationsSummary? {
|
||||||
|
return EventAnnotationsSummaryEntity.where(realm, event.eventId).findFirst()?.asDomain()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
package im.vector.matrix.android.internal.session.room
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.auth.data.Credentials
|
||||||
|
import im.vector.matrix.android.api.session.events.model.*
|
||||||
|
import im.vector.matrix.android.api.session.room.model.annotation.ReactionContent
|
||||||
|
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntity
|
||||||
|
import im.vector.matrix.android.internal.database.query.create
|
||||||
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
|
import io.realm.Realm
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
|
||||||
|
internal class EventRelationsAggregationUpdater(private val credentials: Credentials) {
|
||||||
|
|
||||||
|
fun update(realm: Realm, roomId: String, events: List<Event>?) {
|
||||||
|
events?.forEach { event ->
|
||||||
|
when (event.type) {
|
||||||
|
EventType.REACTION -> {
|
||||||
|
//we got a reaction!!
|
||||||
|
Timber.v("###REACTION in room $roomId")
|
||||||
|
handleReaction(event, roomId, realm)
|
||||||
|
}
|
||||||
|
EventType.MESSAGE -> {
|
||||||
|
event.unsignedData?.relations?.annotations?.let {
|
||||||
|
Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}")
|
||||||
|
handleInitialAggregatedRelations(event, roomId, it, realm)
|
||||||
|
}
|
||||||
|
//TODO message edits
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleInitialAggregatedRelations(event: Event, roomId: String, aggregation: AggregatedAnnotation, realm: Realm) {
|
||||||
|
aggregation.chunk?.forEach {
|
||||||
|
if (it.type == EventType.REACTION) {
|
||||||
|
val eventId = event.eventId ?: ""
|
||||||
|
val existing = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
|
||||||
|
if (existing == null) {
|
||||||
|
val eventSummary = EventAnnotationsSummaryEntity.create(realm, eventId)
|
||||||
|
eventSummary.roomId = roomId
|
||||||
|
val sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java)
|
||||||
|
sum.key = it.key
|
||||||
|
sum.firstTimestamp = event.originServerTs ?: 0 //TODO how to maintain order?
|
||||||
|
sum.count = it.count
|
||||||
|
eventSummary.reactionsSummary.add(sum)
|
||||||
|
} else {
|
||||||
|
//TODO how to handle that
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleReaction(event: Event, roomId: String, realm: Realm) {
|
||||||
|
event.content.toModel<ReactionContent>()?.let { content ->
|
||||||
|
//rel_type must be m.annotation
|
||||||
|
if (RelationType.ANNOTATION == content.relatesTo?.type) {
|
||||||
|
val reaction = content.relatesTo.key
|
||||||
|
val eventId = content.relatesTo.eventId
|
||||||
|
val eventSummary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
|
||||||
|
?: EventAnnotationsSummaryEntity.create(realm, eventId).apply { this.roomId = roomId }
|
||||||
|
|
||||||
|
var sum = eventSummary.reactionsSummary.find { it.key == reaction }
|
||||||
|
if (sum == null) {
|
||||||
|
sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java)
|
||||||
|
sum.key = reaction
|
||||||
|
sum.firstTimestamp = event.originServerTs ?: 0
|
||||||
|
sum.count = 1
|
||||||
|
sum.addedByMe = sum.addedByMe || (credentials.userId == event.sender)
|
||||||
|
eventSummary.reactionsSummary.add(sum)
|
||||||
|
} else {
|
||||||
|
//is this a known event (is possible? pagination?)
|
||||||
|
if (!sum.sourceEvents.contains(eventId)) {
|
||||||
|
sum.count += 1
|
||||||
|
sum.sourceEvents.add(eventId)
|
||||||
|
sum.addedByMe = sum.addedByMe || (credentials.userId == event.sender)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -46,7 +46,7 @@ internal class RoomFactory(private val loadRoomMembersTask: LoadRoomMembersTask,
|
|||||||
|
|
||||||
fun instantiate(roomId: String): Room {
|
fun instantiate(roomId: String): Room {
|
||||||
val roomMemberExtractor = SenderRoomMemberExtractor(roomId)
|
val roomMemberExtractor = SenderRoomMemberExtractor(roomId)
|
||||||
val timelineEventFactory = TimelineEventFactory(roomMemberExtractor)
|
val timelineEventFactory = TimelineEventFactory(roomMemberExtractor, EventRelationExtractor())
|
||||||
val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask)
|
val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask)
|
||||||
val sendService = DefaultSendService(roomId, eventFactory, monarchy)
|
val sendService = DefaultSendService(roomId, eventFactory, monarchy)
|
||||||
val stateService = DefaultStateService(roomId, sendStateTask, taskExecutor)
|
val stateService = DefaultStateService(roomId, sendStateTask, taskExecutor)
|
||||||
|
@ -51,7 +51,7 @@ class RoomModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scope(DefaultSession.SCOPE) {
|
scope(DefaultSession.SCOPE) {
|
||||||
TokenChunkEventPersistor(get())
|
TokenChunkEventPersistor(get(), get())
|
||||||
}
|
}
|
||||||
|
|
||||||
scope(DefaultSession.SCOPE) {
|
scope(DefaultSession.SCOPE) {
|
||||||
|
@ -27,29 +27,22 @@ import im.vector.matrix.android.api.session.room.timeline.Timeline
|
|||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.matrix.android.api.util.CancelableBag
|
import im.vector.matrix.android.api.util.CancelableBag
|
||||||
import im.vector.matrix.android.api.util.addTo
|
import im.vector.matrix.android.api.util.addTo
|
||||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||||
import im.vector.matrix.android.internal.database.model.ChunkEntityFields
|
import im.vector.matrix.android.internal.database.model.*
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
|
||||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
|
||||||
import im.vector.matrix.android.internal.database.query.findIncludingEvent
|
import im.vector.matrix.android.internal.database.query.findIncludingEvent
|
||||||
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
|
import im.vector.matrix.android.internal.database.query.whereInRoom
|
||||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
import im.vector.matrix.android.internal.task.configureWith
|
import im.vector.matrix.android.internal.task.configureWith
|
||||||
import im.vector.matrix.android.internal.util.Debouncer
|
import im.vector.matrix.android.internal.util.Debouncer
|
||||||
import io.realm.OrderedCollectionChangeSet
|
import io.realm.*
|
||||||
import io.realm.OrderedRealmCollectionChangeListener
|
|
||||||
import io.realm.Realm
|
|
||||||
import io.realm.RealmConfiguration
|
|
||||||
import io.realm.RealmQuery
|
|
||||||
import io.realm.RealmResults
|
|
||||||
import io.realm.Sort
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
import kotlin.collections.HashMap
|
||||||
|
|
||||||
|
|
||||||
private const val INITIAL_LOAD_SIZE = 20
|
private const val INITIAL_LOAD_SIZE = 20
|
||||||
@ -92,19 +85,23 @@ internal class DefaultTimeline(
|
|||||||
private var nextDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN
|
private var nextDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN
|
||||||
private val isLive = initialEventId == null
|
private val isLive = initialEventId == null
|
||||||
private val builtEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
|
private val builtEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
|
||||||
|
private val builtEventsIdMap = Collections.synchronizedMap(HashMap<String, Int>())
|
||||||
private val backwardsPaginationState = AtomicReference(PaginationState())
|
private val backwardsPaginationState = AtomicReference(PaginationState())
|
||||||
private val forwardsPaginationState = AtomicReference(PaginationState())
|
private val forwardsPaginationState = AtomicReference(PaginationState())
|
||||||
|
|
||||||
|
|
||||||
|
private lateinit var eventRelations: RealmResults<EventAnnotationsSummaryEntity>
|
||||||
|
|
||||||
private val eventsChangeListener = OrderedRealmCollectionChangeListener<RealmResults<EventEntity>> { _, changeSet ->
|
private val eventsChangeListener = OrderedRealmCollectionChangeListener<RealmResults<EventEntity>> { _, changeSet ->
|
||||||
if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL ) {
|
if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL) {
|
||||||
handleInitialLoad()
|
handleInitialLoad()
|
||||||
} else {
|
} else {
|
||||||
// If changeSet has deletion we are having a gap, so we clear everything
|
// If changeSet has deletion we are having a gap, so we clear everything
|
||||||
if(changeSet.deletionRanges.isNotEmpty()){
|
if (changeSet.deletionRanges.isNotEmpty()) {
|
||||||
prevDisplayIndex = DISPLAY_INDEX_UNKNOWN
|
prevDisplayIndex = DISPLAY_INDEX_UNKNOWN
|
||||||
nextDisplayIndex = DISPLAY_INDEX_UNKNOWN
|
nextDisplayIndex = DISPLAY_INDEX_UNKNOWN
|
||||||
builtEvents.clear()
|
builtEvents.clear()
|
||||||
|
builtEventsIdMap.clear()
|
||||||
timelineEventFactory.clear()
|
timelineEventFactory.clear()
|
||||||
}
|
}
|
||||||
changeSet.insertionRanges.forEach { range ->
|
changeSet.insertionRanges.forEach { range ->
|
||||||
@ -130,6 +127,38 @@ 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
changeSet.deletions?.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 = null)
|
||||||
|
hasChange = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasChange)
|
||||||
|
postSnapshot()
|
||||||
|
}
|
||||||
|
|
||||||
// Public methods ******************************************************************************
|
// Public methods ******************************************************************************
|
||||||
|
|
||||||
override fun paginate(direction: Timeline.Direction, count: Int) {
|
override fun paginate(direction: Timeline.Direction, count: Int) {
|
||||||
@ -146,6 +175,7 @@ internal class DefaultTimeline(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
if (isStarted.compareAndSet(false, true)) {
|
if (isStarted.compareAndSet(false, true)) {
|
||||||
Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId")
|
Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId")
|
||||||
@ -171,6 +201,10 @@ internal class DefaultTimeline(
|
|||||||
.also { it.addChangeListener(eventsChangeListener) }
|
.also { it.addChangeListener(eventsChangeListener) }
|
||||||
|
|
||||||
isReady.set(true)
|
isReady.set(true)
|
||||||
|
|
||||||
|
eventRelations = EventAnnotationsSummaryEntity.whereInRoom(realm, roomId)
|
||||||
|
.findAllAsync()
|
||||||
|
.also { it.addChangeListener(relationsListener) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -242,6 +276,7 @@ internal class DefaultTimeline(
|
|||||||
} else {
|
} else {
|
||||||
updatePaginationState(direction) { it.copy(isPaginating = false, requestedCount = 0) }
|
updatePaginationState(direction) { it.copy(isPaginating = false, requestedCount = 0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
return !shouldFetchMore
|
return !shouldFetchMore
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -266,14 +301,14 @@ internal class DefaultTimeline(
|
|||||||
|
|
||||||
private fun getPaginationState(direction: Timeline.Direction): PaginationState {
|
private fun getPaginationState(direction: Timeline.Direction): PaginationState {
|
||||||
return when (direction) {
|
return when (direction) {
|
||||||
Timeline.Direction.FORWARDS -> forwardsPaginationState.get()
|
Timeline.Direction.FORWARDS -> forwardsPaginationState.get()
|
||||||
Timeline.Direction.BACKWARDS -> backwardsPaginationState.get()
|
Timeline.Direction.BACKWARDS -> backwardsPaginationState.get()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updatePaginationState(direction: Timeline.Direction, update: (PaginationState) -> PaginationState) {
|
private fun updatePaginationState(direction: Timeline.Direction, update: (PaginationState) -> PaginationState) {
|
||||||
val stateReference = when (direction) {
|
val stateReference = when (direction) {
|
||||||
Timeline.Direction.FORWARDS -> forwardsPaginationState
|
Timeline.Direction.FORWARDS -> forwardsPaginationState
|
||||||
Timeline.Direction.BACKWARDS -> backwardsPaginationState
|
Timeline.Direction.BACKWARDS -> backwardsPaginationState
|
||||||
}
|
}
|
||||||
val currentValue = stateReference.get()
|
val currentValue = stateReference.get()
|
||||||
@ -316,9 +351,9 @@ internal class DefaultTimeline(
|
|||||||
private fun executePaginationTask(direction: Timeline.Direction, limit: Int) {
|
private fun executePaginationTask(direction: Timeline.Direction, limit: Int) {
|
||||||
val token = getTokenLive(direction) ?: return
|
val token = getTokenLive(direction) ?: return
|
||||||
val params = PaginationTask.Params(roomId = roomId,
|
val params = PaginationTask.Params(roomId = roomId,
|
||||||
from = token,
|
from = token,
|
||||||
direction = direction.toPaginationDirection(),
|
direction = direction.toPaginationDirection(),
|
||||||
limit = limit)
|
limit = limit)
|
||||||
|
|
||||||
Timber.v("Should fetch $limit items $direction")
|
Timber.v("Should fetch $limit items $direction")
|
||||||
paginationTask.configureWith(params)
|
paginationTask.configureWith(params)
|
||||||
@ -384,6 +419,7 @@ internal class DefaultTimeline(
|
|||||||
val timelineEvent = timelineEventFactory.create(eventEntity)
|
val timelineEvent = timelineEventFactory.create(eventEntity)
|
||||||
val position = if (direction == Timeline.Direction.FORWARDS) 0 else builtEvents.size
|
val position = if (direction == Timeline.Direction.FORWARDS) 0 else builtEvents.size
|
||||||
builtEvents.add(position, timelineEvent)
|
builtEvents.add(position, timelineEvent)
|
||||||
|
builtEventsIdMap[eventEntity.eventId] = position
|
||||||
}
|
}
|
||||||
Timber.v("Built ${offsetResults.size} items from db")
|
Timber.v("Built ${offsetResults.size} items from db")
|
||||||
return offsetResults.size
|
return offsetResults.size
|
||||||
|
@ -19,10 +19,13 @@ package im.vector.matrix.android.internal.session.room.timeline
|
|||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
|
import im.vector.matrix.android.internal.session.room.EventRelationExtractor
|
||||||
import im.vector.matrix.android.internal.session.room.members.SenderRoomMemberExtractor
|
import im.vector.matrix.android.internal.session.room.members.SenderRoomMemberExtractor
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
|
|
||||||
internal class TimelineEventFactory(private val roomMemberExtractor: SenderRoomMemberExtractor) {
|
internal class TimelineEventFactory(
|
||||||
|
private val roomMemberExtractor: SenderRoomMemberExtractor,
|
||||||
|
private val relationExtractor: EventRelationExtractor) {
|
||||||
|
|
||||||
private val cached = mutableMapOf<String, SenderData>()
|
private val cached = mutableMapOf<String, SenderData>()
|
||||||
|
|
||||||
@ -30,20 +33,22 @@ internal class TimelineEventFactory(private val roomMemberExtractor: SenderRoomM
|
|||||||
val sender = eventEntity.sender
|
val sender = eventEntity.sender
|
||||||
val cacheKey = sender + eventEntity.stateIndex
|
val cacheKey = sender + eventEntity.stateIndex
|
||||||
val senderData = cached.getOrPut(cacheKey) {
|
val senderData = cached.getOrPut(cacheKey) {
|
||||||
val senderRoomMember = roomMemberExtractor.extractFrom(eventEntity,realm)
|
val senderRoomMember = roomMemberExtractor.extractFrom(eventEntity, realm)
|
||||||
SenderData(senderRoomMember?.displayName, senderRoomMember?.avatarUrl)
|
SenderData(senderRoomMember?.displayName, senderRoomMember?.avatarUrl)
|
||||||
}
|
}
|
||||||
|
val relations = relationExtractor.extractFrom(eventEntity, realm)
|
||||||
return TimelineEvent(
|
return TimelineEvent(
|
||||||
eventEntity.asDomain(),
|
eventEntity.asDomain(),
|
||||||
eventEntity.localId,
|
eventEntity.localId,
|
||||||
eventEntity.displayIndex,
|
eventEntity.displayIndex,
|
||||||
senderData.senderName,
|
senderData.senderName,
|
||||||
senderData.senderAvatar,
|
senderData.senderAvatar,
|
||||||
eventEntity.sendState
|
eventEntity.sendState,
|
||||||
|
relations
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clear(){
|
fun clear() {
|
||||||
cached.clear()
|
cached.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,12 +24,14 @@ import im.vector.matrix.android.internal.database.helper.addStateEvents
|
|||||||
import im.vector.matrix.android.internal.database.helper.deleteOnCascade
|
import im.vector.matrix.android.internal.database.helper.deleteOnCascade
|
||||||
import im.vector.matrix.android.internal.database.helper.isUnlinked
|
import im.vector.matrix.android.internal.database.helper.isUnlinked
|
||||||
import im.vector.matrix.android.internal.database.helper.merge
|
import im.vector.matrix.android.internal.database.helper.merge
|
||||||
|
import im.vector.matrix.android.internal.database.mapper.EventMapper
|
||||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||||
import im.vector.matrix.android.internal.database.query.create
|
import im.vector.matrix.android.internal.database.query.create
|
||||||
import im.vector.matrix.android.internal.database.query.find
|
import im.vector.matrix.android.internal.database.query.find
|
||||||
import im.vector.matrix.android.internal.database.query.findAllIncludingEvents
|
import im.vector.matrix.android.internal.database.query.findAllIncludingEvents
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
|
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
|
||||||
import im.vector.matrix.android.internal.util.tryTransactionSync
|
import im.vector.matrix.android.internal.util.tryTransactionSync
|
||||||
import io.realm.kotlin.createObject
|
import io.realm.kotlin.createObject
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@ -37,7 +39,8 @@ import timber.log.Timber
|
|||||||
/**
|
/**
|
||||||
* Insert Chunk in DB, and eventually merge with existing chunk event
|
* Insert Chunk in DB, and eventually merge with existing chunk event
|
||||||
*/
|
*/
|
||||||
internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
|
internal class TokenChunkEventPersistor(private val monarchy: Monarchy,
|
||||||
|
private val eventRelationsAggregationUpdater: EventRelationsAggregationUpdater) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <pre>
|
* <pre>
|
||||||
@ -147,6 +150,9 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
|
|||||||
} else {
|
} else {
|
||||||
Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}")
|
Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}")
|
||||||
currentChunk.addAll(roomId, receivedChunk.events, direction, isUnlinked = currentChunk.isUnlinked())
|
currentChunk.addAll(roomId, receivedChunk.events, direction, isUnlinked = currentChunk.isUnlinked())
|
||||||
|
|
||||||
|
//Event
|
||||||
|
eventRelationsAggregationUpdater.update(realm,roomId,receivedChunk.events.toList())
|
||||||
// Then we merge chunks if needed
|
// Then we merge chunks if needed
|
||||||
if (currentChunk != prevChunk && prevChunk != null) {
|
if (currentChunk != prevChunk && prevChunk != null) {
|
||||||
currentChunk = handleMerge(roomEntity, direction, currentChunk, prevChunk)
|
currentChunk = handleMerge(roomEntity, direction, currentChunk, prevChunk)
|
||||||
|
@ -31,6 +31,7 @@ import im.vector.matrix.android.internal.database.model.RoomEntity
|
|||||||
import im.vector.matrix.android.internal.database.query.find
|
import im.vector.matrix.android.internal.database.query.find
|
||||||
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
|
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
|
||||||
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
|
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
|
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
|
||||||
import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync
|
import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync
|
||||||
@ -45,7 +46,8 @@ import timber.log.Timber
|
|||||||
internal class RoomSyncHandler(private val monarchy: Monarchy,
|
internal class RoomSyncHandler(private val monarchy: Monarchy,
|
||||||
private val readReceiptHandler: ReadReceiptHandler,
|
private val readReceiptHandler: ReadReceiptHandler,
|
||||||
private val roomSummaryUpdater: RoomSummaryUpdater,
|
private val roomSummaryUpdater: RoomSummaryUpdater,
|
||||||
private val roomTagHandler: RoomTagHandler) {
|
private val roomTagHandler: RoomTagHandler,
|
||||||
|
private val eventRelationsAggregationUpdater: EventRelationsAggregationUpdater) {
|
||||||
|
|
||||||
sealed class HandlingStrategy {
|
sealed class HandlingStrategy {
|
||||||
data class JOINED(val data: Map<String, RoomSync>) : HandlingStrategy()
|
data class JOINED(val data: Map<String, RoomSync>) : HandlingStrategy()
|
||||||
@ -120,6 +122,7 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
roomSummaryUpdater.update(realm, roomId, roomSync.summary, roomSync.unreadNotifications)
|
roomSummaryUpdater.update(realm, roomId, roomSync.summary, roomSync.unreadNotifications)
|
||||||
|
eventRelationsAggregationUpdater.update(realm,roomId,roomSync.timeline?.events)
|
||||||
|
|
||||||
if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) {
|
if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) {
|
||||||
handleEphemeral(realm, roomId, roomSync.ephemeral)
|
handleEphemeral(realm, roomId, roomSync.ephemeral)
|
||||||
@ -174,6 +177,9 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
|
|||||||
lastChunk?.isLastForward = false
|
lastChunk?.isLastForward = false
|
||||||
chunkEntity.isLastForward = true
|
chunkEntity.isLastForward = true
|
||||||
chunkEntity.addAll(roomId, eventList, PaginationDirection.FORWARDS, stateIndexOffset)
|
chunkEntity.addAll(roomId, eventList, PaginationDirection.FORWARDS, stateIndexOffset)
|
||||||
|
|
||||||
|
//update eventAnnotationSummary here?
|
||||||
|
|
||||||
return chunkEntity
|
return chunkEntity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ internal class SyncModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scope(DefaultSession.SCOPE) {
|
scope(DefaultSession.SCOPE) {
|
||||||
RoomSyncHandler(get(), get(), get(), get())
|
RoomSyncHandler(get(), get(), get(), get(), get())
|
||||||
}
|
}
|
||||||
|
|
||||||
scope(DefaultSession.SCOPE) {
|
scope(DefaultSession.SCOPE) {
|
||||||
|
@ -59,6 +59,7 @@ import im.vector.matrix.android.api.session.user.model.User
|
|||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
import im.vector.riotredesign.core.dialogs.DialogListItem
|
import im.vector.riotredesign.core.dialogs.DialogListItem
|
||||||
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
|
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
|
||||||
|
import im.vector.riotredesign.core.extensions.hideKeyboard
|
||||||
import im.vector.riotredesign.core.extensions.observeEvent
|
import im.vector.riotredesign.core.extensions.observeEvent
|
||||||
import im.vector.riotredesign.core.glide.GlideApp
|
import im.vector.riotredesign.core.glide.GlideApp
|
||||||
import im.vector.riotredesign.core.platform.ToolbarConfigurable
|
import im.vector.riotredesign.core.platform.ToolbarConfigurable
|
||||||
@ -460,6 +461,7 @@ class RoomDetailFragment :
|
|||||||
Timber.e("Missing RoomId, cannot open bottomsheet")
|
Timber.e("Missing RoomId, cannot open bottomsheet")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
this.view?.hideKeyboard()
|
||||||
MessageActionsBottomSheet
|
MessageActionsBottomSheet
|
||||||
.newInstance(roomId, informationData)
|
.newInstance(roomId, informationData)
|
||||||
.show(requireActivity().supportFragmentManager, "MESSAGE_CONTEXTUAL_ACTIONS")
|
.show(requireActivity().supportFragmentManager, "MESSAGE_CONTEXTUAL_ACTIONS")
|
||||||
|
@ -89,7 +89,7 @@ class MessageActionsBottomSheet : BaseMvRxBottomSheetDialog() {
|
|||||||
|
|
||||||
var quickReactionFragment = cfm.findFragmentByTag("QuickReaction") as? QuickReactionFragment
|
var quickReactionFragment = cfm.findFragmentByTag("QuickReaction") as? QuickReactionFragment
|
||||||
if (quickReactionFragment == null) {
|
if (quickReactionFragment == null) {
|
||||||
quickReactionFragment = QuickReactionFragment.newInstance()
|
quickReactionFragment = QuickReactionFragment.newInstance(arguments!!.get(MvRx.KEY_ARG) as ParcelableArgs)
|
||||||
cfm.beginTransaction()
|
cfm.beginTransaction()
|
||||||
.replace(R.id.bottom_sheet_quick_reaction_container, quickReactionFragment, "QuickReaction")
|
.replace(R.id.bottom_sheet_quick_reaction_container, quickReactionFragment, "QuickReaction")
|
||||||
.commit()
|
.commit()
|
||||||
|
@ -25,6 +25,7 @@ import androidx.transition.TransitionManager
|
|||||||
import butterknife.BindView
|
import butterknife.BindView
|
||||||
import butterknife.ButterKnife
|
import butterknife.ButterKnife
|
||||||
import com.airbnb.mvrx.BaseMvRxFragment
|
import com.airbnb.mvrx.BaseMvRxFragment
|
||||||
|
import com.airbnb.mvrx.MvRx
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
@ -62,10 +63,10 @@ class QuickReactionFragment : BaseMvRxFragment() {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
quickReact1Text.text = viewModel.agreePositive
|
quickReact1Text.text = QuickReactionViewModel.agreePositive
|
||||||
quickReact2Text.text = viewModel.agreeNegative
|
quickReact2Text.text = QuickReactionViewModel.agreeNegative
|
||||||
quickReact3Text.text = viewModel.likePositive
|
quickReact3Text.text = QuickReactionViewModel.likePositive
|
||||||
quickReact4Text.text = viewModel.likeNegative
|
quickReact4Text.text = QuickReactionViewModel.likeNegative
|
||||||
|
|
||||||
//configure click listeners
|
//configure click listeners
|
||||||
quickReact1Text.setOnClickListener {
|
quickReact1Text.setOnClickListener {
|
||||||
@ -127,8 +128,12 @@ class QuickReactionFragment : BaseMvRxFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun newInstance(): QuickReactionFragment {
|
fun newInstance(pa: MessageActionsBottomSheet.ParcelableArgs): QuickReactionFragment {
|
||||||
return QuickReactionFragment()
|
val args = Bundle()
|
||||||
|
args.putParcelable(MvRx.KEY_ARG, pa)
|
||||||
|
val fragment = QuickReactionFragment()
|
||||||
|
fragment.arguments = args
|
||||||
|
return fragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -18,7 +18,9 @@ package im.vector.riotredesign.features.home.room.detail.timeline.action
|
|||||||
import com.airbnb.mvrx.MvRxState
|
import com.airbnb.mvrx.MvRxState
|
||||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
import com.airbnb.mvrx.ViewModelContext
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
|
import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.riotredesign.core.platform.VectorViewModel
|
import im.vector.riotredesign.core.platform.VectorViewModel
|
||||||
|
import org.koin.android.ext.android.get
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Quick reactions state, it's a toggle with 3rd state
|
* Quick reactions state, it's a toggle with 3rd state
|
||||||
@ -37,11 +39,6 @@ data class QuickReactionState(val agreeTrigleState: TriggleState, val likeTriggl
|
|||||||
*/
|
*/
|
||||||
class QuickReactionViewModel(initialState: QuickReactionState) : VectorViewModel<QuickReactionState>(initialState) {
|
class QuickReactionViewModel(initialState: QuickReactionState) : VectorViewModel<QuickReactionState>(initialState) {
|
||||||
|
|
||||||
val agreePositive = "👍"
|
|
||||||
val agreeNegative = "👎"
|
|
||||||
val likePositive = "😀"
|
|
||||||
val likeNegative = "😞"
|
|
||||||
|
|
||||||
|
|
||||||
fun toggleAgree(isFirst: Boolean) = withState {
|
fun toggleAgree(isFirst: Boolean) = withState {
|
||||||
if (isFirst) {
|
if (isFirst) {
|
||||||
@ -99,10 +96,37 @@ class QuickReactionViewModel(initialState: QuickReactionState) : VectorViewModel
|
|||||||
|
|
||||||
companion object : MvRxViewModelFactory<QuickReactionViewModel, QuickReactionState> {
|
companion object : MvRxViewModelFactory<QuickReactionViewModel, QuickReactionState> {
|
||||||
|
|
||||||
|
val agreePositive = "👍"
|
||||||
|
val agreeNegative = "👎"
|
||||||
|
val likePositive = "🙂"
|
||||||
|
val likeNegative = "😔"
|
||||||
|
|
||||||
override fun initialState(viewModelContext: ViewModelContext): QuickReactionState? {
|
override fun initialState(viewModelContext: ViewModelContext): QuickReactionState? {
|
||||||
// Args are accessible from the context.
|
// Args are accessible from the context.
|
||||||
// val foo = vieWModelContext.args<MyArgs>.foo
|
// val foo = vieWModelContext.args<MyArgs>.foo
|
||||||
return QuickReactionState(TriggleState.NONE, TriggleState.NONE)
|
val currentSession = viewModelContext.activity.get<Session>()
|
||||||
|
val parcel = viewModelContext.args as MessageActionsBottomSheet.ParcelableArgs
|
||||||
|
val event = currentSession.getRoom(parcel.roomId)?.getTimeLineEvent(parcel.eventId)
|
||||||
|
?: return null
|
||||||
|
var agreeTriggle: TriggleState = TriggleState.NONE
|
||||||
|
var likeTriggle: TriggleState = TriggleState.NONE
|
||||||
|
event.annotations?.reactionsSummary?.forEach {
|
||||||
|
//it.addedByMe
|
||||||
|
if (it.addedByMe) {
|
||||||
|
if (agreePositive == it.key) {
|
||||||
|
agreeTriggle = TriggleState.FIRST
|
||||||
|
} else if (agreeNegative == it.key) {
|
||||||
|
agreeTriggle = TriggleState.SECOND
|
||||||
|
}
|
||||||
|
|
||||||
|
if (likePositive == it.key) {
|
||||||
|
likeTriggle = TriggleState.FIRST
|
||||||
|
} else if (likeNegative == it.key) {
|
||||||
|
likeTriggle = TriggleState.SECOND
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return QuickReactionState(agreeTriggle, likeTriggle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -71,7 +71,8 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
val avatarUrl = event.senderAvatar
|
val avatarUrl = event.senderAvatar
|
||||||
val memberName = event.senderName ?: event.root.sender ?: ""
|
val memberName = event.senderName ?: event.root.sender ?: ""
|
||||||
val formattedMemberName = span(memberName) {
|
val formattedMemberName = span(memberName) {
|
||||||
textColor = colorProvider.getColor(AvatarRenderer.getColorFromUserId(event.root.sender ?: ""))
|
textColor = colorProvider.getColor(AvatarRenderer.getColorFromUserId(event.root.sender
|
||||||
|
?: ""))
|
||||||
}
|
}
|
||||||
val informationData = MessageInformationData(eventId = eventId,
|
val informationData = MessageInformationData(eventId = eventId,
|
||||||
senderId = event.root.sender ?: "",
|
senderId = event.root.sender ?: "",
|
||||||
@ -79,7 +80,9 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
time = time,
|
time = time,
|
||||||
avatarUrl = avatarUrl,
|
avatarUrl = avatarUrl,
|
||||||
memberName = formattedMemberName,
|
memberName = formattedMemberName,
|
||||||
showInformation = showInformation)
|
showInformation = showInformation,
|
||||||
|
orderedReactionList = event.annotations?.reactionsSummary?.map { Triple(it.key, it.count, it.addedByMe) }
|
||||||
|
)
|
||||||
|
|
||||||
//Test for reactions UX
|
//Test for reactions UX
|
||||||
//informationData.orderedReactionList = listOf( Triple("👍",1,false), Triple("👎",2,false))
|
//informationData.orderedReactionList = listOf( Triple("👍",1,false), Triple("👎",2,false))
|
||||||
|
@ -19,6 +19,7 @@ package im.vector.riotredesign.features.home.room.detail.timeline.item
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.view.ViewStub
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.constraintlayout.helper.widget.Flow
|
import androidx.constraintlayout.helper.widget.Flow
|
||||||
@ -76,14 +77,19 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
|||||||
holder.view.setOnLongClickListener(longClickListener)
|
holder.view.setOnLongClickListener(longClickListener)
|
||||||
|
|
||||||
if (informationData.orderedReactionList.isNullOrEmpty()) {
|
if (informationData.orderedReactionList.isNullOrEmpty()) {
|
||||||
holder.reactionWrapper.isVisible = false
|
holder.reactionWrapper?.isVisible = false
|
||||||
} else {
|
} else {
|
||||||
holder.reactionWrapper.isVisible = true
|
//inflate if needed
|
||||||
|
if (holder.reactionFlowHelper == null) {
|
||||||
|
holder.reactionWrapper = holder.view.findViewById<ViewStub>(R.id.messageBottomInfo).inflate() as? ViewGroup
|
||||||
|
holder.reactionFlowHelper = holder.view.findViewById(R.id.reactionsFlowHelper)
|
||||||
|
}
|
||||||
|
holder.reactionWrapper?.isVisible = true
|
||||||
//clear all reaction buttons (but not the Flow helper!)
|
//clear all reaction buttons (but not the Flow helper!)
|
||||||
holder.reactionWrapper.children.forEach { (it as? ReactionButton)?.isGone = true }
|
holder.reactionWrapper?.children?.forEach { (it as? ReactionButton)?.isGone = true }
|
||||||
val idToRefInFlow = ArrayList<Int>()
|
val idToRefInFlow = ArrayList<Int>()
|
||||||
informationData.orderedReactionList?.forEachIndexed { index, reaction ->
|
informationData.orderedReactionList?.forEachIndexed { index, reaction ->
|
||||||
(holder.reactionWrapper.children.elementAt(index) as? ReactionButton)?.let { reactionButton ->
|
(holder.reactionWrapper?.children?.elementAt(index) as? ReactionButton)?.let { reactionButton ->
|
||||||
reactionButton.isVisible = true
|
reactionButton.isVisible = true
|
||||||
idToRefInFlow.add(reactionButton.id)
|
idToRefInFlow.add(reactionButton.id)
|
||||||
reactionButton.reactionString = reaction.first
|
reactionButton.reactionString = reaction.first
|
||||||
@ -93,9 +99,9 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
|||||||
}
|
}
|
||||||
// Just setting the view as gone will break the FlowHelper (and invisible will take too much space),
|
// Just setting the view as gone will break the FlowHelper (and invisible will take too much space),
|
||||||
// so have to update ref ids
|
// so have to update ref ids
|
||||||
holder.reactionFlowHelper.referencedIds = idToRefInFlow.toIntArray()
|
holder.reactionFlowHelper?.referencedIds = idToRefInFlow.toIntArray()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && !holder.view.isInLayout) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && !holder.view.isInLayout) {
|
||||||
holder.reactionFlowHelper.requestLayout()
|
holder.reactionFlowHelper?.requestLayout()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -112,8 +118,8 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
|||||||
val memberNameView by bind<TextView>(R.id.messageMemberNameView)
|
val memberNameView by bind<TextView>(R.id.messageMemberNameView)
|
||||||
val timeView by bind<TextView>(R.id.messageTimeView)
|
val timeView by bind<TextView>(R.id.messageTimeView)
|
||||||
|
|
||||||
val reactionWrapper: ViewGroup by bind(R.id.messageBottomInfo)
|
var reactionWrapper: ViewGroup? = null
|
||||||
val reactionFlowHelper: Flow by bind(R.id.reactionsFlowHelper)
|
var reactionFlowHelper: Flow? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<size android:width="40dp" android:height="22dp"/>
|
<size android:width="40dp" android:height="22dp"/>
|
||||||
|
|
||||||
<solid android:color="@color/light_blue_grey" />
|
<solid android:color="?vctr_list_header_background_color" />
|
||||||
|
|
||||||
<stroke android:width="1dp" android:color="@color/accent_color_light" />
|
<stroke android:width="1dp" android:color="@color/accent_color_light" />
|
||||||
|
|
||||||
|
@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
<size android:width="40dp" android:height="22dp"/>
|
<size android:width="40dp" android:height="22dp"/>
|
||||||
|
|
||||||
<solid android:color="@color/light_blue_grey" />
|
<solid android:color="?vctr_list_header_background_color" />
|
||||||
|
|
||||||
<stroke android:width="1dp" android:color="@color/list_divider_color_light" />
|
<!--<stroke android:width="1dp" android:color="@color/list_divider_color_light" />-->
|
||||||
|
|
||||||
<corners android:radius="20dp" />
|
<corners android:radius="20dp" />
|
||||||
|
|
||||||
|
@ -82,116 +82,23 @@
|
|||||||
tools:ignore="MissingConstraints" />
|
tools:ignore="MissingConstraints" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- TODO: For now we show 8 reactions maximum, this will need rework when needed-->
|
<!-- TODO: For now we show 8 reactions maximum, this will need rework when needed-->
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<ViewStub
|
||||||
android:id="@+id/messageBottomInfo"
|
android:id="@+id/messageBottomInfo"
|
||||||
|
android:inflatedId="@+id/messageBottomInfo"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout="@layout/item_timeline_event_bottom_reactions_stub"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/messageStartGuideline"
|
app:layout_constraintStart_toEndOf="@id/messageStartGuideline"
|
||||||
app:layout_constraintVertical_chainStyle="packed"
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
tools:visibility="visible">
|
tools:visibility="visible">
|
||||||
|
|
||||||
<im.vector.riotredesign.features.reactions.widget.ReactionButton
|
</ViewStub>
|
||||||
android:id="@+id/messageBottomReaction1"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:emoji="👍"
|
|
||||||
tools:ignore="MissingConstraints"
|
|
||||||
tools:reaction_count="3"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
|
|
||||||
<im.vector.riotredesign.features.reactions.widget.ReactionButton
|
|
||||||
android:id="@+id/messageBottomReaction2"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:emoji="👎"
|
|
||||||
tools:ignore="MissingConstraints"
|
|
||||||
tools:reaction_count="10"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
|
|
||||||
<im.vector.riotredesign.features.reactions.widget.ReactionButton
|
|
||||||
android:id="@+id/messageBottomReaction3"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:emoji="😀"
|
|
||||||
tools:ignore="MissingConstraints"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
|
|
||||||
<im.vector.riotredesign.features.reactions.widget.ReactionButton
|
|
||||||
android:id="@+id/messageBottomReaction4"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:emoji="☹️"
|
|
||||||
tools:ignore="MissingConstraints"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
|
|
||||||
<im.vector.riotredesign.features.reactions.widget.ReactionButton
|
|
||||||
android:id="@+id/messageBottomReaction5"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:emoji="😱"
|
|
||||||
tools:ignore="MissingConstraints"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
|
|
||||||
<im.vector.riotredesign.features.reactions.widget.ReactionButton
|
|
||||||
android:id="@+id/messageBottomReaction6"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:emoji="❌"
|
|
||||||
tools:ignore="MissingConstraints"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
|
|
||||||
<im.vector.riotredesign.features.reactions.widget.ReactionButton
|
|
||||||
android:id="@+id/messageBottomReaction7"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:emoji="✔️"
|
|
||||||
tools:ignore="MissingConstraints"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
<im.vector.riotredesign.features.reactions.widget.ReactionButton
|
|
||||||
android:id="@+id/messageBottomReaction8"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:emoji="♥️"
|
|
||||||
tools:ignore="MissingConstraints"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
|
|
||||||
<androidx.constraintlayout.helper.widget.Flow
|
|
||||||
android:id="@+id/reactionsFlowHelper"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:padding="2dp"
|
|
||||||
app:constraint_referenced_ids="messageBottomReaction1,messageBottomReaction2,messageBottomReaction3,messageBottomReaction4,messageBottomReaction5,messageBottomReaction6,messageBottomReaction7,messageBottomReaction8"
|
|
||||||
app:flow_horizontalBias="0"
|
|
||||||
app:flow_horizontalGap="8dp"
|
|
||||||
app:flow_horizontalStyle="packed"
|
|
||||||
app:flow_verticalBias="0"
|
|
||||||
app:flow_verticalGap="4dp"
|
|
||||||
app:flow_wrapMode="chain"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -0,0 +1,102 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/messageBottomInfo"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<im.vector.riotredesign.features.reactions.widget.ReactionButton
|
||||||
|
android:id="@+id/messageBottomReaction1"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:emoji="👍"
|
||||||
|
tools:ignore="MissingConstraints"
|
||||||
|
tools:reaction_count="3"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<im.vector.riotredesign.features.reactions.widget.ReactionButton
|
||||||
|
android:id="@+id/messageBottomReaction2"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:emoji="👎"
|
||||||
|
tools:ignore="MissingConstraints"
|
||||||
|
tools:reaction_count="10"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<im.vector.riotredesign.features.reactions.widget.ReactionButton
|
||||||
|
android:id="@+id/messageBottomReaction3"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:emoji="😀"
|
||||||
|
tools:ignore="MissingConstraints"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<im.vector.riotredesign.features.reactions.widget.ReactionButton
|
||||||
|
android:id="@+id/messageBottomReaction4"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:emoji="☹️"
|
||||||
|
tools:ignore="MissingConstraints"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<im.vector.riotredesign.features.reactions.widget.ReactionButton
|
||||||
|
android:id="@+id/messageBottomReaction5"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:emoji="😱"
|
||||||
|
tools:ignore="MissingConstraints"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
|
||||||
|
<im.vector.riotredesign.features.reactions.widget.ReactionButton
|
||||||
|
android:id="@+id/messageBottomReaction6"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:emoji="❌"
|
||||||
|
tools:ignore="MissingConstraints"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
|
||||||
|
<im.vector.riotredesign.features.reactions.widget.ReactionButton
|
||||||
|
android:id="@+id/messageBottomReaction7"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:emoji="✔️"
|
||||||
|
tools:ignore="MissingConstraints"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<im.vector.riotredesign.features.reactions.widget.ReactionButton
|
||||||
|
android:id="@+id/messageBottomReaction8"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:emoji="♥️"
|
||||||
|
tools:ignore="MissingConstraints"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
|
||||||
|
<androidx.constraintlayout.helper.widget.Flow
|
||||||
|
android:id="@+id/reactionsFlowHelper"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="2dp"
|
||||||
|
app:constraint_referenced_ids="messageBottomReaction1,messageBottomReaction2,messageBottomReaction3,messageBottomReaction4,messageBottomReaction5,messageBottomReaction6,messageBottomReaction7,messageBottomReaction8"
|
||||||
|
app:flow_horizontalBias="0"
|
||||||
|
app:flow_horizontalGap="8dp"
|
||||||
|
app:flow_horizontalStyle="packed"
|
||||||
|
app:flow_verticalBias="0"
|
||||||
|
app:flow_verticalGap="4dp"
|
||||||
|
app:flow_wrapMode="chain"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -252,7 +252,7 @@
|
|||||||
<item name="android:layout_height">wrap_content</item>
|
<item name="android:layout_height">wrap_content</item>
|
||||||
<item name="android:layout_marginStart">8dp</item>
|
<item name="android:layout_marginStart">8dp</item>
|
||||||
<item name="android:layout_marginLeft">8dp</item>
|
<item name="android:layout_marginLeft">8dp</item>
|
||||||
<item name="android:layout_marginBottom">8dp</item>
|
<item name="android:layout_marginBottom">4dp</item>
|
||||||
<item name="android:layout_marginTop">4dp</item>
|
<item name="android:layout_marginTop">4dp</item>
|
||||||
<item name="layout_constraintBottom_toBottomOf">parent</item>
|
<item name="layout_constraintBottom_toBottomOf">parent</item>
|
||||||
<item name="layout_constraintEnd_toEndOf">parent</item>
|
<item name="layout_constraintEnd_toEndOf">parent</item>
|
||||||
|
Loading…
Reference in New Issue
Block a user