From 24de6c0101a7dde94c10d1150b89c6b65617c985 Mon Sep 17 00:00:00 2001
From: ganfra <francoisg@matrix.org>
Date: Thu, 3 Dec 2020 19:39:01 +0100
Subject: [PATCH] VoIP: add tiles for call events

---
 .../im/vector/app/core/extensions/TextView.kt |  16 ++
 .../timeline/TimelineEventController.kt       |  19 ++-
 .../timeline/factory/CallItemFactory.kt       | 151 +++++++++++++++++
 .../timeline/factory/TimelineItemFactory.kt   |  19 ++-
 .../helper/TimelineDisplayableEvents.kt       |   1 +
 .../timeline/item/CallTileTimelineItem.kt     | 157 ++++++++++++++++++
 .../main/res/drawable/ic_call_audio_small.xml |   9 +
 .../res/drawable/ic_call_conference_small.xml |  14 ++
 .../main/res/drawable/ic_call_video_small.xml |  12 ++
 .../layout/item_timeline_event_base_state.xml |   8 +-
 .../item_timeline_event_call_tile_stub.xml    |  91 ++++++++++
 vector/src/main/res/values/strings.xml        |   7 +
 vector/src/main/res/values/styles_riot.xml    |   2 +
 13 files changed, 494 insertions(+), 12 deletions(-)
 create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt
 create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt
 create mode 100644 vector/src/main/res/drawable/ic_call_audio_small.xml
 create mode 100644 vector/src/main/res/drawable/ic_call_conference_small.xml
 create mode 100644 vector/src/main/res/drawable/ic_call_video_small.xml
 create mode 100644 vector/src/main/res/layout/item_timeline_event_call_tile_stub.xml

diff --git a/vector/src/main/java/im/vector/app/core/extensions/TextView.kt b/vector/src/main/java/im/vector/app/core/extensions/TextView.kt
index 44b85df93a..28524f6a91 100644
--- a/vector/src/main/java/im/vector/app/core/extensions/TextView.kt
+++ b/vector/src/main/java/im/vector/app/core/extensions/TextView.kt
@@ -22,7 +22,11 @@ import android.text.style.ForegroundColorSpan
 import android.text.style.UnderlineSpan
 import android.widget.TextView
 import androidx.annotation.AttrRes
+import androidx.annotation.ColorRes
+import androidx.annotation.DrawableRes
 import androidx.annotation.StringRes
+import androidx.core.content.ContextCompat
+import androidx.core.graphics.drawable.DrawableCompat
 import androidx.core.view.isVisible
 import com.google.android.material.snackbar.Snackbar
 import im.vector.app.R
@@ -71,6 +75,18 @@ fun TextView.setTextWithColoredPart(@StringRes fullTextRes: Int,
             }
 }
 
+fun TextView.setLeftDrawable(@DrawableRes iconRes: Int, @ColorRes tintColor: Int? = null) {
+    val icon = if(tintColor != null){
+        val tint = ContextCompat.getColor(context, tintColor)
+        ContextCompat.getDrawable(context, iconRes)?.also {
+            DrawableCompat.setTint(it, tint)
+        }
+    }else {
+        ContextCompat.getDrawable(context, iconRes)
+    }
+    setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null)
+}
+
 /**
  * Set long click listener to copy the current text of the TextView to the clipboard and show a Snackbar
  */
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 bddc7fa126..20fbe52731 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
@@ -43,6 +43,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisi
 import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
 import im.vector.app.features.home.room.detail.timeline.item.BaseEventItem
 import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem
+import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem
 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.MessageInformationData
@@ -184,10 +185,22 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
     override fun intercept(models: MutableList<EpoxyModel<*>>) = synchronized(modelCache) {
         positionOfReadMarker = null
         adapterPositionMapping.clear()
-        models.forEachIndexed { index, epoxyModel ->
+        val callIds = mutableSetOf<String>()
+        val modelsIterator = models.listIterator()
+        modelsIterator.withIndex().forEach {
+            val index = it.index
+            val epoxyModel = it.value
+            if (epoxyModel is CallTileTimelineItem) {
+                val callId = epoxyModel.attributes.callId
+                if (callIds.contains(callId)) {
+                    modelsIterator.remove()
+                    return@forEach
+                }
+                callIds.add(callId)
+            }
             if (epoxyModel is BaseEventItem) {
-                epoxyModel.getEventIds().forEach {
-                    adapterPositionMapping[it] = index
+                epoxyModel.getEventIds().forEach { eventId ->
+                    adapterPositionMapping[eventId] = index
                 }
             }
         }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt
new file mode 100644
index 0000000000..36acf5d766
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2019 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package im.vector.app.features.home.room.detail.timeline.factory
+
+import im.vector.app.core.epoxy.VectorEpoxyModel
+import im.vector.app.features.call.webrtc.WebRtcCallManager
+import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
+import im.vector.app.features.home.room.detail.timeline.TimelineEventController
+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.helper.RoomSummaryHolder
+import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem
+import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem_
+import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
+import org.matrix.android.sdk.api.extensions.orFalse
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent
+import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
+import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
+import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent
+import org.matrix.android.sdk.api.session.room.model.call.CallSignallingContent
+import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+import org.matrix.android.sdk.api.util.toMatrixItem
+import javax.inject.Inject
+
+class CallItemFactory @Inject constructor(
+        private val messageColorProvider: MessageColorProvider,
+        private val messageInformationDataFactory: MessageInformationDataFactory,
+        private val messageItemAttributesFactory: MessageItemAttributesFactory,
+        private val avatarSizeProvider: AvatarSizeProvider,
+        private val roomSummaryHolder: RoomSummaryHolder,
+        private val callManager: WebRtcCallManager
+) {
+
+    fun create(event: TimelineEvent,
+               highlight: Boolean,
+               callback: TimelineEventController.Callback?
+    ): VectorEpoxyModel<*>? {
+        if (event.root.eventId == null) return null
+        val informationData = messageInformationDataFactory.create(event, null)
+        val callSignalingContent = event.getCallSignallingContent() ?: return null
+        val callId = callSignalingContent.callId ?: return null
+        val call = callManager.getCallById(callId)
+        val callKind = if (call?.mxCall?.isVideoCall.orFalse()) {
+            CallTileTimelineItem.CallKind.VIDEO
+        } else {
+            CallTileTimelineItem.CallKind.AUDIO
+        }
+        return when (event.root.getClearType()) {
+            EventType.CALL_ANSWER -> {
+                if (call == null) return null
+                createCallTileTimelineItem(
+                        callId = callId,
+                        callStatus = CallTileTimelineItem.CallStatus.IN_CALL,
+                        callKind = callKind,
+                        callback = callback,
+                        highlight = highlight,
+                        informationData = informationData
+                )
+            }
+            EventType.CALL_INVITE -> {
+                if (call == null) return null
+                createCallTileTimelineItem(
+                        callId = callId,
+                        callStatus = CallTileTimelineItem.CallStatus.INVITED,
+                        callKind = callKind,
+                        callback = callback,
+                        highlight = highlight,
+                        informationData = informationData
+                )
+            }
+            EventType.CALL_REJECT -> {
+                createCallTileTimelineItem(
+                        callId = callId,
+                        callStatus = CallTileTimelineItem.CallStatus.REJECTED,
+                        callKind = callKind,
+                        callback = callback,
+                        highlight = highlight,
+                        informationData = informationData
+                )
+            }
+            EventType.CALL_HANGUP -> {
+                createCallTileTimelineItem(
+                        callId = callId,
+                        callStatus = CallTileTimelineItem.CallStatus.ENDED,
+                        callKind = callKind,
+                        callback = callback,
+                        highlight = highlight,
+                        informationData = informationData
+                )
+            }
+            else                  -> null
+        }
+    }
+
+    private fun TimelineEvent.getCallSignallingContent(): CallSignallingContent? {
+        return when (root.getClearType()) {
+            EventType.CALL_INVITE -> root.getClearContent().toModel<CallInviteContent>()
+            EventType.CALL_HANGUP -> root.getClearContent().toModel<CallHangupContent>()
+            EventType.CALL_REJECT -> root.getClearContent().toModel<CallRejectContent>()
+            EventType.CALL_ANSWER -> root.getClearContent().toModel<CallAnswerContent>()
+            else                  -> null
+        }
+    }
+
+    private fun createCallTileTimelineItem(
+            callId: String,
+            callKind: CallTileTimelineItem.CallKind,
+            callStatus: CallTileTimelineItem.CallStatus,
+            informationData: MessageInformationData,
+            highlight: Boolean,
+            callback: TimelineEventController.Callback?
+    ): CallTileTimelineItem? {
+
+        val userOfInterest = roomSummaryHolder.roomSummary?.toMatrixItem() ?: return null
+        val attributes = messageItemAttributesFactory.create(null, informationData, callback).let {
+            CallTileTimelineItem.Attributes(
+                    callId = callId,
+                    callKind = callKind,
+                    callStatus = callStatus,
+                    informationData = informationData,
+                    avatarRenderer = it.avatarRenderer,
+                    messageColorProvider = messageColorProvider,
+                    itemClickListener = it.itemClickListener,
+                    itemLongClickListener = it.itemLongClickListener,
+                    reactionPillCallback = it.reactionPillCallback,
+                    readReceiptsCallback = it.readReceiptsCallback,
+                    userOfInterest = userOfInterest
+            )
+        }
+        return CallTileTimelineItem_()
+                .attributes(attributes)
+                .highlighted(highlight)
+                .leftGuideline(avatarSizeProvider.leftGuideline)
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
index 243cbbd0e6..4e3e6b84a1 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
@@ -34,6 +34,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
                                               private val roomCreateItemFactory: RoomCreateItemFactory,
                                               private val roomSummaryHolder: RoomSummaryHolder,
                                               private val verificationConclusionItemFactory: VerificationItemFactory,
+                                              private val callItemFactory: CallItemFactory,
                                               private val userPreferencesProvider: UserPreferencesProvider) {
 
     fun create(event: TimelineEvent,
@@ -45,7 +46,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
         val computedModel = try {
             when (event.root.getClearType()) {
                 EventType.STICKER,
-                EventType.MESSAGE               -> messageItemFactory.create(event, nextEvent, highlight, callback)
+                EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, callback)
                 // State and call
                 EventType.STATE_ROOM_TOMBSTONE,
                 EventType.STATE_ROOM_NAME,
@@ -60,17 +61,19 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
                 EventType.STATE_ROOM_GUEST_ACCESS,
                 EventType.STATE_ROOM_WIDGET_LEGACY,
                 EventType.STATE_ROOM_WIDGET,
-                EventType.CALL_INVITE,
-                EventType.CALL_HANGUP,
-                EventType.CALL_ANSWER,
                 EventType.STATE_ROOM_POWER_LEVELS,
                 EventType.REACTION,
-                EventType.REDACTION             -> noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback)
+                EventType.REDACTION -> noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback)
                 EventType.STATE_ROOM_ENCRYPTION -> encryptionItemFactory.create(event, highlight, callback)
                 // State room create
-                EventType.STATE_ROOM_CREATE     -> roomCreateItemFactory.create(event, callback)
+                EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback)
+                // Calls
+                EventType.CALL_INVITE,
+                EventType.CALL_HANGUP,
+                EventType.CALL_REJECT,
+                EventType.CALL_ANSWER -> callItemFactory.create(event, highlight, callback)
                 // Crypto
-                EventType.ENCRYPTED             -> {
+                EventType.ENCRYPTED -> {
                     if (event.root.isRedacted()) {
                         // Redacted event, let the MessageItemFactory handle it
                         messageItemFactory.create(event, nextEvent, highlight, callback)
@@ -84,7 +87,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
                 EventType.KEY_VERIFICATION_KEY,
                 EventType.KEY_VERIFICATION_READY,
                 EventType.KEY_VERIFICATION_MAC,
-                EventType.CALL_CANDIDATES       -> {
+                EventType.CALL_CANDIDATES -> {
                     // TODO These are not filtered out by timeline when encrypted
                     // For now manually ignore
                     if (userPreferencesProvider.shouldShowHiddenEvents()) {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt
index 4fcac6c7f7..eb5b8081f9 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt
@@ -38,6 +38,7 @@ object TimelineDisplayableEvents {
             EventType.CALL_INVITE,
             EventType.CALL_HANGUP,
             EventType.CALL_ANSWER,
+            EventType.CALL_REJECT,
             EventType.ENCRYPTED,
             EventType.STATE_ROOM_ENCRYPTION,
             EventType.STATE_ROOM_GUEST_ACCESS,
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
new file mode 100644
index 0000000000..85f093bfec
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2019 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package im.vector.app.features.home.room.detail.timeline.item
+
+import android.content.Context
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.ImageView
+import android.widget.RelativeLayout
+import android.widget.TextView
+import androidx.annotation.ColorRes
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import androidx.core.content.ContextCompat
+import androidx.core.graphics.drawable.DrawableCompat
+import androidx.core.view.isVisible
+import androidx.core.view.updateLayoutParams
+import com.airbnb.epoxy.EpoxyAttribute
+import com.airbnb.epoxy.EpoxyModelClass
+import im.vector.app.R
+import im.vector.app.core.extensions.setLeftDrawable
+import im.vector.app.core.extensions.setTextOrHide
+import im.vector.app.core.extensions.setTextWithColoredPart
+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.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>() {
+
+    override val baseAttributes: AbsBaseMessageItem.Attributes
+        get() = attributes
+
+    @EpoxyAttribute
+    lateinit var attributes: Attributes
+
+    override fun getViewType() = STUB_ID
+
+    override fun bind(holder: Holder) {
+        super.bind(holder)
+        holder.endGuideline.updateLayoutParams<RelativeLayout.LayoutParams> {
+            this.marginEnd = leftGuideline
+        }
+
+        holder.creatorNameView.text = attributes.userOfInterest.getBestName()
+        attributes.avatarRenderer.render(attributes.userOfInterest, holder.creatorAvatarView)
+        holder.callKindView.setText(attributes.callKind.title)
+        holder.callKindView.setLeftDrawable(attributes.callKind.icon)
+        if (attributes.callStatus == CallStatus.INVITED && !attributes.informationData.sentByMe) {
+            holder.acceptRejectViewGroup.isVisible = true
+            holder.acceptView.setOnClickListener {
+                Timber.v("On accept call: $attributes.callId ")
+            }
+            holder.rejectView.setLeftDrawable(R.drawable.ic_call_hangup, R.color.riotx_notice)
+            holder.rejectView.setOnClickListener {
+                Timber.v("On reject call: $attributes.callId")
+            }
+            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.color.riotx_accent)
+                }
+                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.color.riotx_accent)
+                }
+                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.color.riotx_accent)
+                }
+            }
+        } 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)
+            }
+            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)
+            } else {
+                text = context.getString(R.string.call_tile_other_declined, attributes.userOfInterest.getBestName())
+            }
+            CallStatus.ENDED -> setText(R.string.call_tile_ended)
+        }
+    }
+
+    class Holder : AbsBaseMessageItem.Holder(STUB_ID) {
+        val acceptView by bind<Button>(R.id.itemCallAcceptView)
+        val rejectView by bind<Button>(R.id.itemCallRejectView)
+        val acceptRejectViewGroup by bind<ViewGroup>(R.id.itemCallAcceptRejectViewGroup)
+        val callKindView by bind<TextView>(R.id.itemCallKindTextView)
+        val creatorAvatarView by bind<ImageView>(R.id.itemCallCreatorAvatar)
+        val creatorNameView by bind<TextView>(R.id.itemCallCreatorNameTextView)
+        val statusView by bind<TextView>(R.id.itemCallStatusTextView)
+        val endGuideline by bind<View>(R.id.messageEndGuideline)
+        val failedToSendIndicator by bind<ImageView>(R.id.messageFailToSendIndicator)
+    }
+
+    companion object {
+        private const val STUB_ID = R.id.messageCallStub
+    }
+
+    data class Attributes(
+            val callId: String,
+            val callKind: CallKind,
+            val callStatus: CallStatus,
+            val userOfInterest: MatrixItem,
+            override val informationData: MessageInformationData,
+            override val avatarRenderer: AvatarRenderer,
+            override val messageColorProvider: MessageColorProvider,
+            override val itemLongClickListener: View.OnLongClickListener? = null,
+            override val itemClickListener: View.OnClickListener? = null,
+            override val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null,
+            override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
+    ) : AbsBaseMessageItem.Attributes
+
+    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)
+    }
+
+    enum class CallStatus {
+        INVITED,
+        IN_CALL,
+        REJECTED,
+        ENDED
+    }
+}
diff --git a/vector/src/main/res/drawable/ic_call_audio_small.xml b/vector/src/main/res/drawable/ic_call_audio_small.xml
new file mode 100644
index 0000000000..9a407cdf7e
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_call_audio_small.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="14dp"
+    android:height="14dp"
+    android:viewportWidth="14"
+    android:viewportHeight="14">
+  <path
+      android:pathData="M4.3514,9.6408C5.1121,10.4621 6.9433,11.8842 7.4424,12.176C7.4719,12.1933 7.5057,12.2134 7.5435,12.2358C8.3051,12.6886 10.6916,14.1072 12.4304,12.7796C13.7775,11.751 13.3395,10.5939 12.886,10.25C12.5756,10.0085 11.661,9.3429 10.8005,8.7434C9.9555,8.1548 9.4846,8.6264 9.1662,8.9453C9.1603,8.9512 9.1545,8.957 9.1488,8.9627L8.5082,9.6034C8.345,9.7665 8.0968,9.707 7.8591,9.5203C7.0062,8.8707 6.3788,8.2439 6.0649,7.93L6.0623,7.9273C5.7484,7.6135 5.1293,6.9938 4.4798,6.1409C4.2931,5.9032 4.2335,5.655 4.3967,5.4919L5.0373,4.8512C5.0431,4.8455 5.0489,4.8397 5.0547,4.8338C5.3736,4.5154 5.8453,4.0445 5.2566,3.1995C4.6571,2.339 3.9915,1.4244 3.7501,1.114C3.4061,0.6606 2.249,0.2226 1.2205,1.5697C-0.1072,3.3084 1.3115,5.6949 1.7642,6.4565C1.7867,6.4943 1.8068,6.5281 1.824,6.5576C2.1159,7.0567 3.5301,8.8801 4.3514,9.6408Z"
+      android:fillColor="#737D8C"/>
+</vector>
diff --git a/vector/src/main/res/drawable/ic_call_conference_small.xml b/vector/src/main/res/drawable/ic_call_conference_small.xml
new file mode 100644
index 0000000000..1ba596d4a9
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_call_conference_small.xml
@@ -0,0 +1,14 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="16dp"
+    android:height="16dp"
+    android:viewportWidth="16"
+    android:viewportHeight="16">
+  <path
+      android:pathData="M8,16C12.4183,16 16,12.4183 16,8C16,3.5817 12.4183,0 8,0C3.5817,0 0,3.5817 0,8C0,12.4183 3.5817,16 8,16ZM5.3333,8.3333C6.4379,8.3333 7.3333,7.3633 7.3333,6.1667C7.3333,4.97 6.4379,4 5.3333,4C4.2288,4 3.3333,4.97 3.3333,6.1667C3.3333,7.3633 4.2288,8.3333 5.3333,8.3333ZM11.5043,9.1296C12.472,9.1296 13.2564,8.2798 13.2564,7.2315C13.2564,6.1832 12.472,5.3333 11.5043,5.3333C10.5366,5.3333 9.7522,6.1832 9.7522,7.2315C9.7522,8.2798 10.5366,9.1296 11.5043,9.1296ZM6.1045,9.4089C7.5698,9.7298 8.6666,11.0353 8.6666,12.5969L8.6666,14.7587H4.6666L1.7144,11.6667C2.3548,10.2875 3.7345,9.3333 5.3333,9.3333C5.5971,9.3333 5.855,9.3593 6.1045,9.4089ZM9.5501,10.611C9.8385,11.2121 10,11.8856 10,12.5969L10,14.7587H11.5043L14.4675,11.6667C13.8465,10.6685 12.7515,10.0057 11.5043,10.0057C10.7807,10.0057 10.1084,10.2288 9.5501,10.611Z"
+      android:fillColor="#737D8C"
+      android:fillType="evenOdd"/>
+  <path
+      android:pathData="M8,14.6667C11.6819,14.6667 14.6667,11.6819 14.6667,8C14.6667,4.3181 11.6819,1.3333 8,1.3333C4.3181,1.3333 1.3333,4.3181 1.3333,8C1.3333,11.6819 4.3181,14.6667 8,14.6667ZM8,16C12.4183,16 16,12.4183 16,8C16,3.5817 12.4183,0 8,0C3.5817,0 0,3.5817 0,8C0,12.4183 3.5817,16 8,16Z"
+      android:fillColor="#737D8C"
+      android:fillType="evenOdd"/>
+</vector>
diff --git a/vector/src/main/res/drawable/ic_call_video_small.xml b/vector/src/main/res/drawable/ic_call_video_small.xml
new file mode 100644
index 0000000000..abb2d85719
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_call_video_small.xml
@@ -0,0 +1,12 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="16dp"
+    android:height="12dp"
+    android:viewportWidth="16"
+    android:viewportHeight="12">
+  <path
+      android:pathData="M0,3.6666C0,2.0098 1.3432,0.6666 3,0.6666H8.3333C9.9902,0.6666 11.3333,2.0098 11.3333,3.6666V8.3333C11.3333,9.9901 9.9902,11.3333 8.3333,11.3333H3C1.3431,11.3333 0,9.9902 0,8.3333V3.6666Z"
+      android:fillColor="#737D8C"/>
+  <path
+      android:pathData="M12.6666,3.9999L14.3753,2.633C15.03,2.1092 16,2.5754 16,3.4139V8.586C16,9.4245 15.03,9.8906 14.3753,9.3668L12.6666,7.9999V3.9999Z"
+      android:fillColor="#737D8C"/>
+</vector>
diff --git a/vector/src/main/res/layout/item_timeline_event_base_state.xml b/vector/src/main/res/layout/item_timeline_event_base_state.xml
index a6b21aed15..1262c16360 100644
--- a/vector/src/main/res/layout/item_timeline_event_base_state.xml
+++ b/vector/src/main/res/layout/item_timeline_event_base_state.xml
@@ -40,6 +40,12 @@
         android:background="@drawable/rounded_rect_shape_8"
         android:padding="8dp">
 
+        <ViewStub
+            android:id="@+id/messageCallStub"
+            style="@style/TimelineContentStubBaseParams"
+            android:layout="@layout/item_timeline_event_call_tile_stub"
+            tools:visibility="visible" />
+
         <ViewStub
             android:id="@+id/messageVerificationRequestStub"
             style="@style/TimelineContentStubBaseParams"
@@ -50,7 +56,7 @@
             android:id="@+id/messageVerificationDoneStub"
             style="@style/TimelineContentStubBaseParams"
             android:layout="@layout/item_timeline_event_status_tile_stub"
-            tools:visibility="visible" />
+            tools:visibility="gone" />
 
     </FrameLayout>
 
diff --git a/vector/src/main/res/layout/item_timeline_event_call_tile_stub.xml b/vector/src/main/res/layout/item_timeline_event_call_tile_stub.xml
new file mode 100644
index 0000000000..9ee40738d0
--- /dev/null
+++ b/vector/src/main/res/layout/item_timeline_event_call_tile_stub.xml
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:orientation="vertical">
+
+    <ImageView
+        android:id="@+id/itemCallCreatorAvatar"
+        android:layout_width="40dp"
+        android:layout_height="40dp"
+        android:layout_gravity="center_horizontal" />
+
+    <TextView
+        android:id="@+id/itemCallCreatorNameTextView"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal"
+        android:drawablePadding="6dp"
+        android:layout_marginTop="4dp"
+        android:gravity="center"
+        android:textColor="?riotx_text_primary"
+        android:textSize="15sp"
+        android:textStyle="bold"
+        tools:text="@sample/matrix.json/data/displayName" />
+
+
+    <TextView
+        android:id="@+id/itemCallKindTextView"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:layout_marginStart="8dp"
+        android:layout_marginEnd="8dp"
+        android:layout_marginTop="4dp"
+        android:drawablePadding="4dp"
+        android:layout_marginBottom="12dp"
+        android:gravity="center"
+        android:textColor="?riotx_text_primary"
+        android:textSize="12sp"
+        tools:text="@string/action_video_call" />
+
+    <TextView
+        android:id="@+id/itemCallStatusTextView"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:layout_marginStart="8dp"
+        android:layout_marginTop="12dp"
+        android:layout_marginEnd="8dp"
+        android:layout_marginBottom="12dp"
+        android:textColor="?attr/vctr_notice_secondary"
+        android:textSize="13sp"
+        tools:text="@string/video_call_in_progress" />
+
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/itemCallAcceptRejectViewGroup"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+
+        <Button
+            android:id="@+id/itemCallAcceptView"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="4dp"
+            android:minWidth="120dp"
+            style="@style/VectorButtonStylePositive"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintHorizontal_chainStyle="packed"
+            app:layout_constraintStart_toEndOf="@+id/itemCallRejectView"
+            app:layout_constraintTop_toTopOf="@id/itemCallRejectView" />
+
+        <Button
+            android:id="@+id/itemCallRejectView"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="16dp"
+            android:layout_marginEnd="4dp"
+            android:minWidth="120dp"
+            style="@style/VectorButtonStyleDestructive"
+            app:layout_constraintEnd_toStartOf="@+id/itemCallAcceptView"
+            app:layout_constraintHorizontal_chainStyle="packed"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintStart_toStartOf="parent" />
+
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
index f7b897cd3e..3a4c1602e8 100644
--- a/vector/src/main/res/values/strings.xml
+++ b/vector/src/main/res/values/strings.xml
@@ -2747,4 +2747,11 @@
     <string name="warning_unsaved_change_discard">Discard changes</string>
 
     <string name="matrix_to_card_title">Matrix Link</string>
+
+    <string name="call_tile_you_started_call">You started a call</string>
+    <string name="call_tile_in_call">You\'re currently in this call</string>
+    <string name="call_tile_you_declined">You declined this call %1$s</string>
+    <string name="call_tile_other_declined">%1$s declined this call</string>
+    <string name="call_tile_ended">This call has ended</string>
+    <string name="call_tile_call_back">Call back</string>
 </resources>
diff --git a/vector/src/main/res/values/styles_riot.xml b/vector/src/main/res/values/styles_riot.xml
index 2dff0ab39c..d81b4cb6e9 100644
--- a/vector/src/main/res/values/styles_riot.xml
+++ b/vector/src/main/res/values/styles_riot.xml
@@ -135,11 +135,13 @@
         <item name="android:textSize">14sp</item>
         <item name="android:textAllCaps">false</item>
         <item name="android:textColor">@color/button_destructive_text_color_selector</item>
+        <item name="drawableTint">@color/riotx_notice</item>
     </style>
 
     <style name="VectorButtonStylePositive" parent="VectorButtonStyleDestructive">
         <item name="backgroundTint">@color/button_positive_background_selector</item>
         <item name="android:textColor">@color/button_positive_text_color_selector</item>
+        <item name="drawableTint">@color/riotx_accent</item>
     </style>
 
     <style name="VectorButtonStyleInlineBot" parent="VectorButtonStyleDestructive">