Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/fga/timeline virtual room #3520

Merged
merged 11 commits into from
Jun 24, 2021
1 change: 1 addition & 0 deletions newsfragment/3520.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VoIP: Merge virtual room timeline in corresponding native room (call events only).
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

fun nativeRoomForVirtualRoom(roomId: String): String? {
if (!protocolsChecker.supportVirtualRooms) return null
val virtualRoom = session.getRoom(roomId) ?: return null
val virtualRoomEvent = virtualRoom.getAccountDataEvent(RoomAccountDataTypes.EVENT_TYPE_VIRTUAL_ROOM)
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? {
protocolsChecker.awaitCheckProtocols()
if (!protocolsChecker.supportVirtualRooms) return null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1614,8 +1614,7 @@ class RoomDetailFragment @Inject constructor(

override fun onEventLongClicked(informationData: MessageInformationData, messageContent: Any?, view: View): Boolean {
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
val roomId = roomDetailArgs.roomId

val roomId = roomDetailViewModel.timeline.getTimelineEventWithId(informationData.eventId)?.roomId ?: return false
this.view?.hideKeyboard()

MessageActionsBottomSheet
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.home.room.detail.composer.rainbow.RainbowGenerator
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.TimelineSettingsFactory
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.powerlevel.PowerLevelsObservableFactory
Expand Down Expand Up @@ -118,17 +118,16 @@ class RoomDetailViewModel @AssistedInject constructor(
private val chatEffectManager: ChatEffectManager,
private val directRoomHelper: DirectRoomHelper,
private val jitsiService: JitsiService,
timelineSettingsFactory: TimelineSettingsFactory
timelineFactory: TimelineFactory
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener {

private val room = session.getRoom(initialState.roomId)!!
private val eventId = initialState.eventId
private val invisibleEventsObservable = BehaviorRelay.create<RoomDetailAction.TimelineEventTurnsInvisible>()
private val visibleEventsObservable = BehaviorRelay.create<RoomDetailAction.TimelineEventTurnsVisible>()
private val timelineSettings = timelineSettingsFactory.create()
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)
val previewUrlRetriever = PreviewUrlRetriever(session, viewModelScope)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package im.vector.app.features.home.room.detail.timeline.factory

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.home.room.detail.timeline.MessageColorProvider
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
Expand All @@ -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.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.toModel
import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent
Expand All @@ -38,6 +40,7 @@ import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject

class CallItemFactory @Inject constructor(
private val session: Session,
private val messageColorProvider: MessageColorProvider,
private val messageInformationDataFactory: MessageInformationDataFactory,
private val messageItemAttributesFactory: MessageItemAttributesFactory,
Expand Down Expand Up @@ -132,7 +135,8 @@ class CallItemFactory @Inject constructor(
isStillActive: Boolean,
callback: TimelineEventController.Callback?
): 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 {
CallTileTimelineItem.Attributes(
callId = callId,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
)
)
}
}
}
Original file line number Diff line number Diff line change
@@ -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
)
}
}
11 changes: 0 additions & 11 deletions vector/src/main/res/drawable/rounded_rect_shape_16.xml

This file was deleted.

Loading