diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index e6714005a1..69b4d57e28 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -3078,6 +3078,11 @@
%1$s (%2$s)
(%1$s)
+ Live
+ Resume voice broadcast record
+ Pause voice broadcast record
+ Stop voice broadcast record
+
Anyone in %s will be able to find and join this room - no need to manually invite everyone. You’ll be able to change this in room settings anytime.
Anyone in a parent space will be able to find and join this room - no need to manually invite everyone. You’ll be able to change this in room settings anytime.
diff --git a/library/ui-styles/src/main/res/values/dimens.xml b/library/ui-styles/src/main/res/values/dimens.xml
index 52d16eae7d..50d5aaf014 100644
--- a/library/ui-styles/src/main/res/values/dimens.xml
+++ b/library/ui-styles/src/main/res/values/dimens.xml
@@ -73,6 +73,9 @@
12dp
22dp
+
+ 48dp
+
112dp
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
index cb947a67ce..245d92f95b 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
@@ -201,7 +201,7 @@ class MessageItemFactory @Inject constructor(
is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes)
is MessageLocationContent -> buildLocationItem(messageContent, informationData, highlight, attributes)
is MessageBeaconInfoContent -> liveLocationShareMessageItemFactory.create(params.event, highlight, attributes)
- is MessageVoiceBroadcastInfoContent -> voiceBroadcastItemFactory.create(messageContent, params.eventsGroup, highlight, callback, attributes)
+ is MessageVoiceBroadcastInfoContent -> voiceBroadcastItemFactory.create(params, messageContent, highlight, callback, attributes)
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
}
return messageItem?.apply {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt
index f2dfb020a1..1064d2bbc5 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt
@@ -15,46 +15,66 @@
*/
package im.vector.app.features.home.room.detail.timeline.factory
+import im.vector.app.core.resources.ColorProvider
+import im.vector.app.core.resources.DrawableProvider
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
-import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
-import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventsGroup
import im.vector.app.features.home.room.detail.timeline.helper.VoiceBroadcastEventsGroup
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
-import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastItem
-import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastItem_
+import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastRecordingItem
+import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastRecordingItem_
+import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.api.session.getRoom
+import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
class VoiceBroadcastItemFactory @Inject constructor(
private val session: Session,
private val avatarSizeProvider: AvatarSizeProvider,
- private val audioMessagePlaybackTracker: AudioMessagePlaybackTracker,
+ private val colorProvider: ColorProvider,
+ private val drawableProvider: DrawableProvider,
+ private val voiceBroadcastRecorder: VoiceBroadcastRecorder?,
) {
fun create(
+ params: TimelineItemFactoryParams,
messageContent: MessageVoiceBroadcastInfoContent,
- eventsGroup: TimelineEventsGroup?,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes,
- ): MessageVoiceBroadcastItem? {
+ ): MessageVoiceBroadcastRecordingItem? {
// Only display item of the initial event with updated data
if (messageContent.voiceBroadcastState != VoiceBroadcastState.STARTED) return null
- val voiceBroadcastEventsGroup = eventsGroup?.let { VoiceBroadcastEventsGroup(it) } ?: return null
+ val voiceBroadcastEventsGroup = params.eventsGroup?.let { VoiceBroadcastEventsGroup(it) } ?: return null
val mostRecentTimelineEvent = voiceBroadcastEventsGroup.getLastDisplayableEvent()
val mostRecentEvent = mostRecentTimelineEvent.root.asVoiceBroadcastEvent()
val mostRecentMessageContent = mostRecentEvent?.content ?: return null
val isRecording = mostRecentMessageContent.voiceBroadcastState != VoiceBroadcastState.STOPPED && mostRecentEvent.root.stateKey == session.myUserId
- return MessageVoiceBroadcastItem_()
+ return if (isRecording) {
+ createRecordingItem(params.event.roomId, highlight, callback, attributes)
+ } else {
+ createRecordingItem(params.event.roomId, highlight, callback, attributes)
+ }
+ }
+
+ private fun createRecordingItem(
+ roomId: String,
+ highlight: Boolean,
+ callback: TimelineEventController.Callback?,
+ attributes: AbsMessageItem.Attributes,
+ ): MessageVoiceBroadcastRecordingItem? {
+ val roomSummary = session.getRoom(roomId)?.roomSummary()
+ return MessageVoiceBroadcastRecordingItem_()
.attributes(attributes)
.highlighted(highlight)
- .voiceBroadcastState(mostRecentMessageContent.voiceBroadcastState)
- .recording(isRecording)
- .audioMessagePlaybackTracker(audioMessagePlaybackTracker)
+ .roomItem(roomSummary?.toMatrixItem())
+ .colorProvider(colorProvider)
+ .drawableProvider(drawableProvider)
+ .voiceBroadcastRecorder(voiceBroadcastRecorder)
.leftGuideline(avatarSizeProvider.leftGuideline)
.callback(callback)
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastItem.kt
deleted file mode 100644
index 1927024a36..0000000000
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastItem.kt
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Copyright (c) 2022 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.annotation.SuppressLint
-import android.widget.ImageButton
-import android.widget.TextView
-import com.airbnb.epoxy.EpoxyAttribute
-import com.airbnb.epoxy.EpoxyModelClass
-import im.vector.app.R
-import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction
-import im.vector.app.features.home.room.detail.timeline.TimelineEventController
-import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
-import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker.Listener.State
-import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
-
-@EpoxyModelClass
-abstract class MessageVoiceBroadcastItem : AbsMessageItem() {
-
- @EpoxyAttribute
- var callback: TimelineEventController.Callback? = null
-
- @EpoxyAttribute
- var voiceBroadcastState: VoiceBroadcastState? = null
-
- @EpoxyAttribute
- var recording: Boolean = false
-
- @EpoxyAttribute
- lateinit var audioMessagePlaybackTracker: AudioMessagePlaybackTracker
-
- private val voiceBroadcastEventId
- get() = attributes.informationData.eventId
-
- override fun isCacheable(): Boolean = false
-
- override fun bind(holder: Holder) {
- super.bind(holder)
- bindVoiceBroadcastItem(holder)
- }
-
- @SuppressLint("SetTextI18n") // Temporary text
- private fun bindVoiceBroadcastItem(holder: Holder) {
- holder.currentStateText.text = "Voice Broadcast state: ${voiceBroadcastState?.value ?: "None"}"
- if (recording) {
- renderRecording(holder)
- } else {
- renderListening(holder)
- }
- }
-
- private fun renderListening(holder: Holder) {
- audioMessagePlaybackTracker.track(attributes.informationData.eventId, object : AudioMessagePlaybackTracker.Listener {
- override fun onUpdate(state: State) {
- holder.playButton.isEnabled = state !is State.Playing
- holder.pauseButton.isEnabled = state is State.Playing
- holder.stopButton.isEnabled = state !is State.Idle
- }
- })
- holder.playButton.setOnClickListener { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastEventId)) }
- holder.pauseButton.setOnClickListener { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause) }
- holder.stopButton.setOnClickListener { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Stop) }
- }
-
- private fun renderRecording(holder: Holder) {
- with(holder) {
- playButton.isEnabled = voiceBroadcastState == VoiceBroadcastState.PAUSED
- pauseButton.isEnabled = voiceBroadcastState == VoiceBroadcastState.STARTED || voiceBroadcastState == VoiceBroadcastState.RESUMED
- stopButton.isEnabled = voiceBroadcastState == VoiceBroadcastState.STARTED ||
- voiceBroadcastState == VoiceBroadcastState.RESUMED ||
- voiceBroadcastState == VoiceBroadcastState.PAUSED
- playButton.setOnClickListener { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Resume) }
- pauseButton.setOnClickListener { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Pause) }
- stopButton.setOnClickListener { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) }
- }
- }
-
- override fun getViewStubId() = STUB_ID
-
- class Holder : AbsMessageLocationItem.Holder(STUB_ID) {
- val currentStateText by bind(R.id.currentStateText)
- val playButton by bind(R.id.playButton)
- val pauseButton by bind(R.id.pauseButton)
- val stopButton by bind(R.id.stopButton)
- }
-
- companion object {
- private val STUB_ID = R.id.messageVoiceBroadcastStub
- }
-}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt
new file mode 100644
index 0000000000..d271c55ebb
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt
@@ -0,0 +1,137 @@
+/*
+ * Copyright (c) 2022 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.widget.ImageButton
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.core.view.isVisible
+import com.airbnb.epoxy.EpoxyAttribute
+import com.airbnb.epoxy.EpoxyModelClass
+import im.vector.app.R
+import im.vector.app.core.extensions.tintBackground
+import im.vector.app.core.resources.ColorProvider
+import im.vector.app.core.resources.DrawableProvider
+import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction
+import im.vector.app.features.home.room.detail.timeline.TimelineEventController
+import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
+import org.matrix.android.sdk.api.util.MatrixItem
+
+@EpoxyModelClass
+abstract class MessageVoiceBroadcastRecordingItem : AbsMessageItem() {
+
+ @EpoxyAttribute
+ var callback: TimelineEventController.Callback? = null
+
+ @EpoxyAttribute
+ var voiceBroadcastRecorder: VoiceBroadcastRecorder? = null
+
+ @EpoxyAttribute
+ lateinit var colorProvider: ColorProvider
+
+ @EpoxyAttribute
+ lateinit var drawableProvider: DrawableProvider
+
+ @EpoxyAttribute
+ var roomItem: MatrixItem? = null
+
+ @EpoxyAttribute
+ var title: String? = null
+
+ private lateinit var recorderListener: VoiceBroadcastRecorder.Listener
+
+ override fun isCacheable(): Boolean = false
+
+ override fun bind(holder: Holder) {
+ super.bind(holder)
+ bindVoiceBroadcastItem(holder)
+ }
+
+ private fun bindVoiceBroadcastItem(holder: Holder) {
+ recorderListener = object : VoiceBroadcastRecorder.Listener {
+ override fun onStateUpdated(state: VoiceBroadcastRecorder.State) {
+ renderState(holder, state)
+ }
+ }
+ voiceBroadcastRecorder?.addListener(recorderListener)
+ renderHeader(holder)
+ }
+
+ private fun renderHeader(holder: Holder) {
+ with(holder) {
+ roomItem?.let {
+ attributes.avatarRenderer.render(it, roomAvatarImageView)
+ titleText.text = it.displayName
+ }
+ }
+ }
+
+ private fun renderState(holder: Holder, state: VoiceBroadcastRecorder.State) {
+ with(holder) {
+ when (state) {
+ VoiceBroadcastRecorder.State.Recording -> {
+ stopRecordButton.isEnabled = true
+
+ liveIndicator.isVisible = true
+ liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.colorOnError))
+
+ val drawableColor = colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary)
+ val drawable = drawableProvider.getDrawable(R.drawable.ic_play_pause_pause, drawableColor)
+ recordButton.setImageDrawable(drawable)
+ recordButton.contentDescription = holder.view.resources.getString(R.string.a11y_pause_voice_broadcast_record)
+ recordButton.setOnClickListener { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Pause) }
+ stopRecordButton.setOnClickListener { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) }
+ }
+ VoiceBroadcastRecorder.State.Paused -> {
+ stopRecordButton.isEnabled = true
+
+ liveIndicator.isVisible = true
+ liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.vctr_content_quaternary))
+
+ recordButton.setImageResource(R.drawable.ic_recording_dot)
+ recordButton.contentDescription = holder.view.resources.getString(R.string.a11y_resume_voice_broadcast_record)
+ recordButton.setOnClickListener { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Resume) }
+ stopRecordButton.setOnClickListener { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) }
+ }
+ VoiceBroadcastRecorder.State.Idle -> {
+ recordButton.isEnabled = false
+ stopRecordButton.isEnabled = false
+ liveIndicator.isVisible = false
+ }
+ }
+ }
+ }
+
+ override fun unbind(holder: Holder) {
+ super.unbind(holder)
+ voiceBroadcastRecorder?.removeListener(recorderListener)
+ }
+
+ override fun getViewStubId() = STUB_ID
+
+ class Holder : AbsMessageItem.Holder(STUB_ID) {
+ val liveIndicator by bind(R.id.liveIndicator)
+ val roomAvatarImageView by bind(R.id.roomAvatarImageView)
+ val titleText by bind(R.id.titleText)
+ val recordButton by bind(R.id.recordButton)
+ val stopRecordButton by bind(R.id.stopRecordButton)
+ }
+
+ companion object {
+ private val STUB_ID = R.id.messageVoiceBroadcastStub
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorder.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorder.kt
index c9bb0c5f54..8b69051823 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorder.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorder.kt
@@ -22,12 +22,21 @@ import java.io.File
interface VoiceBroadcastRecorder : VoiceRecorder {
- var listener: Listener?
- var currentSequence: Int
+ val currentSequence: Int
+ val state: State
fun startRecord(roomId: String, chunkLength: Int)
+ fun addListener(listener: Listener)
+ fun removeListener(listener: Listener)
- fun interface Listener {
- fun onVoiceMessageCreated(file: File, @IntRange(from = 1) sequence: Int)
+ interface Listener {
+ fun onVoiceMessageCreated(file: File, @IntRange(from = 1) sequence: Int) = Unit
+ fun onStateUpdated(state: State) = Unit
+ }
+
+ enum class State {
+ Recording,
+ Paused,
+ Idle,
}
}
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorderQ.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorderQ.kt
index a65aae6f8a..cd1a61b986 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorderQ.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorderQ.kt
@@ -32,8 +32,13 @@ class VoiceBroadcastRecorderQ(
private var maxFileSize = 0L // zero or negative for no limit
private var currentRoomId: String? = null
override var currentSequence = 0
+ override var state = VoiceBroadcastRecorder.State.Idle
+ set(value) {
+ field = value
+ listeners.forEach { it.onStateUpdated(value) }
+ }
- override var listener: VoiceBroadcastRecorder.Listener? = null
+ private val listeners = mutableListOf()
override val outputFormat = MediaRecorder.OutputFormat.MPEG_4
override val audioEncoder = MediaRecorder.AudioEncoder.HE_AAC
@@ -57,24 +62,28 @@ class VoiceBroadcastRecorderQ(
maxFileSize = (chunkLength * audioEncodingBitRate / 8).toLong()
currentSequence = 1
startRecord(roomId)
+ state = VoiceBroadcastRecorder.State.Recording
}
override fun pauseRecord() {
tryOrNull { mediaRecorder?.stop() }
mediaRecorder?.reset()
notifyOutputFileCreated()
+ state = VoiceBroadcastRecorder.State.Paused
}
override fun resumeRecord() {
currentSequence++
currentRoomId?.let { startRecord(it) }
+ state = VoiceBroadcastRecorder.State.Recording
}
override fun stopRecord() {
super.stopRecord()
notifyOutputFileCreated()
- listener = null
+ listeners.clear()
currentSequence = 0
+ state = VoiceBroadcastRecorder.State.Idle
}
override fun release() {
@@ -82,6 +91,15 @@ class VoiceBroadcastRecorderQ(
super.release()
}
+ override fun addListener(listener: VoiceBroadcastRecorder.Listener) {
+ listeners.add(listener)
+ listener.onStateUpdated(state)
+ }
+
+ override fun removeListener(listener: VoiceBroadcastRecorder.Listener) {
+ listeners.remove(listener)
+ }
+
private fun onMaxFileSizeApproaching(roomId: String) {
setNextOutputFile(roomId)
}
@@ -92,8 +110,8 @@ class VoiceBroadcastRecorderQ(
}
private fun notifyOutputFileCreated() {
- outputFile?.let {
- listener?.onVoiceMessageCreated(it, currentSequence)
+ outputFile?.let { file ->
+ listeners.forEach { it.onVoiceMessageCreated(file, currentSequence) }
outputFile = nextOutputFile
nextOutputFile = null
}
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt
index d5d58f822e..7934d18e36 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt
@@ -81,9 +81,11 @@ class StartVoiceBroadcastUseCase @Inject constructor(
}
private fun startRecording(room: Room, eventId: String, chunkLength: Int) {
- voiceBroadcastRecorder?.listener = VoiceBroadcastRecorder.Listener { file, sequence ->
- sendVoiceFile(room, file, eventId, sequence)
- }
+ voiceBroadcastRecorder?.addListener(object : VoiceBroadcastRecorder.Listener {
+ override fun onVoiceMessageCreated(file: File, sequence: Int) {
+ sendVoiceFile(room, file, eventId, sequence)
+ }
+ })
voiceBroadcastRecorder?.startRecord(room.roomId, chunkLength)
}
diff --git a/vector/src/main/res/drawable/ic_live_broadcast_16.xml b/vector/src/main/res/drawable/ic_live_broadcast_16.xml
new file mode 100644
index 0000000000..7d427a56d0
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_live_broadcast_16.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/drawable/ic_recording_dot.xml b/vector/src/main/res/drawable/ic_recording_dot.xml
new file mode 100644
index 0000000000..f5d92f9718
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_recording_dot.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/vector/src/main/res/drawable/ic_stop.xml b/vector/src/main/res/drawable/ic_stop.xml
new file mode 100644
index 0000000000..459a7cfce2
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_stop.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/vector/src/main/res/drawable/rounded_rect_shape_2.xml b/vector/src/main/res/drawable/rounded_rect_shape_2.xml
new file mode 100644
index 0000000000..977de2fd09
--- /dev/null
+++ b/vector/src/main/res/drawable/rounded_rect_shape_2.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_stub.xml
index e35060f72a..6773280ba5 100644
--- a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_stub.xml
+++ b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_stub.xml
@@ -5,58 +5,89 @@
android:id="@+id/messageRootLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:background="@drawable/rounded_rect_shape_8"
+ android:backgroundTint="?vctr_content_quinary"
android:padding="@dimen/layout_vertical_margin"
tools:viewBindingIgnore="true">
+
+
+ tools:src="@sample/user_round_avatars" />
+
+
+
+
+
+
+ app:layout_constraintTop_toBottomOf="@id/headerBottomBarrier" />
-
-
+ app:layout_constraintStart_toEndOf="@id/recordButton"
+ app:layout_constraintTop_toTopOf="@id/recordButton" />