Merge pull request #250 from vector-im/feature/fix_impure_reducers

Fix impure reducer and use live event
This commit is contained in:
Valere 2019-07-01 11:33:34 +02:00 committed by GitHub
commit 3960742f38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 144 additions and 81 deletions

View File

@ -19,6 +19,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.RoomSummary
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import io.reactivex.Observable

class RxRoom(private val room: Room) {
@ -31,10 +32,14 @@ class RxRoom(private val room: Room) {
return room.getRoomMemberIdsLive().asObservable()
}

fun liveAnnotationSummary(eventId: String): Observable<List<EventAnnotationsSummary>> {
fun liveAnnotationSummary(eventId: String): Observable<EventAnnotationsSummary> {
return room.getEventSummaryLive(eventId).asObservable()
}

fun liveTimelineEvent(eventId: String): Observable<TimelineEvent> {
return room.liveTimeLineEvent(eventId).asObservable()
}

}

fun Room.rx(): RxRoom {

View File

@ -80,5 +80,5 @@ interface RelationService {
*/
fun replyToMessage(eventReplied: Event, replyText: String): Cancelable?

fun getEventSummaryLive(eventId: String): LiveData<List<EventAnnotationsSummary>>
fun getEventSummaryLive(eventId: String): LiveData<EventAnnotationsSummary>
}

View File

@ -16,6 +16,8 @@

package im.vector.matrix.android.api.session.room.timeline

import androidx.lifecycle.LiveData

/**
* This interface defines methods to interact with the timeline. It's implemented at the room level.
*/
@ -32,4 +34,7 @@ interface TimelineService {


fun getTimeLineEvent(eventId: String): TimelineEvent?


fun liveTimeLineEvent(eventId: String): LiveData<TimelineEvent>
}

View File

@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.session.room.relation

import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import androidx.work.OneTimeWorkRequest
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
@ -27,6 +28,7 @@ import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.relation.RelationService
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.database.RealmLiveData
import im.vector.matrix.android.internal.database.helper.addSendingEvent
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.ChunkEntity
@ -155,15 +157,13 @@ internal class DefaultRelationService @Inject constructor(private val context: C
return workRequest
}

override fun getEventSummaryLive(eventId: String): LiveData<List<EventAnnotationsSummary>> {
return monarchy.findAllMappedWithChanges(
{ realm ->
EventAnnotationsSummaryEntity.where(realm, eventId)
},
{
it.asDomain()
}
)
override fun getEventSummaryLive(eventId: String): LiveData<EventAnnotationsSummary> {
val liveEntity = RealmLiveData(monarchy.realmConfiguration) { realm ->
EventAnnotationsSummaryEntity.where(realm, eventId)
}
return Transformations.map(liveEntity) { realmResults ->
realmResults.firstOrNull()?.asDomain() ?: EventAnnotationsSummary(eventId, emptyList(), null)
}
}

/**

View File

@ -16,10 +16,14 @@

package im.vector.matrix.android.internal.session.room.timeline

import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import com.zhuinden.monarchy.Monarchy
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.internal.database.RealmLiveData
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 im.vector.matrix.android.internal.task.TaskExecutor
@ -47,5 +51,27 @@ internal class DefaultTimelineService @Inject constructor(private val roomId: St
})
}

override fun liveTimeLineEvent(eventId: String): LiveData<TimelineEvent> {
val liveEventEntity = RealmLiveData(monarchy.realmConfiguration) { realm ->
EventEntity.where(realm, eventId = eventId)
}
val liveAnnotationsEntity = RealmLiveData(monarchy.realmConfiguration) { realm ->
EventAnnotationsSummaryEntity.where(realm, eventId = eventId)
}
val result = MediatorLiveData<TimelineEvent>()
result.addSource(liveEventEntity) { realmResults ->
result.value = realmResults.firstOrNull()?.let { timelineEventFactory.create(it) }
}

result.addSource(liveAnnotationsEntity) {
liveEventEntity.value?.let {
result.value = liveEventEntity.value?.let { realmResults ->
//recreate the timeline event
realmResults.firstOrNull()?.let { timelineEventFactory.create(it) }
}
}
}
return result
}

}

View File

@ -15,10 +15,7 @@
*/
package im.vector.riotredesign.features.home.room.detail.timeline.action

import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.airbnb.mvrx.*
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session
@ -28,7 +25,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageContent
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.timeline.TimelineEvent
import im.vector.riotredesign.core.di.HasScreenInjector
import im.vector.matrix.rx.RxRoom
import im.vector.riotredesign.core.platform.VectorViewModel
import im.vector.riotredesign.features.home.room.detail.timeline.format.NoticeEventFormatter
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
@ -37,27 +34,30 @@ import java.text.SimpleDateFormat
import java.util.*


val dateFormat = SimpleDateFormat("EEE, d MMM yyyy HH:mm", Locale.getDefault())

data class MessageActionState(
val roomId: String,
val eventId: String,
val informationData: MessageInformationData,
val timelineEvent: TimelineEvent?
val timelineEvent: Async<TimelineEvent> = Uninitialized
) : MvRxState {

constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData)


private val dateFormat = SimpleDateFormat("EEE, d MMM yyyy HH:mm", Locale.getDefault())

fun senderName(): String = informationData.memberName?.toString() ?: ""

fun time(): String? = timelineEvent?.root?.originServerTs?.let { dateFormat.format(Date(it)) }
fun time(): String? = timelineEvent()?.root?.originServerTs?.let { dateFormat.format(Date(it)) }
?: ""

fun canReact(): Boolean = timelineEvent?.root?.type == EventType.MESSAGE && timelineEvent.sendState.isSent()
fun canReact(): Boolean = timelineEvent()?.root?.type == EventType.MESSAGE && timelineEvent()?.sendState?.isSent() == true

fun messageBody(eventHtmlRenderer: EventHtmlRenderer?, noticeEventFormatter: NoticeEventFormatter?): CharSequence? {
return when (timelineEvent?.root?.getClearType()) {
return when (timelineEvent()?.root?.getClearType()) {
EventType.MESSAGE -> {
val messageContent: MessageContent? = timelineEvent.annotations?.editSummary?.aggregatedContent?.toModel()
?: timelineEvent.root.getClearContent().toModel()
val messageContent: MessageContent? = timelineEvent()?.annotations?.editSummary?.aggregatedContent?.toModel()
?: timelineEvent()?.root?.getClearContent().toModel()
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
eventHtmlRenderer?.render(messageContent.formattedBody
?: messageContent.body)
@ -72,7 +72,7 @@ data class MessageActionState(
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
EventType.CALL_ANSWER -> {
noticeEventFormatter?.format(timelineEvent)
timelineEvent()?.let { noticeEventFormatter?.format(it) }
}
else -> null
}
@ -85,10 +85,14 @@ data class MessageActionState(
class MessageActionsViewModel @AssistedInject constructor(@Assisted
initialState: MessageActionState,
private val eventHtmlRenderer: EventHtmlRenderer,
private val session: Session,
session: Session,
private val noticeEventFormatter: NoticeEventFormatter
) : VectorViewModel<MessageActionState>(initialState) {


private val eventId = initialState.eventId
private val room = session.getRoom(initialState.roomId)

@AssistedInject.Factory
interface Factory {
fun create(initialState: MessageActionState): MessageActionsViewModel
@ -101,18 +105,19 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
return fragment.messageActionViewModelFactory.create(state)
}

override fun initialState(viewModelContext: ViewModelContext): MessageActionState? {
val session = (viewModelContext.activity as HasScreenInjector).injector().session()
val args: TimelineEventFragmentArgs = viewModelContext.args()
val event = session.getRoom(args.roomId)?.getTimeLineEvent(args.eventId)
return MessageActionState(
args.roomId,
args.eventId,
args.informationData,
event
)
}
}

init {
observeEvent()
}

private fun observeEvent() {
if (room == null) return
RxRoom(room)
.liveTimelineEvent(eventId)
.execute {
copy(timelineEvent = it)
}
}

fun resolveBody(state: MessageActionState): CharSequence? {

View File

@ -55,7 +55,8 @@ class MessageMenuFragment : VectorBaseFragment() {
val inflater = LayoutInflater.from(linearLayout.context)
linearLayout.removeAllViews()
var insertIndex = 0
state.actions.forEachIndexed { index, action ->
val actions = state.actions()
actions?.forEachIndexed { index, action ->
inflateActionView(action, inflater, linearLayout)?.let {
it.setOnClickListener {
interactionListener?.didSelectMenuAction(action)
@ -63,7 +64,7 @@ class MessageMenuFragment : VectorBaseFragment() {
linearLayout.addView(it, insertIndex)
insertIndex++
if (addSeparators) {
if (index < state.actions.size - 1) {
if (index < actions.size - 1) {
linearLayout.addView(inflateSeparatorView(), insertIndex)
insertIndex++
}

View File

@ -15,10 +15,7 @@
*/
package im.vector.riotredesign.features.home.room.detail.timeline.action

import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.airbnb.mvrx.*
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session
@ -30,6 +27,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageImageConte
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.rx.RxRoom
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.VectorViewModel
import im.vector.riotredesign.core.resources.StringProvider
@ -44,7 +42,7 @@ data class MessageMenuState(
val roomId: String,
val eventId: String,
val informationData: MessageInformationData,
val actions: List<SimpleAction> = emptyList()
val actions: Async<List<SimpleAction>> = Uninitialized
) : MvRxState {

constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData)
@ -63,6 +61,12 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
fun create(initialState: MessageMenuState): MessageMenuViewModel
}

private val room = session.getRoom(initialState.roomId)
?: throw IllegalStateException("Shouldn't use this ViewModel without a room")

private val eventId = initialState.eventId
private val informationData: MessageInformationData = initialState.informationData

companion object : MvRxViewModelFactory<MessageMenuViewModel, MessageMenuState> {

const val ACTION_ADD_REACTION = "add_reaction"
@ -87,13 +91,23 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
}

init {
setState { reduceState(this) }
observeEvent()
}

private fun reduceState(state: MessageMenuState): MessageMenuState {
val event = session.getRoom(state.roomId)?.getTimeLineEvent(state.eventId) ?: return state
private fun observeEvent() {
RxRoom(room)
.liveTimelineEvent(eventId)
?.map {
actionsForEvent(it)
}
?.execute {
copy(actions = it)
}
}

val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent?.toModel()
private fun actionsForEvent(event: TimelineEvent): List<SimpleAction> {

val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent.toModel()
?: event.root.getClearContent().toModel()
val type = messageContent?.type

@ -114,7 +128,7 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
//TODO is downloading attachement?

if (canReact(event, messageContent)) {
this.add(SimpleAction(ACTION_ADD_REACTION, R.string.message_add_reaction, R.drawable.ic_add_reaction, event.root.eventId))
this.add(SimpleAction(ACTION_ADD_REACTION, R.string.message_add_reaction, R.drawable.ic_add_reaction, eventId))
}
if (canCopy(type)) {
//TODO copy images? html? see ClipBoard
@ -122,23 +136,23 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
}

if (canReply(event, messageContent)) {
this.add(SimpleAction(ACTION_REPLY, R.string.reply, R.drawable.ic_reply, event.root.eventId))
this.add(SimpleAction(ACTION_REPLY, R.string.reply, R.drawable.ic_reply, eventId))
}

if (canEdit(event, session.sessionParams.credentials.userId)) {
this.add(SimpleAction(ACTION_EDIT, R.string.edit, R.drawable.ic_edit, event.root.eventId))
this.add(SimpleAction(ACTION_EDIT, R.string.edit, R.drawable.ic_edit, eventId))
}

if (canRedact(event, session.sessionParams.credentials.userId)) {
this.add(SimpleAction(ACTION_DELETE, R.string.delete, R.drawable.ic_delete, event.root.eventId))
this.add(SimpleAction(ACTION_DELETE, R.string.delete, R.drawable.ic_delete, eventId))
}

if (canQuote(event, messageContent)) {
this.add(SimpleAction(ACTION_QUOTE, R.string.quote, R.drawable.ic_quote, state.eventId))
this.add(SimpleAction(ACTION_QUOTE, R.string.quote, R.drawable.ic_quote, eventId))
}

if (canViewReactions(event)) {
this.add(SimpleAction(ACTION_VIEW_REACTIONS, R.string.message_view_reaction, R.drawable.ic_view_reactions, state.informationData))
this.add(SimpleAction(ACTION_VIEW_REACTIONS, R.string.message_view_reaction, R.drawable.ic_view_reactions, informationData))
}

if (canShare(type)) {
@ -162,20 +176,20 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M

this.add(SimpleAction(VIEW_SOURCE, R.string.view_source, R.drawable.ic_view_source, JSONObject(event.root.toContent()).toString(4)))
if (event.isEncrypted()) {
val decryptedContent = event.root.mClearEvent?.toContent()?.let {
val decryptedContent = event.root.mClearEvent.toContent()?.let {
JSONObject(it).toString(4)
} ?: stringProvider.getString(R.string.encryption_information_decryption_error)
this.add(SimpleAction(VIEW_DECRYPTED_SOURCE, R.string.view_decrypted_source, R.drawable.ic_view_source, decryptedContent))
}
this.add(SimpleAction(ACTION_COPY_PERMALINK, R.string.permalink, R.drawable.ic_permalink, state.eventId))
this.add(SimpleAction(ACTION_COPY_PERMALINK, R.string.permalink, R.drawable.ic_permalink, event.root.eventId))

if (session.sessionParams.credentials.userId != event.root.senderId && event.root.getClearType() == EventType.MESSAGE) {
//not sent by me
this.add(SimpleAction(ACTION_FLAG, R.string.report_content, R.drawable.ic_flag, state.eventId))
this.add(SimpleAction(ACTION_FLAG, R.string.report_content, R.drawable.ic_flag, event.root.eventId))
}
}
}
return state.copy(actions = actions)
return actions
}



View File

@ -47,7 +47,7 @@ class QuickReactionFragment : VectorBaseFragment() {
injector.inject(this)
}

lateinit var textViews: List<TextView>
private lateinit var textViews: List<TextView>

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -63,7 +63,8 @@ class QuickReactionFragment : VectorBaseFragment() {
}

override fun invalidate() = withState(viewModel) {
it.quickStates.forEachIndexed { index, qs ->
val quickReactionsStates = it.quickStates() ?: return@withState
quickReactionsStates.forEachIndexed { index, qs ->
textViews[index].text = qs.reaction
textViews[index].alpha = if (qs.isSelected) 0.2f else 1f
}

View File

@ -15,13 +15,11 @@
*/
package im.vector.riotredesign.features.home.room.detail.timeline.action

import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.airbnb.mvrx.*
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.rx.RxRoom
import im.vector.riotredesign.core.platform.VectorViewModel
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData

@ -37,7 +35,7 @@ data class QuickReactionState(
val roomId: String,
val eventId: String,
val informationData: MessageInformationData,
val quickStates: List<ToggleState> = emptyList(),
val quickStates: Async<List<ToggleState>> = Uninitialized,
val result: ToggleState? = null
/** Pair of 'clickedOn' and current toggles state*/
) : MvRxState {
@ -56,6 +54,9 @@ class QuickReactionViewModel @AssistedInject constructor(@Assisted initialState:
fun create(initialState: QuickReactionState): QuickReactionViewModel
}

private val room = session.getRoom(initialState.roomId)
private val eventId = initialState.eventId

companion object : MvRxViewModelFactory<QuickReactionViewModel, QuickReactionState> {

val quickEmojis = listOf("👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀")
@ -67,22 +68,30 @@ class QuickReactionViewModel @AssistedInject constructor(@Assisted initialState:
}

init {
setState { reduceState(this) }
observeReactions()
}

private fun reduceState(state: QuickReactionState): QuickReactionState {
val event = session.getRoom(state.roomId)?.getTimeLineEvent(state.eventId) ?: return state
val summary = event.annotations?.reactionsSummary
val quickReactions = quickEmojis.map { emoji ->
ToggleState(emoji, summary?.firstOrNull { it.key == emoji }?.addedByMe ?: false)
}
return state.copy(quickStates = quickReactions)
private fun observeReactions() {
if (room == null) return
RxRoom(room)
.liveAnnotationSummary(eventId)
.map { annotations ->
quickEmojis.map { emoji ->
ToggleState(emoji, annotations.reactionsSummary.firstOrNull { it.key == emoji }?.addedByMe
?: false)
}
}
.execute {
copy(quickStates = it)
}
}


fun didSelect(index: Int) = withState {
val isSelected = it.quickStates[index].isSelected
val selectedReaction = it.quickStates()?.get(index) ?: return@withState
val isSelected = selectedReaction.isSelected
setState {
copy(result = ToggleState(it.quickStates[index].reaction, !isSelected))
copy(result = ToggleState(selectedReaction.reaction, !isSelected))
}
}


View File

@ -85,10 +85,8 @@ class ViewReactionViewModel @AssistedInject constructor(@Assisted
.liveAnnotationSummary(eventId)
.flatMapSingle { summaries ->
Observable
.fromIterable(summaries)
.flatMapIterable { it.reactionsSummary
.filter { reactionAggregatedSummary -> isSingleEmoji(reactionAggregatedSummary.key) }
}
.fromIterable(summaries.reactionsSummary)
.filter { reactionAggregatedSummary -> isSingleEmoji(reactionAggregatedSummary.key) }
.toReactionInfoList()
}
.execute {
@ -112,7 +110,6 @@ class ViewReactionViewModel @AssistedInject constructor(@Assisted
timelineDateFormatter.formatMessageHour(localDate)
)
}
}
.toList()
}.toList()
}
}