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.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.OnLifecycleEvent
import androidx.localbroadcastmanager.content.LocalBroadcastManager 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.BroadcastEvent
import org.jitsi.meet.sdk.JitsiMeet
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
private const val CONFERENCE_URL_DATA_KEY = "url"
fun BroadcastEvent.extractConferenceUrl(): String? { fun BroadcastEvent.extractConferenceUrl(): String? {
return when (type) { return when (type) {
BroadcastEvent.Type.CONFERENCE_TERMINATED, BroadcastEvent.Type.CONFERENCE_TERMINATED,
BroadcastEvent.Type.CONFERENCE_WILL_JOIN, 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 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, class JitsiBroadcastEventObserver(private val context: Context,
private val onBroadcastEvent: (BroadcastEvent) -> Unit) : LifecycleObserver { 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.MvRx
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.viewModel
import com.facebook.react.bridge.JavaOnlyMap
import com.facebook.react.modules.core.PermissionListener import com.facebook.react.modules.core.PermissionListener
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R 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.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivityJitsiBinding import im.vector.app.databinding.ActivityJitsiBinding
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.jitsi.meet.sdk.BroadcastEmitter
import org.jitsi.meet.sdk.BroadcastEvent import org.jitsi.meet.sdk.BroadcastEvent
import org.jitsi.meet.sdk.JitsiMeet import org.jitsi.meet.sdk.JitsiMeet
import org.jitsi.meet.sdk.JitsiMeetActivityDelegate import org.jitsi.meet.sdk.JitsiMeetActivityDelegate
@ -115,8 +113,7 @@ class VectorJitsiActivity : VectorBaseActivity<ActivityJitsiBinding>(), JitsiMee
jitsiMeetView?.dispose() jitsiMeetView?.dispose()
// Fake emitting CONFERENCE_TERMINATED event when currentConf is not null (probably when closing the PiP screen). // Fake emitting CONFERENCE_TERMINATED event when currentConf is not null (probably when closing the PiP screen).
if (currentConf != null) { if (currentConf != null) {
val broadcastEventData = JavaOnlyMap.of("url", currentConf) JitsiBroadcastEmitter(this).emitConferenceEnded()
BroadcastEmitter(this).sendBroadcast(BroadcastEvent.Type.CONFERENCE_TERMINATED.name, broadcastEventData)
} }
JitsiMeetActivityDelegate.onHostDestroy(this) JitsiMeetActivityDelegate.onHostDestroy(this)
super.onDestroy() 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.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent 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.Widget
import org.matrix.android.sdk.api.session.widgets.model.WidgetContent
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
sealed class RoomDetailAction : VectorViewModelAction { sealed class RoomDetailAction : VectorViewModelAction {
@ -90,6 +91,10 @@ sealed class RoomDetailAction : VectorViewModelAction {
object ManageIntegrations : RoomDetailAction() object ManageIntegrations : RoomDetailAction()
data class AddJitsiWidget(val withVideo: Boolean) : RoomDetailAction() data class AddJitsiWidget(val withVideo: Boolean) : RoomDetailAction()
data class RemoveWidget(val widgetId: String) : RoomDetailAction() data class RemoveWidget(val widgetId: String) : RoomDetailAction()
object JoinJitsiCall: RoomDetailAction()
object LeaveJitsiCall: RoomDetailAction()
data class EnsureNativeWidgetAllowed(val widget: Widget, data class EnsureNativeWidgetAllowed(val widget: Widget,
val userJustAccepted: Boolean, val userJustAccepted: Boolean,
val grantedEvents: RoomDetailViewEvents) : RoomDetailAction() val grantedEvents: RoomDetailViewEvents) : RoomDetailAction()

View file

@ -67,6 +67,7 @@ import com.airbnb.mvrx.Success
import com.airbnb.mvrx.args import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import com.facebook.react.bridge.JavaOnlyMap
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.jakewharton.rxbinding3.view.focusChanges import com.jakewharton.rxbinding3.view.focusChanges
import com.jakewharton.rxbinding3.widget.textChanges 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.attachments.toGroupedContentAttachmentData
import im.vector.app.features.call.SharedKnownCallsViewModel import im.vector.app.features.call.SharedKnownCallsViewModel
import im.vector.app.features.call.VectorCallActivity 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.JitsiBroadcastEventObserver
import im.vector.app.features.call.conference.JitsiCallViewModel import im.vector.app.features.call.conference.JitsiCallViewModel
import im.vector.app.features.call.conference.extractConferenceUrl 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 nl.dionsegijn.konfetti.models.Size
import org.billcarsonfr.jsonviewer.JSonViewerDialog import org.billcarsonfr.jsonviewer.JSonViewerDialog
import org.commonmark.parser.Parser import org.commonmark.parser.Parser
import org.jitsi.meet.sdk.BroadcastEmitter
import org.jitsi.meet.sdk.BroadcastEvent import org.jitsi.meet.sdk.BroadcastEvent
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.content.ContentAttachmentData
@ -394,6 +397,7 @@ class RoomDetailFragment @Inject constructor(
RoomDetailViewEvents.OpenActiveWidgetBottomSheet -> onViewWidgetsClicked() RoomDetailViewEvents.OpenActiveWidgetBottomSheet -> onViewWidgetsClicked()
is RoomDetailViewEvents.ShowInfoOkDialog -> showDialogWithMessage(it.message) is RoomDetailViewEvents.ShowInfoOkDialog -> showDialogWithMessage(it.message)
is RoomDetailViewEvents.JoinJitsiConference -> joinJitsiRoom(it.widget, it.withVideo) is RoomDetailViewEvents.JoinJitsiConference -> joinJitsiRoom(it.widget, it.withVideo)
RoomDetailViewEvents.LeaveJitsiConference -> leaveJitsiConference()
RoomDetailViewEvents.ShowWaitingView -> vectorBaseActivity.showWaitingView() RoomDetailViewEvents.ShowWaitingView -> vectorBaseActivity.showWaitingView()
RoomDetailViewEvents.HideWaitingView -> vectorBaseActivity.hideWaitingView() RoomDetailViewEvents.HideWaitingView -> vectorBaseActivity.hideWaitingView()
is RoomDetailViewEvents.RequestNativeWidgetPermission -> requestNativeWidgetPermission(it) is RoomDetailViewEvents.RequestNativeWidgetPermission -> requestNativeWidgetPermission(it)
@ -416,6 +420,10 @@ class RoomDetailFragment @Inject constructor(
} }
} }
private fun leaveJitsiConference() {
JitsiBroadcastEmitter(vectorBaseActivity).emitConferenceEnded()
}
private fun onBroadcastEvent(event: BroadcastEvent) { private fun onBroadcastEvent(event: BroadcastEvent) {
roomDetailViewModel.handle(RoomDetailAction.UpdateJoinJitsiCallStatus(event)) roomDetailViewModel.handle(RoomDetailAction.UpdateJoinJitsiCallStatus(event))
} }

View file

@ -45,6 +45,7 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
data class NavigateToEvent(val eventId: String) : RoomDetailViewEvents() data class NavigateToEvent(val eventId: String) : RoomDetailViewEvents()
data class JoinJitsiConference(val widget: Widget, val withVideo: Boolean) : RoomDetailViewEvents() data class JoinJitsiConference(val widget: Widget, val withVideo: Boolean) : RoomDetailViewEvents()
object LeaveJitsiConference : RoomDetailViewEvents()
object OpenInvitePeople : RoomDetailViewEvents() object OpenInvitePeople : RoomDetailViewEvents()
object OpenSetRoomAvatarDialog : RoomDetailViewEvents() object OpenSetRoomAvatarDialog : RoomDetailViewEvents()

View file

@ -65,6 +65,7 @@ import kotlinx.coroutines.withContext
import org.commonmark.parser.Parser import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer import org.commonmark.renderer.html.HtmlRenderer
import org.jitsi.meet.sdk.BroadcastEvent 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.MatrixCallback
import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
@ -240,10 +241,17 @@ class RoomDetailViewModel @AssistedInject constructor(
widgets.filter { it.isActive } widgets.filter { it.isActive }
} }
.execute { widgets -> .execute { widgets ->
val jitsiConfId = widgets()?.firstOrNull { it.type == WidgetType.Jitsi }?.let { jitsiWidget -> val jitsiWidget = widgets()?.firstOrNull { it.type == WidgetType.Jitsi }
jitsiService.extractProperties(jitsiWidget)?.confId 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.ManageIntegrations -> handleManageIntegrations()
is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action) is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action)
is RoomDetailAction.UpdateJoinJitsiCallStatus -> handleJitsiCallJoinStatus(action) is RoomDetailAction.UpdateJoinJitsiCallStatus -> handleJitsiCallJoinStatus(action)
is RoomDetailAction.JoinJitsiCall -> handleJoinJitsiCall()
is RoomDetailAction.LeaveJitsiCall -> handleLeaveJitsiCall()
is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId) is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId)
is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action) is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action)
is RoomDetailAction.CancelSend -> handleCancel(action) is RoomDetailAction.CancelSend -> handleCancel(action)
@ -331,24 +341,34 @@ class RoomDetailViewModel @AssistedInject constructor(
} }
private fun handleJitsiCallJoinStatus(action: RoomDetailAction.UpdateJoinJitsiCallStatus) = withState { state -> 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 jitsi widget is removed while on the call
if (state.hasJoinedActiveJitsiConference) { if (state.jitsiState.hasJoined) {
setState { copy(hasJoinedActiveJitsiConference = false) } setState { copy(jitsiState = jitsiState.copy(hasJoined = false)) }
} }
return@withState return@withState
} }
when (action.broadcastEvent.type) { when (action.broadcastEvent.type) {
BroadcastEvent.Type.CONFERENCE_JOINED, BroadcastEvent.Type.CONFERENCE_JOINED,
BroadcastEvent.Type.CONFERENCE_TERMINATED -> { BroadcastEvent.Type.CONFERENCE_TERMINATED -> {
if (action.broadcastEvent.extractConferenceUrl()?.endsWith(state.jitsiConfId).orFalse()) { if (action.broadcastEvent.extractConferenceUrl()?.endsWith(state.jitsiState.confId).orFalse()) {
setState { copy(hasJoinedActiveJitsiConference = action.broadcastEvent.type == BroadcastEvent.Type.CONFERENCE_JOINED) } setState { copy(jitsiState = jitsiState.copy(hasJoined = action.broadcastEvent.type == BroadcastEvent.Type.CONFERENCE_JOINED)) }
} }
} }
else -> Unit 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) { private fun handleAcceptCall(action: RoomDetailAction.AcceptCall) {
callManager.getCallById(action.callId)?.also { callManager.getCallById(action.callId)?.also {
_viewEvents.post(RoomDetailViewEvents.DisplayAndAcceptCall(it)) _viewEvents.post(RoomDetailViewEvents.DisplayAndAcceptCall(it))

View file

@ -55,6 +55,13 @@ sealed class UnreadState {
data class HasUnread(val firstUnreadEventId: String) : 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( data class RoomDetailViewState(
val roomId: String, val roomId: String,
val eventId: String?, val eventId: String?,
@ -76,9 +83,7 @@ data class RoomDetailViewState(
val isAllowedToManageWidgets: Boolean = false, val isAllowedToManageWidgets: Boolean = false,
val isAllowedToStartWebRTCCall: Boolean = true, val isAllowedToStartWebRTCCall: Boolean = true,
val hasFailedSending: Boolean = false, val hasFailedSending: Boolean = false,
val hasJoinedActiveJitsiConference: Boolean = false, val jitsiState: JitsiState = JitsiState()
// Not null if we have an active jitsi widget on the room
val jitsiConfId: String? = null
) : MvRxState { ) : MvRxState {
constructor(args: RoomDetailArgs) : this( constructor(args: RoomDetailArgs) : this(
@ -90,7 +95,7 @@ data class RoomDetailViewState(
fun isWebRTCCallOptionAvailable() = (asyncRoomSummary.invoke()?.joinedMembersCount ?: 0) <= 2 fun isWebRTCCallOptionAvailable() = (asyncRoomSummary.invoke()?.joinedMembersCount ?: 0) <= 2
fun hasActiveJitsiWidget() =jitsiConfId != null fun hasActiveJitsiWidget() = jitsiState.confId != null
fun isDm() = asyncRoomSummary()?.isDirect == true 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.extensions.prevOrNull
import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.core.resources.UserPreferencesProvider
import im.vector.app.features.call.webrtc.WebRtcCallManager 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.RoomDetailAction
import im.vector.app.features.home.room.detail.RoomDetailViewState import im.vector.app.features.home.room.detail.RoomDetailViewState
import im.vector.app.features.home.room.detail.UnreadState 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.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.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.MessageInformationData
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem 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 private val readReceiptsItemFactory: ReadReceiptsItemFactory
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor { ) : 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 : interface Callback :
BaseCallback, BaseCallback,
ReactionPillCallback, ReactionPillCallback,
@ -151,9 +169,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
private var inSubmitList: Boolean = false private var inSubmitList: Boolean = false
private var hasReachedInvite: Boolean = false private var hasReachedInvite: Boolean = false
private var hasUTD: Boolean = false private var hasUTD: Boolean = false
private var unreadState: UnreadState = UnreadState.Unknown
private var positionOfReadMarker: Int? = null private var positionOfReadMarker: Int? = null
private var eventIdToHighlight: String? = null private var partialState: PartialState = PartialState()
var callback: Callback? = null var callback: Callback? = null
var timeline: Timeline? = 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. // 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 invalidatedSenderId: String? = currentSnapshot.getOrNull(position)?.senderInfo?.userId
val prevDisplayableEventIndex = currentSnapshot.subList(0, position).indexOfLast { val prevDisplayableEventIndex = currentSnapshot.subList(0, position).indexOfLast {
timelineEventVisibilityHelper.shouldShowEvent(it, eventIdToHighlight) timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId)
} }
if (prevDisplayableEventIndex != -1 && currentSnapshot[prevDisplayableEventIndex].senderInfo.userId == invalidatedSenderId) { if (prevDisplayableEventIndex != -1 && currentSnapshot[prevDisplayableEventIndex].senderInfo.userId == invalidatedSenderId) {
modelCache[prevDisplayableEventIndex] = null modelCache[prevDisplayableEventIndex] = null
@ -223,29 +240,22 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
} }
override fun intercept(models: MutableList<EpoxyModel<*>>) = synchronized(modelCache) { 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) { fun update(viewState: RoomDetailViewState) = synchronized(modelCache) {
var requestModelBuild = false val newPartialState = PartialState(viewState)
if (eventIdToHighlight != viewState.highlightedEventId) { if (partialState.highlightedEventId != newPartialState.highlightedEventId) {
// Clear cache to force a refresh // Clear cache to force a refresh
synchronized(modelCache) { for (i in 0 until modelCache.size) {
for (i in 0 until modelCache.size) { if (modelCache[i]?.eventId == viewState.highlightedEventId
if (modelCache[i]?.eventId == viewState.highlightedEventId || modelCache[i]?.eventId == partialState.highlightedEventId) {
|| modelCache[i]?.eventId == eventIdToHighlight) { modelCache[i] = null
modelCache[i] = null
}
} }
} }
eventIdToHighlight = viewState.highlightedEventId
requestModelBuild = true
} }
if (this.unreadState != viewState.unreadState) { if (newPartialState != partialState) {
this.unreadState = viewState.unreadState partialState = newPartialState
requestModelBuild = true
}
if (requestModelBuild) {
requestModelBuild() requestModelBuild()
} }
} }
@ -350,19 +360,19 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
val nextEvent = currentSnapshot.nextOrNull(position) val nextEvent = currentSnapshot.nextOrNull(position)
val prevEvent = currentSnapshot.prevOrNull(position) val prevEvent = currentSnapshot.prevOrNull(position)
val nextDisplayableEvent = currentSnapshot.subList(position + 1, currentSnapshot.size).firstOrNull { val nextDisplayableEvent = currentSnapshot.subList(position + 1, currentSnapshot.size).firstOrNull {
timelineEventVisibilityHelper.shouldShowEvent(it, eventIdToHighlight) timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId)
} }
val params = TimelineItemFactoryParams( val params = TimelineItemFactoryParams(
event = event, event = event,
prevEvent = prevEvent, prevEvent = prevEvent,
nextEvent = nextEvent, nextEvent = nextEvent,
nextDisplayableEvent = nextDisplayableEvent, nextDisplayableEvent = nextDisplayableEvent,
highlightedEventId = eventIdToHighlight, partialState = partialState,
lastSentEventIdWithoutReadReceipts = lastSentEventWithoutReadReceipts, lastSentEventIdWithoutReadReceipts = lastSentEventWithoutReadReceipts,
callback = callback callback = callback
) )
// Should be build if not cached or if model should be refreshed // 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) modelCache[position] = buildCacheItem(params)
} }
val itemCachedData = modelCache[position] ?: return@forEach val itemCachedData = modelCache[position] ?: return@forEach
@ -381,12 +391,13 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
it.id(event.localId) it.id(event.localId)
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) 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( return CacheItemData(
localId = event.localId, localId = event.localId,
eventId = event.root.eventId, eventId = event.root.eventId,
eventModel = eventModel, eventModel = eventModel,
shouldTriggerBuild = shouldTriggerBuild) isCacheable = isCacheable
)
} }
private fun CacheItemData.enrichWithModels(event: TimelineEvent, private fun CacheItemData.enrichWithModels(event: TimelineEvent,
@ -399,7 +410,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
items = this@TimelineEventController.currentSnapshot, items = this@TimelineEventController.currentSnapshot,
addDaySeparator = wantsDateSeparator, addDaySeparator = wantsDateSeparator,
currentPosition = position, currentPosition = position,
eventIdToHighlight = eventIdToHighlight, eventIdToHighlight = partialState.highlightedEventId,
callback = callback callback = callback
) { ) {
requestModelBuild() requestModelBuild()
@ -428,7 +439,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
return null return null
} }
// If the event is not shown, we go to the next one // If the event is not shown, we go to the next one
if (!timelineEventVisibilityHelper.shouldShowEvent(event, eventIdToHighlight)) { if (!timelineEventVisibilityHelper.shouldShowEvent(event, partialState.highlightedEventId)) {
continue continue
} }
// If the event is sent by us, we update the holder with the eventId and stop the search // 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 { val currentReadReceipts = ArrayList(event.readReceipts).filter {
it.user.userId != session.myUserId it.user.userId != session.myUserId
} }
if (timelineEventVisibilityHelper.shouldShowEvent(event, eventIdToHighlight)) { if (timelineEventVisibilityHelper.shouldShowEvent(event, partialState.highlightedEventId)) {
lastShownEventId = event.eventId lastShownEventId = event.eventId
} }
if (lastShownEventId == null) { if (lastShownEventId == null) {
@ -533,6 +544,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
val eventModel: EpoxyModel<*>? = null, val eventModel: EpoxyModel<*>? = null,
val mergedHeaderModel: BasedMergedItem<*>? = null, val mergedHeaderModel: BasedMergedItem<*>? = null,
val formattedDayModel: DaySeparatorItem? = 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 prevEvent: TimelineEvent? = null,
val nextEvent: TimelineEvent? = null, val nextEvent: TimelineEvent? = null,
val nextDisplayableEvent: TimelineEvent? = null, val nextDisplayableEvent: TimelineEvent? = null,
val highlightedEventId: String? = null, val partialState: TimelineEventController.PartialState = TimelineEventController.PartialState(),
val lastSentEventIdWithoutReadReceipts: String? = null, val lastSentEventIdWithoutReadReceipts: String? = null,
val callback: TimelineEventController.Callback? = null val callback: TimelineEventController.Callback? = null
) { ) {
val highlightedEventId: String?
get() = partialState.highlightedEventId
val isHighlighted = highlightedEventId == event.eventId val isHighlighted = highlightedEventId == event.eventId
} }

View file

@ -17,28 +17,29 @@
package im.vector.app.features.home.room.detail.timeline.factory package im.vector.app.features.home.room.detail.timeline.factory
import im.vector.app.ActiveSessionDataSource import im.vector.app.ActiveSessionDataSource
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyModel 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.AvatarSizeProvider
import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory 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.helper.RoomSummariesHolder
import im.vector.app.features.home.room.detail.timeline.item.WidgetTileTimelineItem import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem
import im.vector.app.features.home.room.detail.timeline.item.WidgetTileTimelineItem_ import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem_
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.events.model.Event 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.events.model.toModel
import org.matrix.android.sdk.api.session.widgets.model.WidgetContent 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.session.widgets.model.WidgetType
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject import javax.inject.Inject
class WidgetItemFactory @Inject constructor( class WidgetItemFactory @Inject constructor(
private val sp: StringProvider,
private val messageItemAttributesFactory: MessageItemAttributesFactory,
private val informationDataFactory: MessageInformationDataFactory, private val informationDataFactory: MessageInformationDataFactory,
private val noticeItemFactory: NoticeItemFactory, private val noticeItemFactory: NoticeItemFactory,
private val avatarSizeProvider: AvatarSizeProvider, 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? private val currentUserId: String?
get() = activeSessionDataSource.currentValue?.orNull()?.myUserId get() = activeSessionDataSource.currentValue?.orNull()?.myUserId
@ -57,56 +58,41 @@ class WidgetItemFactory @Inject constructor(
} }
} }
private fun createJitsiItem(params: TimelineItemFactoryParams, private fun createJitsiItem(params: TimelineItemFactoryParams, widgetContent: WidgetContent, prevWidgetContent: WidgetContent?): VectorEpoxyModel<*>? {
widgetContent: WidgetContent,
previousWidgetContent: WidgetContent?): VectorEpoxyModel<*> {
val timelineEvent = params.event
val informationData = informationDataFactory.create(params) val informationData = informationDataFactory.create(params)
val attributes = messageItemAttributesFactory.create(null, informationData, params.callback) val event = params.event
val roomId = event.roomId
val disambiguatedDisplayName = timelineEvent.senderInfo.disambiguatedDisplayName val userOfInterest = roomSummariesHolder.get(roomId)?.toMatrixItem() ?: return null
val message = if (widgetContent.isActive()) { val isActive = widgetContent.isActive()
val widgetName = widgetContent.getHumanName() val callStatus = if (isActive && widgetContent.id == params.partialState.jitsiState.widgetId) {
if (previousWidgetContent?.isActive().orFalse()) { if (params.partialState.jitsiState.hasJoined) {
// Widget has been modified CallTileTimelineItem.CallStatus.IN_CALL
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)
}
} else { } else {
// Widget has been added CallTileTimelineItem.CallStatus.INVITED
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)
}
} }
} else { } else {
// Widget has been removed CallTileTimelineItem.CallStatus.ENDED
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)
}
} }
return WidgetTileTimelineItem_() val fakeCallId = widgetContent.id ?: prevWidgetContent?.id ?: return null
.attributes( val attributes = CallTileTimelineItem.Attributes(
WidgetTileTimelineItem.Attributes( callId = fakeCallId,
title = message, callKind = CallTileTimelineItem.CallKind.CONFERENCE,
drawableStart = R.drawable.ic_video, callStatus = callStatus,
informationData = informationData, informationData = informationData,
avatarRenderer = attributes.avatarRenderer, avatarRenderer = avatarRenderer,
messageColorProvider = attributes.messageColorProvider, messageColorProvider = messageColorProvider,
itemLongClickListener = attributes.itemLongClickListener, itemClickListener = null,
itemClickListener = attributes.itemClickListener, itemLongClickListener = null,
reactionPillCallback = attributes.reactionPillCallback, reactionPillCallback = params.callback,
readReceiptsCallback = attributes.readReceiptsCallback, readReceiptsCallback = params.callback,
emojiTypeFace = attributes.emojiTypeFace userOfInterest = userOfInterest,
) callback = params.callback,
) isStillActive = isActive
)
return CallTileTimelineItem_()
.attributes(attributes)
.highlighted(params.isHighlighted)
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
} }
} }

View file

@ -16,6 +16,7 @@
package im.vector.app.features.home.room.detail.timeline.helper package im.vector.app.features.home.room.detail.timeline.helper
import android.telecom.Conference
import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.VisibilityState import com.airbnb.epoxy.VisibilityState
import im.vector.app.core.epoxy.LoadingItem_ import im.vector.app.core.epoxy.LoadingItem_
@ -113,11 +114,12 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut
callIds: MutableSet<String>, callIds: MutableSet<String>,
showHiddenEvents: Boolean showHiddenEvents: Boolean
): 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 // 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) // if this is an active call tile without an actual call (which can happen with permalink)
val shouldRemoveCallItem = callIds.contains(callId) 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 val removed = shouldRemoveCallItem && !showHiddenEvents
if (removed) { if (removed) {
remove() remove()

View file

@ -42,6 +42,10 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
override val baseAttributes: AbsBaseMessageItem.Attributes override val baseAttributes: AbsBaseMessageItem.Attributes
get() = attributes get() = attributes
override fun isCacheable(): Boolean {
return attributes.informationData.sendStateDecoration != SendStateDecoration.SENT
}
@EpoxyAttribute @EpoxyAttribute
lateinit var attributes: Attributes 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.MessageColorProvider
import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
import timber.log.Timber
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_state) @EpoxyModelClass(layout = R.layout.item_timeline_event_base_state)
abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Holder>() { abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Holder>() {
@ -45,6 +44,10 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Ho
override val baseAttributes: AbsBaseMessageItem.Attributes override val baseAttributes: AbsBaseMessageItem.Attributes
get() = attributes get() = attributes
override fun isCacheable(): Boolean {
return attributes.callKind != CallKind.CONFERENCE
}
@EpoxyAttribute @EpoxyAttribute
lateinit var attributes: Attributes lateinit var attributes: Attributes
@ -64,61 +67,108 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Ho
} else { } else {
holder.callKindView.isVisible = false holder.callKindView.isVisible = false
} }
if (attributes.callStatus == CallStatus.INVITED && !attributes.informationData.sentByMe && attributes.isStillActive) { when (attributes.callStatus) {
holder.acceptRejectViewGroup.isVisible = true CallStatus.INVITED -> renderInvitedStatus(holder)
holder.acceptView.onClick { CallStatus.IN_CALL -> renderInCallStatus(holder)
attributes.callback?.onTimelineItemAction(RoomDetailAction.AcceptCall(callId = attributes.callId)) 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) } else {
holder.rejectView.onClick { holder.statusView.text = holder.view.context.getString(R.string.call_tile_other_declined, attributes.userOfInterest.getBestName())
attributes.callback?.onTimelineItemAction(RoomDetailAction.EndCall) }
} }
holder.statusView.isVisible = false
when (attributes.callKind) { private fun renderInCallStatus(holder: Holder) {
CallKind.CONFERENCE -> { holder.acceptRejectViewGroup.isVisible = true
holder.rejectView.setText(R.string.ignore) holder.acceptView.isVisible = false
holder.acceptView.setText(R.string.join) when {
holder.acceptView.setLeftDrawable(R.drawable.ic_call_audio_small, R.attr.colorOnPrimary) 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.rejectView.setText(R.string.call_notification_reject)
holder.acceptView.setText(R.string.call_notification_answer) holder.acceptView.setText(R.string.call_notification_answer)
holder.acceptView.setLeftDrawable(R.drawable.ic_call_audio_small, R.attr.colorOnPrimary) holder.acceptView.setLeftDrawable(R.drawable.ic_call_audio_small, R.attr.colorOnPrimary)
} } else if (attributes.callKind == CallKind.VIDEO) {
CallKind.VIDEO -> {
holder.rejectView.setText(R.string.call_notification_reject) holder.rejectView.setText(R.string.call_notification_reject)
holder.acceptView.setText(R.string.call_notification_answer) holder.acceptView.setText(R.string.call_notification_answer)
holder.acceptView.setLeftDrawable(R.drawable.ic_call_video_small, R.attr.colorOnPrimary) 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) { enum class CallKind(@DrawableRes val icon: Int, @StringRes val title: Int) {
VIDEO(R.drawable.ic_call_video_small, R.string.action_video_call), VIDEO(R.drawable.ic_call_video_small, R.string.action_video_call),
AUDIO(R.drawable.ic_call_audio_small, R.string.action_voice_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) UNKNOWN(0, 0)
} }

View file

@ -26,4 +26,9 @@ interface ItemWithEvents {
fun canAppendReadMarker(): Boolean = true fun canAppendReadMarker(): Boolean = true
fun isVisible(): 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
} }