diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiBroadcastEventObserver.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiBroadcastEvent.kt similarity index 81% rename from vector/src/main/java/im/vector/app/features/call/conference/JitsiBroadcastEventObserver.kt rename to vector/src/main/java/im/vector/app/features/call/conference/JitsiBroadcastEvent.kt index 57f89c557e..00ad7c540e 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/JitsiBroadcastEventObserver.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiBroadcastEvent.kt @@ -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 { diff --git a/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt b/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt index 85e2b63d6a..a7a6f99cfc 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt @@ -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(), 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() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index 6d13fa05cc..72398d70e2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -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() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index b320bb9291..3e55b2b924 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -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)) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt index 4d1e62da7e..95469def2d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt @@ -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() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index fe40ff09d8..106c753068 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -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)) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt index 9304ddcba3..75650ed322 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt @@ -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 } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index 95d7acb571..6c75bb1e63 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -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>) = 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 ) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt index 0e595ba30e..ea5d21dc10 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt @@ -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 } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt index 1fc57489a5..84867e15c6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt @@ -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) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt index 389dd15413..a8d7407ff7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt @@ -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, 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() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt index fd5eea1b49..b53495fdaf 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -42,6 +42,10 @@ abstract class AbsMessageItem : AbsBaseMessageItem override val baseAttributes: AbsBaseMessageItem.Attributes get() = attributes + override fun isCacheable(): Boolean { + return attributes.informationData.sendStateDecoration != SendStateDecoration.SENT + } + @EpoxyAttribute lateinit var attributes: Attributes diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt index 1f12bdbd2c..c59f2258f8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt @@ -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() { @@ -45,6 +44,10 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem 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