Merge pull request #3520 from vector-im/feature/fga/timeline_virtual_room

Feature/fga/timeline virtual room
This commit is contained in:
Benoit Marty 2021-06-24 14:43:00 +02:00 committed by GitHub
commit f5ecaa0339
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 303 additions and 8 deletions

1
newsfragment/3520.misc Normal file
View File

@ -0,0 +1 @@
VoIP: Merge virtual room timeline in corresponding native room (call events only).

View File

@ -27,11 +27,21 @@ import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
class CallUserMapper(private val session: Session, private val protocolsChecker: CallProtocolsChecker) { class CallUserMapper(private val session: Session, private val protocolsChecker: CallProtocolsChecker) {
fun nativeRoomForVirtualRoom(roomId: String): String? { fun nativeRoomForVirtualRoom(roomId: String): String? {
if (!protocolsChecker.supportVirtualRooms) return null
val virtualRoom = session.getRoom(roomId) ?: return null val virtualRoom = session.getRoom(roomId) ?: return null
val virtualRoomEvent = virtualRoom.getAccountDataEvent(RoomAccountDataTypes.EVENT_TYPE_VIRTUAL_ROOM) val virtualRoomEvent = virtualRoom.getAccountDataEvent(RoomAccountDataTypes.EVENT_TYPE_VIRTUAL_ROOM)
return virtualRoomEvent?.content?.toModel<RoomVirtualContent>()?.nativeRoomId return virtualRoomEvent?.content?.toModel<RoomVirtualContent>()?.nativeRoomId
} }
fun virtualRoomForNativeRoom(roomId: String): String? {
if (!protocolsChecker.supportVirtualRooms) return null
val virtualRoomEvents = session.accountDataService().getRoomAccountDataEvents(setOf(RoomAccountDataTypes.EVENT_TYPE_VIRTUAL_ROOM))
return virtualRoomEvents.firstOrNull {
val virtualRoomContent = it.content.toModel<RoomVirtualContent>()
virtualRoomContent?.nativeRoomId == roomId
}?.roomId
}
suspend fun getOrCreateVirtualRoomForRoom(roomId: String, opponentUserId: String): String? { suspend fun getOrCreateVirtualRoomForRoom(roomId: String, opponentUserId: String): String? {
protocolsChecker.awaitCheckProtocols() protocolsChecker.awaitCheckProtocols()
if (!protocolsChecker.supportVirtualRooms) return null if (!protocolsChecker.supportVirtualRooms) return null

View File

@ -1614,8 +1614,7 @@ class RoomDetailFragment @Inject constructor(
override fun onEventLongClicked(informationData: MessageInformationData, messageContent: Any?, view: View): Boolean { override fun onEventLongClicked(informationData: MessageInformationData, messageContent: Any?, view: View): Boolean {
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
val roomId = roomDetailArgs.roomId val roomId = roomDetailViewModel.timeline.getTimelineEventWithId(informationData.eventId)?.roomId ?: return false
this.view?.hideKeyboard() this.view?.hideKeyboard()
MessageActionsBottomSheet MessageActionsBottomSheet

View File

@ -48,8 +48,8 @@ import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrate
import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider
import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator
import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler
import im.vector.app.features.home.room.detail.timeline.factory.TimelineFactory
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder
import im.vector.app.features.home.room.detail.timeline.helper.TimelineSettingsFactory
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
import im.vector.app.features.home.room.typing.TypingHelper import im.vector.app.features.home.room.typing.TypingHelper
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
@ -118,7 +118,7 @@ class RoomDetailViewModel @AssistedInject constructor(
private val chatEffectManager: ChatEffectManager, private val chatEffectManager: ChatEffectManager,
private val directRoomHelper: DirectRoomHelper, private val directRoomHelper: DirectRoomHelper,
private val jitsiService: JitsiService, private val jitsiService: JitsiService,
timelineSettingsFactory: TimelineSettingsFactory timelineFactory: TimelineFactory
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState), ) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener { Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener {
@ -126,9 +126,8 @@ class RoomDetailViewModel @AssistedInject constructor(
private val eventId = initialState.eventId private val eventId = initialState.eventId
private val invisibleEventsObservable = BehaviorRelay.create<RoomDetailAction.TimelineEventTurnsInvisible>() private val invisibleEventsObservable = BehaviorRelay.create<RoomDetailAction.TimelineEventTurnsInvisible>()
private val visibleEventsObservable = BehaviorRelay.create<RoomDetailAction.TimelineEventTurnsVisible>() private val visibleEventsObservable = BehaviorRelay.create<RoomDetailAction.TimelineEventTurnsVisible>()
private val timelineSettings = timelineSettingsFactory.create()
private var timelineEvents = PublishRelay.create<List<TimelineEvent>>() private var timelineEvents = PublishRelay.create<List<TimelineEvent>>()
val timeline = room.createTimeline(eventId, timelineSettings) val timeline = timelineFactory.createTimeline(viewModelScope, room, eventId)
// Same lifecycle than the ViewModel (survive to screen rotation) // Same lifecycle than the ViewModel (survive to screen rotation)
val previewUrlRetriever = PreviewUrlRetriever(session, viewModelScope) val previewUrlRetriever = PreviewUrlRetriever(session, viewModelScope)

View File

@ -16,6 +16,7 @@
package im.vector.app.features.home.room.detail.timeline.factory package im.vector.app.features.home.room.detail.timeline.factory
import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.features.call.vectorCallService
import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.TimelineEventController
@ -26,6 +27,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHold
import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem
import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem_ import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent
@ -38,6 +40,7 @@ import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject import javax.inject.Inject
class CallItemFactory @Inject constructor( class CallItemFactory @Inject constructor(
private val session: Session,
private val messageColorProvider: MessageColorProvider, private val messageColorProvider: MessageColorProvider,
private val messageInformationDataFactory: MessageInformationDataFactory, private val messageInformationDataFactory: MessageInformationDataFactory,
private val messageItemAttributesFactory: MessageItemAttributesFactory, private val messageItemAttributesFactory: MessageItemAttributesFactory,
@ -132,7 +135,8 @@ class CallItemFactory @Inject constructor(
isStillActive: Boolean, isStillActive: Boolean,
callback: TimelineEventController.Callback? callback: TimelineEventController.Callback?
): CallTileTimelineItem? { ): CallTileTimelineItem? {
val userOfInterest = roomSummariesHolder.get(roomId)?.toMatrixItem() ?: return null val correctedRoomId = session.vectorCallService.userMapper.nativeRoomForVirtualRoom(roomId) ?: roomId
val userOfInterest = roomSummariesHolder.get(correctedRoomId)?.toMatrixItem() ?: return null
val attributes = messageItemAttributesFactory.create(null, informationData, callback).let { val attributes = messageItemAttributesFactory.create(null, informationData, callback).let {
CallTileTimelineItem.Attributes( CallTileTimelineItem.Attributes(
callId = callId, callId = callId,

View File

@ -0,0 +1,59 @@
/*
* Copyright (c) 2021 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.app.features.home.room.detail.timeline.factory
import im.vector.app.features.call.vectorCallService
import im.vector.app.features.home.room.detail.timeline.helper.TimelineSettingsFactory
import im.vector.app.features.home.room.detail.timeline.merged.MergedTimelines
import kotlinx.coroutines.CoroutineScope
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import javax.inject.Inject
private val secondaryTimelineAllowedTypes = listOf(
EventType.CALL_HANGUP,
EventType.CALL_INVITE,
EventType.CALL_REJECT,
EventType.CALL_ANSWER
)
class TimelineFactory @Inject constructor(private val session: Session, private val timelineSettingsFactory: TimelineSettingsFactory) {
fun createTimeline(coroutineScope: CoroutineScope, mainRoom: Room, eventId: String?): Timeline {
val settings = timelineSettingsFactory.create()
if (!session.vectorCallService.protocolChecker.supportVirtualRooms) {
return mainRoom.createTimeline(eventId, settings)
}
val virtualRoomId = session.vectorCallService.userMapper.virtualRoomForNativeRoom(mainRoom.roomId)
return if (virtualRoomId == null) {
mainRoom.createTimeline(eventId, settings)
} else {
val virtualRoom = session.getRoom(virtualRoomId)!!
MergedTimelines(
coroutineScope = coroutineScope,
mainTimeline = mainRoom.createTimeline(eventId, settings),
secondaryTimelineParams = MergedTimelines.SecondaryTimelineParams(
timeline = virtualRoom.createTimeline(null, settings),
shouldFilterTypes = true,
allowedTypes = secondaryTimelineAllowedTypes
)
)
}
}
}

View File

@ -0,0 +1,223 @@
/*
* Copyright (c) 2021 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.app.features.home.room.detail.timeline.merged
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import kotlin.reflect.KMutableProperty0
/**
* This can be use to merge timeline tiles from 2 different rooms.
* Be aware it wont work properly with permalink.
*/
class MergedTimelines(
private val coroutineScope: CoroutineScope,
private val mainTimeline: Timeline,
private val secondaryTimelineParams: SecondaryTimelineParams) : Timeline by mainTimeline {
data class SecondaryTimelineParams(
val timeline: Timeline,
val disableReadReceipts: Boolean = true,
val shouldFilterTypes: Boolean = false,
val allowedTypes: List<String> = emptyList()
)
private var mainIsInit = false
private var secondaryIsInit = false
private val secondaryTimeline = secondaryTimelineParams.timeline
private val listenersMapping = HashMap<Timeline.Listener, List<ListenerInterceptor>>()
private val mainTimelineEvents = ArrayList<TimelineEvent>()
private val secondaryTimelineEvents = ArrayList<TimelineEvent>()
private val positionsMapping = HashMap<String, Int>()
private val mergedEvents = ArrayList<TimelineEvent>()
private val processingSemaphore = Semaphore(1)
private class ListenerInterceptor(
var timeline: Timeline?,
private val wrappedListener: Timeline.Listener,
private val shouldFilterTypes: Boolean,
private val allowedTypes: List<String>,
private val onTimelineUpdate: (List<TimelineEvent>) -> Unit
) : Timeline.Listener by wrappedListener {
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
val filteredEvents = if (shouldFilterTypes) {
snapshot.filter {
allowedTypes.contains(it.root.getClearType())
}
} else {
snapshot
}
onTimelineUpdate(filteredEvents)
}
}
override fun addListener(listener: Timeline.Listener): Boolean {
val mainTimelineListener = ListenerInterceptor(
timeline = mainTimeline,
wrappedListener = listener,
shouldFilterTypes = false,
allowedTypes = emptyList()) {
processTimelineUpdates(::mainIsInit, mainTimelineEvents, it)
}
val secondaryTimelineListener = ListenerInterceptor(
timeline = secondaryTimeline,
wrappedListener = listener,
shouldFilterTypes = secondaryTimelineParams.shouldFilterTypes,
allowedTypes = secondaryTimelineParams.allowedTypes) {
processTimelineUpdates(::secondaryIsInit, secondaryTimelineEvents, it)
}
listenersMapping[listener] = listOf(mainTimelineListener, secondaryTimelineListener)
return mainTimeline.addListener(mainTimelineListener) && secondaryTimeline.addListener(secondaryTimelineListener)
}
override fun removeListener(listener: Timeline.Listener): Boolean {
return listenersMapping.remove(listener)?.let {
it.forEach { listener ->
listener.timeline?.removeListener(listener)
listener.timeline = null
}
true
} ?: false
}
override fun removeAllListeners() {
mainTimeline.removeAllListeners()
secondaryTimeline.removeAllListeners()
}
override fun start() {
mainTimeline.start()
secondaryTimeline.start()
}
override fun dispose() {
mainTimeline.dispose()
secondaryTimeline.dispose()
}
override fun restartWithEventId(eventId: String?) {
mainTimeline.restartWithEventId(eventId)
}
override fun hasMoreToLoad(direction: Timeline.Direction): Boolean {
return mainTimeline.hasMoreToLoad(direction) || secondaryTimeline.hasMoreToLoad(direction)
}
override fun paginate(direction: Timeline.Direction, count: Int) {
mainTimeline.paginate(direction, count)
secondaryTimeline.paginate(direction, count)
}
override fun pendingEventCount(): Int {
return mainTimeline.pendingEventCount() + secondaryTimeline.pendingEventCount()
}
override fun failedToDeliverEventCount(): Int {
return mainTimeline.pendingEventCount() + secondaryTimeline.pendingEventCount()
}
override fun getTimelineEventAtIndex(index: Int): TimelineEvent? {
return mergedEvents.getOrNull(index)
}
override fun getIndexOfEvent(eventId: String?): Int? {
return positionsMapping[eventId]
}
override fun getTimelineEventWithId(eventId: String?): TimelineEvent? {
return positionsMapping[eventId]?.let {
getTimelineEventAtIndex(it)
}
}
private fun processTimelineUpdates(isInit: KMutableProperty0<Boolean>, eventsRef: MutableList<TimelineEvent>, newData: List<TimelineEvent>) {
coroutineScope.launch(Dispatchers.Default) {
processingSemaphore.withPermit {
isInit.set(true)
eventsRef.apply {
clear()
addAll(newData)
}
mergeTimeline()
}
}
}
private suspend fun mergeTimeline() {
val merged = mutableListOf<TimelineEvent>()
val mainItr = mainTimelineEvents.toList().listIterator()
val secondaryItr = secondaryTimelineEvents.toList().listIterator()
var index = 0
var correctedSenderInfo: SenderInfo? = mainTimelineEvents.firstOrNull()?.senderInfo
if (!mainIsInit || !secondaryIsInit) {
return
}
while (merged.size < mainTimelineEvents.size + secondaryTimelineEvents.size) {
if (mainItr.hasNext()) {
val nextMain = mainItr.next()
correctedSenderInfo = nextMain.senderInfo
if (secondaryItr.hasNext()) {
val nextSecondary = secondaryItr.next()
if (nextSecondary.root.originServerTs ?: 0 > nextMain.root.originServerTs ?: 0) {
positionsMapping[nextSecondary.eventId] = index
merged.add(nextSecondary.correctBeforeMerging(correctedSenderInfo))
mainItr.previous()
} else {
positionsMapping[nextMain.eventId] = index
merged.add(nextMain)
secondaryItr.previous()
}
} else {
positionsMapping[nextMain.eventId] = index
merged.add(nextMain)
}
} else if (secondaryItr.hasNext()) {
val nextSecondary = secondaryItr.next()
positionsMapping[nextSecondary.eventId] = index
merged.add(nextSecondary.correctBeforeMerging(correctedSenderInfo))
}
index++
}
mergedEvents.apply {
clear()
addAll(merged)
}
withContext(Dispatchers.Main) {
listenersMapping.keys.forEach { listener ->
tryOrNull { listener.onTimelineUpdated(merged) }
}
}
}
private fun TimelineEvent.correctBeforeMerging(correctedSenderInfo: SenderInfo?): TimelineEvent {
return copy(
senderInfo = correctedSenderInfo ?: senderInfo,
readReceipts = if (secondaryTimelineParams.disableReadReceipts) emptyList() else readReceipts
)
}
}

View File

@ -53,7 +53,7 @@
android:layout_marginTop="12dp" android:layout_marginTop="12dp"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:layout_marginBottom="12dp" android:layout_marginBottom="12dp"
android:textColor="?vctr_notice_secondary" android:textColor="?vctr_content_secondary"
tools:text="@string/video_call_in_progress" /> tools:text="@string/video_call_in_progress" />