mirror of
https://github.com/element-hq/element-android
synced 2024-11-28 13:38:49 +03:00
Jitsi call: start using call tiles
This commit is contained in:
parent
cdf97fc29f
commit
b7e5a6cf28
14 changed files with 259 additions and 148 deletions
|
@ -24,18 +24,31 @@ import androidx.lifecycle.Lifecycle
|
|||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import com.facebook.react.bridge.JavaOnlyMap
|
||||
import org.jitsi.meet.sdk.BroadcastEmitter
|
||||
import org.jitsi.meet.sdk.BroadcastEvent
|
||||
import org.jitsi.meet.sdk.JitsiMeet
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
|
||||
private const val CONFERENCE_URL_DATA_KEY = "url"
|
||||
|
||||
fun BroadcastEvent.extractConferenceUrl(): String? {
|
||||
return when (type) {
|
||||
BroadcastEvent.Type.CONFERENCE_TERMINATED,
|
||||
BroadcastEvent.Type.CONFERENCE_WILL_JOIN,
|
||||
BroadcastEvent.Type.CONFERENCE_JOINED -> data["url"] as? String
|
||||
BroadcastEvent.Type.CONFERENCE_JOINED -> data[CONFERENCE_URL_DATA_KEY] as? String
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
class JitsiBroadcastEmitter(private val context: Context) {
|
||||
|
||||
fun emitConferenceEnded() {
|
||||
val broadcastEventData = JavaOnlyMap.of(CONFERENCE_URL_DATA_KEY, JitsiMeet.getCurrentConference())
|
||||
BroadcastEmitter(context).sendBroadcast(BroadcastEvent.Type.CONFERENCE_TERMINATED.name, broadcastEventData)
|
||||
}
|
||||
}
|
||||
|
||||
class JitsiBroadcastEventObserver(private val context: Context,
|
||||
private val onBroadcastEvent: (BroadcastEvent) -> Unit) : LifecycleObserver {
|
||||
|
|
@ -31,7 +31,6 @@ import com.airbnb.mvrx.Fail
|
|||
import com.airbnb.mvrx.MvRx
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.viewModel
|
||||
import com.facebook.react.bridge.JavaOnlyMap
|
||||
import com.facebook.react.modules.core.PermissionListener
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import im.vector.app.R
|
||||
|
@ -40,7 +39,6 @@ import im.vector.app.core.extensions.exhaustive
|
|||
import im.vector.app.core.platform.VectorBaseActivity
|
||||
import im.vector.app.databinding.ActivityJitsiBinding
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.jitsi.meet.sdk.BroadcastEmitter
|
||||
import org.jitsi.meet.sdk.BroadcastEvent
|
||||
import org.jitsi.meet.sdk.JitsiMeet
|
||||
import org.jitsi.meet.sdk.JitsiMeetActivityDelegate
|
||||
|
@ -115,8 +113,7 @@ class VectorJitsiActivity : VectorBaseActivity<ActivityJitsiBinding>(), JitsiMee
|
|||
jitsiMeetView?.dispose()
|
||||
// Fake emitting CONFERENCE_TERMINATED event when currentConf is not null (probably when closing the PiP screen).
|
||||
if (currentConf != null) {
|
||||
val broadcastEventData = JavaOnlyMap.of("url", currentConf)
|
||||
BroadcastEmitter(this).sendBroadcast(BroadcastEvent.Type.CONFERENCE_TERMINATED.name, broadcastEventData)
|
||||
JitsiBroadcastEmitter(this).emitConferenceEnded()
|
||||
}
|
||||
JitsiMeetActivityDelegate.onHostDestroy(this)
|
||||
super.onDestroy()
|
||||
|
|
|
@ -27,6 +27,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachme
|
|||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.widgets.model.Widget
|
||||
import org.matrix.android.sdk.api.session.widgets.model.WidgetContent
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
|
||||
sealed class RoomDetailAction : VectorViewModelAction {
|
||||
|
@ -90,6 +91,10 @@ sealed class RoomDetailAction : VectorViewModelAction {
|
|||
object ManageIntegrations : RoomDetailAction()
|
||||
data class AddJitsiWidget(val withVideo: Boolean) : RoomDetailAction()
|
||||
data class RemoveWidget(val widgetId: String) : RoomDetailAction()
|
||||
|
||||
object JoinJitsiCall: RoomDetailAction()
|
||||
object LeaveJitsiCall: RoomDetailAction()
|
||||
|
||||
data class EnsureNativeWidgetAllowed(val widget: Widget,
|
||||
val userJustAccepted: Boolean,
|
||||
val grantedEvents: RoomDetailViewEvents) : RoomDetailAction()
|
||||
|
|
|
@ -67,6 +67,7 @@ import com.airbnb.mvrx.Success
|
|||
import com.airbnb.mvrx.args
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import com.facebook.react.bridge.JavaOnlyMap
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.jakewharton.rxbinding3.view.focusChanges
|
||||
import com.jakewharton.rxbinding3.widget.textChanges
|
||||
|
@ -120,6 +121,7 @@ import im.vector.app.features.attachments.preview.AttachmentsPreviewArgs
|
|||
import im.vector.app.features.attachments.toGroupedContentAttachmentData
|
||||
import im.vector.app.features.call.SharedKnownCallsViewModel
|
||||
import im.vector.app.features.call.VectorCallActivity
|
||||
import im.vector.app.features.call.conference.JitsiBroadcastEmitter
|
||||
import im.vector.app.features.call.conference.JitsiBroadcastEventObserver
|
||||
import im.vector.app.features.call.conference.JitsiCallViewModel
|
||||
import im.vector.app.features.call.conference.extractConferenceUrl
|
||||
|
@ -176,6 +178,7 @@ import nl.dionsegijn.konfetti.models.Shape
|
|||
import nl.dionsegijn.konfetti.models.Size
|
||||
import org.billcarsonfr.jsonviewer.JSonViewerDialog
|
||||
import org.commonmark.parser.Parser
|
||||
import org.jitsi.meet.sdk.BroadcastEmitter
|
||||
import org.jitsi.meet.sdk.BroadcastEvent
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||
|
@ -394,6 +397,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
RoomDetailViewEvents.OpenActiveWidgetBottomSheet -> onViewWidgetsClicked()
|
||||
is RoomDetailViewEvents.ShowInfoOkDialog -> showDialogWithMessage(it.message)
|
||||
is RoomDetailViewEvents.JoinJitsiConference -> joinJitsiRoom(it.widget, it.withVideo)
|
||||
RoomDetailViewEvents.LeaveJitsiConference -> leaveJitsiConference()
|
||||
RoomDetailViewEvents.ShowWaitingView -> vectorBaseActivity.showWaitingView()
|
||||
RoomDetailViewEvents.HideWaitingView -> vectorBaseActivity.hideWaitingView()
|
||||
is RoomDetailViewEvents.RequestNativeWidgetPermission -> requestNativeWidgetPermission(it)
|
||||
|
@ -416,6 +420,10 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun leaveJitsiConference() {
|
||||
JitsiBroadcastEmitter(vectorBaseActivity).emitConferenceEnded()
|
||||
}
|
||||
|
||||
private fun onBroadcastEvent(event: BroadcastEvent) {
|
||||
roomDetailViewModel.handle(RoomDetailAction.UpdateJoinJitsiCallStatus(event))
|
||||
}
|
||||
|
|
|
@ -45,6 +45,7 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
|
|||
|
||||
data class NavigateToEvent(val eventId: String) : RoomDetailViewEvents()
|
||||
data class JoinJitsiConference(val widget: Widget, val withVideo: Boolean) : RoomDetailViewEvents()
|
||||
object LeaveJitsiConference : RoomDetailViewEvents()
|
||||
|
||||
object OpenInvitePeople : RoomDetailViewEvents()
|
||||
object OpenSetRoomAvatarDialog : RoomDetailViewEvents()
|
||||
|
|
|
@ -65,6 +65,7 @@ import kotlinx.coroutines.withContext
|
|||
import org.commonmark.parser.Parser
|
||||
import org.commonmark.renderer.html.HtmlRenderer
|
||||
import org.jitsi.meet.sdk.BroadcastEvent
|
||||
import org.jitsi.meet.sdk.JitsiMeet
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.MatrixPatterns
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
|
@ -240,10 +241,17 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
widgets.filter { it.isActive }
|
||||
}
|
||||
.execute { widgets ->
|
||||
val jitsiConfId = widgets()?.firstOrNull { it.type == WidgetType.Jitsi }?.let { jitsiWidget ->
|
||||
jitsiService.extractProperties(jitsiWidget)?.confId
|
||||
val jitsiWidget = widgets()?.firstOrNull { it.type == WidgetType.Jitsi }
|
||||
val jitsiConfId = jitsiWidget?.let {
|
||||
jitsiService.extractProperties(it)?.confId
|
||||
}
|
||||
copy(activeRoomWidgets = widgets, jitsiConfId = jitsiConfId)
|
||||
copy(
|
||||
activeRoomWidgets = widgets,
|
||||
jitsiState = jitsiState.copy(
|
||||
confId = jitsiConfId,
|
||||
widgetId = jitsiWidget?.widgetId
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -310,6 +318,8 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
is RoomDetailAction.ManageIntegrations -> handleManageIntegrations()
|
||||
is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action)
|
||||
is RoomDetailAction.UpdateJoinJitsiCallStatus -> handleJitsiCallJoinStatus(action)
|
||||
is RoomDetailAction.JoinJitsiCall -> handleJoinJitsiCall()
|
||||
is RoomDetailAction.LeaveJitsiCall -> handleLeaveJitsiCall()
|
||||
is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId)
|
||||
is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action)
|
||||
is RoomDetailAction.CancelSend -> handleCancel(action)
|
||||
|
@ -331,24 +341,34 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun handleJitsiCallJoinStatus(action: RoomDetailAction.UpdateJoinJitsiCallStatus) = withState { state ->
|
||||
if (state.jitsiConfId == null) {
|
||||
if (state.jitsiState.confId == null) {
|
||||
// If jitsi widget is removed while on the call
|
||||
if (state.hasJoinedActiveJitsiConference) {
|
||||
setState { copy(hasJoinedActiveJitsiConference = false) }
|
||||
if (state.jitsiState.hasJoined) {
|
||||
setState { copy(jitsiState = jitsiState.copy(hasJoined = false)) }
|
||||
}
|
||||
return@withState
|
||||
}
|
||||
when (action.broadcastEvent.type) {
|
||||
BroadcastEvent.Type.CONFERENCE_JOINED,
|
||||
BroadcastEvent.Type.CONFERENCE_TERMINATED -> {
|
||||
if (action.broadcastEvent.extractConferenceUrl()?.endsWith(state.jitsiConfId).orFalse()) {
|
||||
setState { copy(hasJoinedActiveJitsiConference = action.broadcastEvent.type == BroadcastEvent.Type.CONFERENCE_JOINED) }
|
||||
if (action.broadcastEvent.extractConferenceUrl()?.endsWith(state.jitsiState.confId).orFalse()) {
|
||||
setState { copy(jitsiState = jitsiState.copy(hasJoined = action.broadcastEvent.type == BroadcastEvent.Type.CONFERENCE_JOINED)) }
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleLeaveJitsiCall() {
|
||||
_viewEvents.post(RoomDetailViewEvents.LeaveJitsiConference)
|
||||
}
|
||||
|
||||
private fun handleJoinJitsiCall() = withState{ state ->
|
||||
val jitsiWidget = state.activeRoomWidgets()?.firstOrNull { it.widgetId == state.jitsiState.widgetId} ?: return@withState
|
||||
val action = RoomDetailAction.EnsureNativeWidgetAllowed(jitsiWidget, false, RoomDetailViewEvents.JoinJitsiConference(jitsiWidget, true))
|
||||
handleCheckWidgetAllowed(action)
|
||||
}
|
||||
|
||||
private fun handleAcceptCall(action: RoomDetailAction.AcceptCall) {
|
||||
callManager.getCallById(action.callId)?.also {
|
||||
_viewEvents.post(RoomDetailViewEvents.DisplayAndAcceptCall(it))
|
||||
|
|
|
@ -55,6 +55,13 @@ sealed class UnreadState {
|
|||
data class HasUnread(val firstUnreadEventId: String) : UnreadState()
|
||||
}
|
||||
|
||||
data class JitsiState(
|
||||
val hasJoined: Boolean = false,
|
||||
// Not null if we have an active jitsi widget on the room
|
||||
val confId: String? = null,
|
||||
val widgetId: String? = null
|
||||
)
|
||||
|
||||
data class RoomDetailViewState(
|
||||
val roomId: String,
|
||||
val eventId: String?,
|
||||
|
@ -76,9 +83,7 @@ data class RoomDetailViewState(
|
|||
val isAllowedToManageWidgets: Boolean = false,
|
||||
val isAllowedToStartWebRTCCall: Boolean = true,
|
||||
val hasFailedSending: Boolean = false,
|
||||
val hasJoinedActiveJitsiConference: Boolean = false,
|
||||
// Not null if we have an active jitsi widget on the room
|
||||
val jitsiConfId: String? = null
|
||||
val jitsiState: JitsiState = JitsiState()
|
||||
) : MvRxState {
|
||||
|
||||
constructor(args: RoomDetailArgs) : this(
|
||||
|
@ -90,7 +95,7 @@ data class RoomDetailViewState(
|
|||
|
||||
fun isWebRTCCallOptionAvailable() = (asyncRoomSummary.invoke()?.joinedMembersCount ?: 0) <= 2
|
||||
|
||||
fun hasActiveJitsiWidget() =jitsiConfId != null
|
||||
fun hasActiveJitsiWidget() = jitsiState.confId != null
|
||||
|
||||
fun isDm() = asyncRoomSummary()?.isDirect == true
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ import im.vector.app.core.extensions.nextOrNull
|
|||
import im.vector.app.core.extensions.prevOrNull
|
||||
import im.vector.app.core.resources.UserPreferencesProvider
|
||||
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||
import im.vector.app.features.home.room.detail.JitsiState
|
||||
import im.vector.app.features.home.room.detail.RoomDetailAction
|
||||
import im.vector.app.features.home.room.detail.RoomDetailViewState
|
||||
import im.vector.app.features.home.room.detail.UnreadState
|
||||
|
@ -51,6 +52,7 @@ import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
|
|||
import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
|
||||
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
|
||||
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem
|
||||
|
@ -87,6 +89,22 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||
private val readReceiptsItemFactory: ReadReceiptsItemFactory
|
||||
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor {
|
||||
|
||||
/**
|
||||
* This is a partial state of the RoomDetailViewState
|
||||
*/
|
||||
data class PartialState(
|
||||
val unreadState: UnreadState = UnreadState.Unknown,
|
||||
val highlightedEventId: String? = null,
|
||||
val jitsiState: JitsiState = JitsiState()
|
||||
) {
|
||||
|
||||
constructor(state: RoomDetailViewState) : this(
|
||||
unreadState = state.unreadState,
|
||||
highlightedEventId = state.highlightedEventId,
|
||||
jitsiState = state.jitsiState
|
||||
)
|
||||
}
|
||||
|
||||
interface Callback :
|
||||
BaseCallback,
|
||||
ReactionPillCallback,
|
||||
|
@ -151,9 +169,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||
private var inSubmitList: Boolean = false
|
||||
private var hasReachedInvite: Boolean = false
|
||||
private var hasUTD: Boolean = false
|
||||
private var unreadState: UnreadState = UnreadState.Unknown
|
||||
private var positionOfReadMarker: Int? = null
|
||||
private var eventIdToHighlight: String? = null
|
||||
private var partialState: PartialState = PartialState()
|
||||
|
||||
var callback: Callback? = null
|
||||
var timeline: Timeline? = null
|
||||
|
@ -171,7 +188,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||
// it's sent by the same user so we are sure we have up to date information.
|
||||
val invalidatedSenderId: String? = currentSnapshot.getOrNull(position)?.senderInfo?.userId
|
||||
val prevDisplayableEventIndex = currentSnapshot.subList(0, position).indexOfLast {
|
||||
timelineEventVisibilityHelper.shouldShowEvent(it, eventIdToHighlight)
|
||||
timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId)
|
||||
}
|
||||
if (prevDisplayableEventIndex != -1 && currentSnapshot[prevDisplayableEventIndex].senderInfo.userId == invalidatedSenderId) {
|
||||
modelCache[prevDisplayableEventIndex] = null
|
||||
|
@ -223,29 +240,22 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||
}
|
||||
|
||||
override fun intercept(models: MutableList<EpoxyModel<*>>) = synchronized(modelCache) {
|
||||
interceptorHelper.intercept(models, unreadState, timeline, callback)
|
||||
interceptorHelper.intercept(models, partialState.unreadState, timeline, callback)
|
||||
}
|
||||
|
||||
fun update(viewState: RoomDetailViewState) {
|
||||
var requestModelBuild = false
|
||||
if (eventIdToHighlight != viewState.highlightedEventId) {
|
||||
fun update(viewState: RoomDetailViewState) = synchronized(modelCache) {
|
||||
val newPartialState = PartialState(viewState)
|
||||
if (partialState.highlightedEventId != newPartialState.highlightedEventId) {
|
||||
// Clear cache to force a refresh
|
||||
synchronized(modelCache) {
|
||||
for (i in 0 until modelCache.size) {
|
||||
if (modelCache[i]?.eventId == viewState.highlightedEventId
|
||||
|| modelCache[i]?.eventId == eventIdToHighlight) {
|
||||
modelCache[i] = null
|
||||
}
|
||||
for (i in 0 until modelCache.size) {
|
||||
if (modelCache[i]?.eventId == viewState.highlightedEventId
|
||||
|| modelCache[i]?.eventId == partialState.highlightedEventId) {
|
||||
modelCache[i] = null
|
||||
}
|
||||
}
|
||||
eventIdToHighlight = viewState.highlightedEventId
|
||||
requestModelBuild = true
|
||||
}
|
||||
if (this.unreadState != viewState.unreadState) {
|
||||
this.unreadState = viewState.unreadState
|
||||
requestModelBuild = true
|
||||
}
|
||||
if (requestModelBuild) {
|
||||
if (newPartialState != partialState) {
|
||||
partialState = newPartialState
|
||||
requestModelBuild()
|
||||
}
|
||||
}
|
||||
|
@ -350,19 +360,19 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||
val nextEvent = currentSnapshot.nextOrNull(position)
|
||||
val prevEvent = currentSnapshot.prevOrNull(position)
|
||||
val nextDisplayableEvent = currentSnapshot.subList(position + 1, currentSnapshot.size).firstOrNull {
|
||||
timelineEventVisibilityHelper.shouldShowEvent(it, eventIdToHighlight)
|
||||
timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId)
|
||||
}
|
||||
val params = TimelineItemFactoryParams(
|
||||
event = event,
|
||||
prevEvent = prevEvent,
|
||||
nextEvent = nextEvent,
|
||||
nextDisplayableEvent = nextDisplayableEvent,
|
||||
highlightedEventId = eventIdToHighlight,
|
||||
partialState = partialState,
|
||||
lastSentEventIdWithoutReadReceipts = lastSentEventWithoutReadReceipts,
|
||||
callback = callback
|
||||
)
|
||||
// Should be build if not cached or if model should be refreshed
|
||||
if (modelCache[position] == null || modelCache[position]?.shouldTriggerBuild == true) {
|
||||
if (modelCache[position] == null || modelCache[position]?.isCacheable == false) {
|
||||
modelCache[position] = buildCacheItem(params)
|
||||
}
|
||||
val itemCachedData = modelCache[position] ?: return@forEach
|
||||
|
@ -381,12 +391,13 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||
it.id(event.localId)
|
||||
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
|
||||
}
|
||||
val shouldTriggerBuild = eventModel is AbsMessageItem && eventModel.attributes.informationData.sendStateDecoration == SendStateDecoration.SENT
|
||||
val isCacheable = eventModel is ItemWithEvents && eventModel.isCacheable()
|
||||
return CacheItemData(
|
||||
localId = event.localId,
|
||||
eventId = event.root.eventId,
|
||||
eventModel = eventModel,
|
||||
shouldTriggerBuild = shouldTriggerBuild)
|
||||
isCacheable = isCacheable
|
||||
)
|
||||
}
|
||||
|
||||
private fun CacheItemData.enrichWithModels(event: TimelineEvent,
|
||||
|
@ -399,7 +410,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||
items = this@TimelineEventController.currentSnapshot,
|
||||
addDaySeparator = wantsDateSeparator,
|
||||
currentPosition = position,
|
||||
eventIdToHighlight = eventIdToHighlight,
|
||||
eventIdToHighlight = partialState.highlightedEventId,
|
||||
callback = callback
|
||||
) {
|
||||
requestModelBuild()
|
||||
|
@ -428,7 +439,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||
return null
|
||||
}
|
||||
// If the event is not shown, we go to the next one
|
||||
if (!timelineEventVisibilityHelper.shouldShowEvent(event, eventIdToHighlight)) {
|
||||
if (!timelineEventVisibilityHelper.shouldShowEvent(event, partialState.highlightedEventId)) {
|
||||
continue
|
||||
}
|
||||
// If the event is sent by us, we update the holder with the eventId and stop the search
|
||||
|
@ -451,7 +462,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||
val currentReadReceipts = ArrayList(event.readReceipts).filter {
|
||||
it.user.userId != session.myUserId
|
||||
}
|
||||
if (timelineEventVisibilityHelper.shouldShowEvent(event, eventIdToHighlight)) {
|
||||
if (timelineEventVisibilityHelper.shouldShowEvent(event, partialState.highlightedEventId)) {
|
||||
lastShownEventId = event.eventId
|
||||
}
|
||||
if (lastShownEventId == null) {
|
||||
|
@ -533,6 +544,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||
val eventModel: EpoxyModel<*>? = null,
|
||||
val mergedHeaderModel: BasedMergedItem<*>? = null,
|
||||
val formattedDayModel: DaySeparatorItem? = null,
|
||||
val shouldTriggerBuild: Boolean = false
|
||||
val isCacheable: Boolean = true
|
||||
)
|
||||
}
|
||||
|
|
|
@ -24,9 +24,13 @@ data class TimelineItemFactoryParams(
|
|||
val prevEvent: TimelineEvent? = null,
|
||||
val nextEvent: TimelineEvent? = null,
|
||||
val nextDisplayableEvent: TimelineEvent? = null,
|
||||
val highlightedEventId: String? = null,
|
||||
val partialState: TimelineEventController.PartialState = TimelineEventController.PartialState(),
|
||||
val lastSentEventIdWithoutReadReceipts: String? = null,
|
||||
val callback: TimelineEventController.Callback? = null
|
||||
) {
|
||||
|
||||
val highlightedEventId: String?
|
||||
get() = partialState.highlightedEventId
|
||||
|
||||
val isHighlighted = highlightedEventId == event.eventId
|
||||
}
|
||||
|
|
|
@ -17,28 +17,29 @@
|
|||
package im.vector.app.features.home.room.detail.timeline.factory
|
||||
|
||||
import im.vector.app.ActiveSessionDataSource
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.item.WidgetTileTimelineItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.WidgetTileTimelineItem_
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder
|
||||
import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem_
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.widgets.model.WidgetContent
|
||||
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import javax.inject.Inject
|
||||
|
||||
class WidgetItemFactory @Inject constructor(
|
||||
private val sp: StringProvider,
|
||||
private val messageItemAttributesFactory: MessageItemAttributesFactory,
|
||||
private val informationDataFactory: MessageInformationDataFactory,
|
||||
private val noticeItemFactory: NoticeItemFactory,
|
||||
private val avatarSizeProvider: AvatarSizeProvider,
|
||||
private val activeSessionDataSource: ActiveSessionDataSource
|
||||
private val messageColorProvider: MessageColorProvider,
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val activeSessionDataSource: ActiveSessionDataSource,
|
||||
private val roomSummariesHolder: RoomSummariesHolder
|
||||
) {
|
||||
private val currentUserId: String?
|
||||
get() = activeSessionDataSource.currentValue?.orNull()?.myUserId
|
||||
|
@ -57,56 +58,41 @@ class WidgetItemFactory @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun createJitsiItem(params: TimelineItemFactoryParams,
|
||||
widgetContent: WidgetContent,
|
||||
previousWidgetContent: WidgetContent?): VectorEpoxyModel<*> {
|
||||
val timelineEvent = params.event
|
||||
private fun createJitsiItem(params: TimelineItemFactoryParams, widgetContent: WidgetContent, prevWidgetContent: WidgetContent?): VectorEpoxyModel<*>? {
|
||||
val informationData = informationDataFactory.create(params)
|
||||
val attributes = messageItemAttributesFactory.create(null, informationData, params.callback)
|
||||
|
||||
val disambiguatedDisplayName = timelineEvent.senderInfo.disambiguatedDisplayName
|
||||
val message = if (widgetContent.isActive()) {
|
||||
val widgetName = widgetContent.getHumanName()
|
||||
if (previousWidgetContent?.isActive().orFalse()) {
|
||||
// Widget has been modified
|
||||
if (timelineEvent.root.isSentByCurrentUser()) {
|
||||
sp.getString(R.string.notice_widget_jitsi_modified_by_you, widgetName)
|
||||
} else {
|
||||
sp.getString(R.string.notice_widget_jitsi_modified, disambiguatedDisplayName, widgetName)
|
||||
}
|
||||
val event = params.event
|
||||
val roomId = event.roomId
|
||||
val userOfInterest = roomSummariesHolder.get(roomId)?.toMatrixItem() ?: return null
|
||||
val isActive = widgetContent.isActive()
|
||||
val callStatus = if (isActive && widgetContent.id == params.partialState.jitsiState.widgetId) {
|
||||
if (params.partialState.jitsiState.hasJoined) {
|
||||
CallTileTimelineItem.CallStatus.IN_CALL
|
||||
} else {
|
||||
// Widget has been added
|
||||
if (timelineEvent.root.isSentByCurrentUser()) {
|
||||
sp.getString(R.string.notice_widget_jitsi_added_by_you, widgetName)
|
||||
} else {
|
||||
sp.getString(R.string.notice_widget_jitsi_added, disambiguatedDisplayName, widgetName)
|
||||
}
|
||||
CallTileTimelineItem.CallStatus.INVITED
|
||||
}
|
||||
} else {
|
||||
// Widget has been removed
|
||||
val widgetName = previousWidgetContent?.getHumanName()
|
||||
if (timelineEvent.root.isSentByCurrentUser()) {
|
||||
sp.getString(R.string.notice_widget_jitsi_removed_by_you, widgetName)
|
||||
} else {
|
||||
sp.getString(R.string.notice_widget_jitsi_removed, disambiguatedDisplayName, widgetName)
|
||||
}
|
||||
CallTileTimelineItem.CallStatus.ENDED
|
||||
}
|
||||
|
||||
return WidgetTileTimelineItem_()
|
||||
.attributes(
|
||||
WidgetTileTimelineItem.Attributes(
|
||||
title = message,
|
||||
drawableStart = R.drawable.ic_video,
|
||||
informationData = informationData,
|
||||
avatarRenderer = attributes.avatarRenderer,
|
||||
messageColorProvider = attributes.messageColorProvider,
|
||||
itemLongClickListener = attributes.itemLongClickListener,
|
||||
itemClickListener = attributes.itemClickListener,
|
||||
reactionPillCallback = attributes.reactionPillCallback,
|
||||
readReceiptsCallback = attributes.readReceiptsCallback,
|
||||
emojiTypeFace = attributes.emojiTypeFace
|
||||
)
|
||||
)
|
||||
val fakeCallId = widgetContent.id ?: prevWidgetContent?.id ?: return null
|
||||
val attributes = CallTileTimelineItem.Attributes(
|
||||
callId = fakeCallId,
|
||||
callKind = CallTileTimelineItem.CallKind.CONFERENCE,
|
||||
callStatus = callStatus,
|
||||
informationData = informationData,
|
||||
avatarRenderer = avatarRenderer,
|
||||
messageColorProvider = messageColorProvider,
|
||||
itemClickListener = null,
|
||||
itemLongClickListener = null,
|
||||
reactionPillCallback = params.callback,
|
||||
readReceiptsCallback = params.callback,
|
||||
userOfInterest = userOfInterest,
|
||||
callback = params.callback,
|
||||
isStillActive = isActive
|
||||
)
|
||||
return CallTileTimelineItem_()
|
||||
.attributes(attributes)
|
||||
.highlighted(params.isHighlighted)
|
||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package im.vector.app.features.home.room.detail.timeline.helper
|
||||
|
||||
import android.telecom.Conference
|
||||
import com.airbnb.epoxy.EpoxyModel
|
||||
import com.airbnb.epoxy.VisibilityState
|
||||
import im.vector.app.core.epoxy.LoadingItem_
|
||||
|
@ -113,11 +114,12 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut
|
|||
callIds: MutableSet<String>,
|
||||
showHiddenEvents: Boolean
|
||||
): Boolean {
|
||||
val callId = epoxyModel.attributes.callId
|
||||
val attributes = epoxyModel.attributes
|
||||
val callId = attributes.callId
|
||||
// We should remove the call tile if we already have one for this call or
|
||||
// if this is an active call tile without an actual call (which can happen with permalink)
|
||||
val shouldRemoveCallItem = callIds.contains(callId)
|
||||
|| (!callManager.getAdvertisedCalls().contains(callId) && epoxyModel.attributes.callStatus.isActive())
|
||||
|| (!callManager.getAdvertisedCalls().contains(callId) && attributes.callStatus.isActive() && attributes.callKind != CallTileTimelineItem.CallKind.CONFERENCE)
|
||||
val removed = shouldRemoveCallItem && !showHiddenEvents
|
||||
if (removed) {
|
||||
remove()
|
||||
|
|
|
@ -42,6 +42,10 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
|
|||
override val baseAttributes: AbsBaseMessageItem.Attributes
|
||||
get() = attributes
|
||||
|
||||
override fun isCacheable(): Boolean {
|
||||
return attributes.informationData.sendStateDecoration != SendStateDecoration.SENT
|
||||
}
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var attributes: Attributes
|
||||
|
||||
|
|
|
@ -37,7 +37,6 @@ import im.vector.app.features.home.room.detail.RoomDetailAction
|
|||
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
import timber.log.Timber
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_state)
|
||||
abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Holder>() {
|
||||
|
@ -45,6 +44,10 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Ho
|
|||
override val baseAttributes: AbsBaseMessageItem.Attributes
|
||||
get() = attributes
|
||||
|
||||
override fun isCacheable(): Boolean {
|
||||
return attributes.callKind != CallKind.CONFERENCE
|
||||
}
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var attributes: Attributes
|
||||
|
||||
|
@ -64,61 +67,108 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Ho
|
|||
} else {
|
||||
holder.callKindView.isVisible = false
|
||||
}
|
||||
if (attributes.callStatus == CallStatus.INVITED && !attributes.informationData.sentByMe && attributes.isStillActive) {
|
||||
holder.acceptRejectViewGroup.isVisible = true
|
||||
holder.acceptView.onClick {
|
||||
attributes.callback?.onTimelineItemAction(RoomDetailAction.AcceptCall(callId = attributes.callId))
|
||||
when (attributes.callStatus) {
|
||||
CallStatus.INVITED -> renderInvitedStatus(holder)
|
||||
CallStatus.IN_CALL -> renderInCallStatus(holder)
|
||||
CallStatus.REJECTED -> renderRejectedStatus(holder)
|
||||
CallStatus.ENDED -> renderEndedStatus(holder)
|
||||
}
|
||||
renderSendState(holder.view, null, holder.failedToSendIndicator)
|
||||
}
|
||||
|
||||
private fun renderEndedStatus(holder: Holder) {
|
||||
holder.acceptRejectViewGroup.isVisible = false
|
||||
holder.statusView.isVisible = true
|
||||
holder.statusView.setText(R.string.call_tile_ended)
|
||||
}
|
||||
|
||||
private fun renderRejectedStatus(holder: Holder) {
|
||||
holder.acceptRejectViewGroup.isVisible = false
|
||||
holder.statusView.isVisible = true
|
||||
if (attributes.informationData.sentByMe) {
|
||||
holder.statusView.setTextWithColoredPart(R.string.call_tile_you_declined, R.string.call_tile_call_back) {
|
||||
val callbackAction = RoomDetailAction.StartCall(attributes.callKind == CallKind.VIDEO)
|
||||
attributes.callback?.onTimelineItemAction(callbackAction)
|
||||
}
|
||||
holder.rejectView.setLeftDrawable(R.drawable.ic_call_hangup, R.attr.colorOnPrimary)
|
||||
holder.rejectView.onClick {
|
||||
attributes.callback?.onTimelineItemAction(RoomDetailAction.EndCall)
|
||||
}
|
||||
holder.statusView.isVisible = false
|
||||
when (attributes.callKind) {
|
||||
CallKind.CONFERENCE -> {
|
||||
holder.rejectView.setText(R.string.ignore)
|
||||
holder.acceptView.setText(R.string.join)
|
||||
holder.acceptView.setLeftDrawable(R.drawable.ic_call_audio_small, R.attr.colorOnPrimary)
|
||||
} else {
|
||||
holder.statusView.text = holder.view.context.getString(R.string.call_tile_other_declined, attributes.userOfInterest.getBestName())
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderInCallStatus(holder: Holder) {
|
||||
holder.acceptRejectViewGroup.isVisible = true
|
||||
holder.acceptView.isVisible = false
|
||||
when {
|
||||
attributes.callKind == CallKind.CONFERENCE -> {
|
||||
holder.statusView.isVisible = false
|
||||
holder.rejectView.isVisible = true
|
||||
holder.rejectView.setText(R.string.leave)
|
||||
holder.rejectView.setLeftDrawable(R.drawable.ic_call_hangup, R.attr.colorOnPrimary)
|
||||
holder.rejectView.onClick {
|
||||
attributes.callback?.onTimelineItemAction(RoomDetailAction.LeaveJitsiCall)
|
||||
}
|
||||
CallKind.AUDIO -> {
|
||||
}
|
||||
attributes.isStillActive -> {
|
||||
holder.statusView.isVisible = false
|
||||
holder.rejectView.isVisible = true
|
||||
holder.rejectView.setText(R.string.call_notification_hangup)
|
||||
holder.rejectView.setLeftDrawable(R.drawable.ic_call_hangup, R.attr.colorOnPrimary)
|
||||
holder.rejectView.onClick {
|
||||
attributes.callback?.onTimelineItemAction(RoomDetailAction.EndCall)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
holder.statusView.isVisible = true
|
||||
holder.statusView.setText(R.string.call_tile_in_call)
|
||||
holder.acceptRejectViewGroup.isVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderInvitedStatus(holder: Holder) {
|
||||
when {
|
||||
attributes.callKind == CallKind.CONFERENCE -> {
|
||||
holder.statusView.isVisible = false
|
||||
holder.acceptRejectViewGroup.isVisible = true
|
||||
holder.acceptView.onClick {
|
||||
attributes.callback?.onTimelineItemAction(RoomDetailAction.JoinJitsiCall)
|
||||
}
|
||||
holder.acceptView.isVisible = true
|
||||
holder.rejectView.isVisible = false
|
||||
holder.acceptView.setText(R.string.join)
|
||||
holder.acceptView.setLeftDrawable(R.drawable.ic_call_video_small, R.attr.colorOnPrimary)
|
||||
}
|
||||
!attributes.informationData.sentByMe && attributes.isStillActive -> {
|
||||
holder.acceptRejectViewGroup.isVisible = true
|
||||
holder.acceptView.isVisible = true
|
||||
holder.rejectView.isVisible = true
|
||||
holder.acceptView.onClick {
|
||||
attributes.callback?.onTimelineItemAction(RoomDetailAction.AcceptCall(callId = attributes.callId))
|
||||
}
|
||||
holder.rejectView.setLeftDrawable(R.drawable.ic_call_hangup, R.attr.colorOnPrimary)
|
||||
holder.rejectView.onClick {
|
||||
attributes.callback?.onTimelineItemAction(RoomDetailAction.EndCall)
|
||||
}
|
||||
holder.statusView.isVisible = false
|
||||
if (attributes.callKind == CallKind.AUDIO) {
|
||||
holder.rejectView.setText(R.string.call_notification_reject)
|
||||
holder.acceptView.setText(R.string.call_notification_answer)
|
||||
holder.acceptView.setLeftDrawable(R.drawable.ic_call_audio_small, R.attr.colorOnPrimary)
|
||||
}
|
||||
CallKind.VIDEO -> {
|
||||
} else if (attributes.callKind == CallKind.VIDEO) {
|
||||
holder.rejectView.setText(R.string.call_notification_reject)
|
||||
holder.acceptView.setText(R.string.call_notification_answer)
|
||||
holder.acceptView.setLeftDrawable(R.drawable.ic_call_video_small, R.attr.colorOnPrimary)
|
||||
}
|
||||
else -> {
|
||||
Timber.w("Shouldn't be in that state")
|
||||
}
|
||||
else -> {
|
||||
holder.acceptRejectViewGroup.isVisible = false
|
||||
holder.statusView.isVisible = true
|
||||
if (attributes.informationData.sentByMe) {
|
||||
holder.statusView.setText(R.string.call_tile_you_started_call)
|
||||
} else {
|
||||
holder.statusView.text = holder.view.context.getString(R.string.call_tile_other_started_call, attributes.userOfInterest.getBestName())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
holder.acceptRejectViewGroup.isVisible = false
|
||||
holder.statusView.isVisible = true
|
||||
}
|
||||
holder.statusView.setCallStatus(attributes)
|
||||
renderSendState(holder.view, null, holder.failedToSendIndicator)
|
||||
}
|
||||
|
||||
private fun TextView.setCallStatus(attributes: Attributes) {
|
||||
when (attributes.callStatus) {
|
||||
CallStatus.INVITED -> if (attributes.informationData.sentByMe) {
|
||||
setText(R.string.call_tile_you_started_call)
|
||||
} else {
|
||||
text = context.getString(R.string.call_tile_other_started_call, attributes.userOfInterest.getBestName())
|
||||
}
|
||||
CallStatus.IN_CALL -> setText(R.string.call_tile_in_call)
|
||||
CallStatus.REJECTED -> if (attributes.informationData.sentByMe) {
|
||||
setTextWithColoredPart(R.string.call_tile_you_declined, R.string.call_tile_call_back) {
|
||||
val callbackAction = RoomDetailAction.StartCall(attributes.callKind == CallKind.VIDEO)
|
||||
attributes.callback?.onTimelineItemAction(callbackAction)
|
||||
}
|
||||
} else {
|
||||
text = context.getString(R.string.call_tile_other_declined, attributes.userOfInterest.getBestName())
|
||||
}
|
||||
CallStatus.ENDED -> setText(R.string.call_tile_ended)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -157,7 +207,7 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Ho
|
|||
enum class CallKind(@DrawableRes val icon: Int, @StringRes val title: Int) {
|
||||
VIDEO(R.drawable.ic_call_video_small, R.string.action_video_call),
|
||||
AUDIO(R.drawable.ic_call_audio_small, R.string.action_voice_call),
|
||||
CONFERENCE(R.drawable.ic_call_conference_small, R.string.conference_call_in_progress),
|
||||
CONFERENCE(R.drawable.ic_call_video_small, R.string.action_video_call),
|
||||
UNKNOWN(0, 0)
|
||||
}
|
||||
|
||||
|
|
|
@ -26,4 +26,9 @@ interface ItemWithEvents {
|
|||
fun canAppendReadMarker(): Boolean = true
|
||||
|
||||
fun isVisible(): Boolean = true
|
||||
|
||||
/**
|
||||
* Returns false if you want epoxy controller to rebuild the event each time a built is triggered
|
||||
*/
|
||||
fun isCacheable(): Boolean = true
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue