Jitsi call: start using call tiles

This commit is contained in:
ganfra 2021-07-06 19:58:36 +02:00
parent cdf97fc29f
commit b7e5a6cf28
14 changed files with 259 additions and 148 deletions

View File

@ -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 {

View File

@ -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()

View File

@ -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()

View File

@ -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))
}

View File

@ -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()

View File

@ -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))

View File

@ -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
}

View File

@ -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
)
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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()

View File

@ -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

View File

@ -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)
}

View File

@ -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
}