Merge pull request #7448 from vector-im/feature/fre/voice_broadcast_timeline_improvements

Voice Broadcast - Improve timeline rendering code
This commit is contained in:
Florian Renaud 2022-10-26 11:53:44 +02:00 committed by GitHub
commit 6f1e0b5bbd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 470 additions and 344 deletions

1
changelog.d/7448.wip Normal file
View File

@ -0,0 +1 @@
[Voice Broadcast] Improve timeline items factory and handle bad recording state display

View File

@ -2,6 +2,7 @@
<resources> <resources>
<string name="ellipsis" translatable="false"></string> <string name="ellipsis" translatable="false"></string>
<string name="no_value_placeholder" translatable="false"></string>
<!-- Temporary string --> <!-- Temporary string -->
<string name="not_implemented" translatable="false">Not implemented yet in ${app_name}</string> <string name="not_implemented" translatable="false">Not implemented yet in ${app_name}</string>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="VoiceBroadcastMetadataView">
<attr name="metadataIcon" format="reference" />
<attr name="metadataValue" format="string" />
</declare-styleable>
</resources>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="VoiceBroadcastLiveIndicator" parent="Widget.AppCompat.TextView">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">20dp</item>
<item name="android:backgroundTint">?colorError</item>
<item name="android:drawablePadding">4dp</item>
<item name="android:ellipsize">end</item>
<item name="android:gravity">center_vertical</item>
<item name="android:maxWidth">100dp</item>
<item name="android:paddingEnd">4dp</item>
<item name="android:paddingStart">4dp</item>
<item name="android:singleLine">true</item>
<item name="android:textColor">?colorOnError</item>
<item name="drawableTint">?colorOnError</item>
</style>
</resources>

View File

@ -201,7 +201,7 @@ class MessageItemFactory @Inject constructor(
is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes) is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes)
is MessageLocationContent -> buildLocationItem(messageContent, informationData, highlight, attributes) is MessageLocationContent -> buildLocationItem(messageContent, informationData, highlight, attributes)
is MessageBeaconInfoContent -> liveLocationShareMessageItemFactory.create(params.event, highlight, attributes) is MessageBeaconInfoContent -> liveLocationShareMessageItemFactory.create(params.event, highlight, attributes)
is MessageVoiceBroadcastInfoContent -> voiceBroadcastItemFactory.create(params, messageContent, highlight, callback, attributes) is MessageVoiceBroadcastInfoContent -> voiceBroadcastItemFactory.create(params, messageContent, highlight, attributes)
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
} }
return messageItem?.apply { return messageItem?.apply {

View File

@ -15,14 +15,13 @@
*/ */
package im.vector.app.features.home.room.detail.timeline.factory package im.vector.app.features.home.room.detail.timeline.factory
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.DrawableProvider import im.vector.app.core.resources.DrawableProvider
import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.displayname.getBestName
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.VoiceBroadcastEventsGroup 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.AbsMessageItem
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageVoiceBroadcastItem
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastListeningItem import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastListeningItem
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastListeningItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastListeningItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastRecordingItem import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastRecordingItem
@ -34,7 +33,7 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.getUser import org.matrix.android.sdk.api.session.getUserOrDefault
import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject import javax.inject.Inject
@ -51,81 +50,59 @@ class VoiceBroadcastItemFactory @Inject constructor(
params: TimelineItemFactoryParams, params: TimelineItemFactoryParams,
messageContent: MessageVoiceBroadcastInfoContent, messageContent: MessageVoiceBroadcastInfoContent,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes, attributes: AbsMessageItem.Attributes,
): VectorEpoxyModel<out VectorEpoxyHolder>? { ): AbsMessageVoiceBroadcastItem<*>? {
// Only display item of the initial event with updated data // Only display item of the initial event with updated data
if (messageContent.voiceBroadcastState != VoiceBroadcastState.STARTED) return null if (messageContent.voiceBroadcastState != VoiceBroadcastState.STARTED) return null
val eventsGroup = params.eventsGroup ?: return null
val voiceBroadcastEventsGroup = VoiceBroadcastEventsGroup(eventsGroup) val voiceBroadcastEventsGroup = params.eventsGroup?.let { VoiceBroadcastEventsGroup(it) } ?: return null
val mostRecentTimelineEvent = voiceBroadcastEventsGroup.getLastDisplayableEvent() val voiceBroadcastEvent = voiceBroadcastEventsGroup.getLastDisplayableEvent().root.asVoiceBroadcastEvent() ?: return null
val mostRecentEvent = mostRecentTimelineEvent.root.asVoiceBroadcastEvent() val voiceBroadcastContent = voiceBroadcastEvent.content ?: return null
val mostRecentMessageContent = mostRecentEvent?.content ?: return null val voiceBroadcastId = voiceBroadcastEventsGroup.voiceBroadcastId
val isRecording = mostRecentMessageContent.voiceBroadcastState != VoiceBroadcastState.STOPPED && mostRecentEvent.root.stateKey == session.myUserId
val recorderName = mostRecentTimelineEvent.root.stateKey?.let { session.getUser(it) }?.displayName ?: mostRecentTimelineEvent.root.stateKey val isRecording = voiceBroadcastContent.voiceBroadcastState != VoiceBroadcastState.STOPPED && voiceBroadcastEvent.root.stateKey == session.myUserId
val voiceBroadcastAttributes = AbsMessageVoiceBroadcastItem.Attributes(
voiceBroadcastId = voiceBroadcastId,
voiceBroadcastState = voiceBroadcastContent.voiceBroadcastState,
recorderName = params.event.root.stateKey?.let { session.getUserOrDefault(it) }?.toMatrixItem()?.getBestName().orEmpty(),
recorder = voiceBroadcastRecorder,
player = voiceBroadcastPlayer,
roomItem = session.getRoom(params.event.roomId)?.roomSummary()?.toMatrixItem(),
colorProvider = colorProvider,
drawableProvider = drawableProvider,
)
return if (isRecording) { return if (isRecording) {
createRecordingItem( createRecordingItem(highlight, attributes, voiceBroadcastAttributes)
params.event.roomId,
eventsGroup.groupId,
highlight,
callback,
attributes
)
} else { } else {
createListeningItem( createListeningItem(highlight, attributes, voiceBroadcastAttributes)
params.event.roomId,
eventsGroup.groupId,
mostRecentMessageContent.voiceBroadcastState,
recorderName,
highlight,
callback,
attributes
)
} }
} }
private fun createRecordingItem( private fun createRecordingItem(
roomId: String,
voiceBroadcastId: String,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes, attributes: AbsMessageItem.Attributes,
voiceBroadcastAttributes: AbsMessageVoiceBroadcastItem.Attributes,
): MessageVoiceBroadcastRecordingItem { ): MessageVoiceBroadcastRecordingItem {
val roomSummary = session.getRoom(roomId)?.roomSummary()
return MessageVoiceBroadcastRecordingItem_() return MessageVoiceBroadcastRecordingItem_()
.id("voice_broadcast_$voiceBroadcastId") .id("voice_broadcast_${voiceBroadcastAttributes.voiceBroadcastId}")
.attributes(attributes) .attributes(attributes)
.voiceBroadcastAttributes(voiceBroadcastAttributes)
.highlighted(highlight) .highlighted(highlight)
.roomItem(roomSummary?.toMatrixItem())
.colorProvider(colorProvider)
.drawableProvider(drawableProvider)
.voiceBroadcastRecorder(voiceBroadcastRecorder)
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
.callback(callback)
} }
private fun createListeningItem( private fun createListeningItem(
roomId: String,
voiceBroadcastId: String,
voiceBroadcastState: VoiceBroadcastState?,
broadcasterName: String?,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes, attributes: AbsMessageItem.Attributes,
voiceBroadcastAttributes: AbsMessageVoiceBroadcastItem.Attributes,
): MessageVoiceBroadcastListeningItem { ): MessageVoiceBroadcastListeningItem {
val roomSummary = session.getRoom(roomId)?.roomSummary()
return MessageVoiceBroadcastListeningItem_() return MessageVoiceBroadcastListeningItem_()
.id("voice_broadcast_$voiceBroadcastId") .id("voice_broadcast_${voiceBroadcastAttributes.voiceBroadcastId}")
.attributes(attributes) .attributes(attributes)
.voiceBroadcastAttributes(voiceBroadcastAttributes)
.highlighted(highlight) .highlighted(highlight)
.roomItem(roomSummary?.toMatrixItem())
.colorProvider(colorProvider)
.drawableProvider(drawableProvider)
.voiceBroadcastPlayer(voiceBroadcastPlayer)
.voiceBroadcastId(voiceBroadcastId)
.voiceBroadcastState(voiceBroadcastState)
.broadcasterName(broadcasterName)
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
.callback(callback)
} }
} }

View File

@ -141,6 +141,9 @@ class CallSignalingEventsGroup(private val group: TimelineEventsGroup) {
} }
class VoiceBroadcastEventsGroup(private val group: TimelineEventsGroup) { class VoiceBroadcastEventsGroup(private val group: TimelineEventsGroup) {
val voiceBroadcastId = group.groupId
fun getLastDisplayableEvent(): TimelineEvent { fun getLastDisplayableEvent(): TimelineEvent {
return group.events.find { it.root.asVoiceBroadcastEvent()?.content?.voiceBroadcastState == VoiceBroadcastState.STOPPED } return group.events.find { it.root.asVoiceBroadcastEvent()?.content?.voiceBroadcastState == VoiceBroadcastState.STOPPED }
?: group.events.filter { it.root.type == VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO }.maxBy { it.root.originServerTs ?: 0L } ?: group.events.filter { it.root.type == VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO }.maxBy { it.root.originServerTs ?: 0L }

View File

@ -0,0 +1,104 @@
/*
* 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.ImageView
import android.widget.TextView
import androidx.annotation.IdRes
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
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.voicebroadcast.VoiceBroadcastPlayer
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
import org.matrix.android.sdk.api.util.MatrixItem
abstract class AbsMessageVoiceBroadcastItem<H : AbsMessageVoiceBroadcastItem.Holder> : AbsMessageItem<H>() {
@EpoxyAttribute
lateinit var voiceBroadcastAttributes: Attributes
protected val voiceBroadcastId get() = voiceBroadcastAttributes.voiceBroadcastId
protected val voiceBroadcastState get() = voiceBroadcastAttributes.voiceBroadcastState
protected val recorderName get() = voiceBroadcastAttributes.recorderName
protected val recorder get() = voiceBroadcastAttributes.recorder
protected val player get() = voiceBroadcastAttributes.player
protected val roomItem get() = voiceBroadcastAttributes.roomItem
protected val colorProvider get() = voiceBroadcastAttributes.colorProvider
protected val drawableProvider get() = voiceBroadcastAttributes.drawableProvider
protected val avatarRenderer get() = attributes.avatarRenderer
protected val callback get() = attributes.callback
override fun isCacheable(): Boolean = false
override fun bind(holder: H) {
super.bind(holder)
renderHeader(holder)
}
private fun renderHeader(holder: H) {
with(holder) {
roomItem?.let {
avatarRenderer.render(it, roomAvatarImageView)
titleText.text = it.displayName
}
}
renderLiveIndicator(holder)
renderMetadata(holder)
}
private fun renderLiveIndicator(holder: H) {
with(holder) {
when (voiceBroadcastState) {
VoiceBroadcastState.STARTED,
VoiceBroadcastState.RESUMED -> {
liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.colorError))
liveIndicator.isVisible = true
}
VoiceBroadcastState.PAUSED -> {
liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.vctr_content_quaternary))
liveIndicator.isVisible = true
}
VoiceBroadcastState.STOPPED, null -> {
liveIndicator.isVisible = false
}
}
}
}
abstract fun renderMetadata(holder: H)
abstract class Holder(@IdRes stubId: Int) : AbsMessageItem.Holder(stubId) {
val liveIndicator by bind<TextView>(R.id.liveIndicator)
val roomAvatarImageView by bind<ImageView>(R.id.roomAvatarImageView)
val titleText by bind<TextView>(R.id.titleText)
}
data class Attributes(
val voiceBroadcastId: String,
val voiceBroadcastState: VoiceBroadcastState?,
val recorderName: String,
val recorder: VoiceBroadcastRecorder?,
val player: VoiceBroadcastPlayer,
val roomItem: MatrixItem?,
val colorProvider: ColorProvider,
val drawableProvider: DrawableProvider,
)
}

View File

@ -18,56 +18,19 @@ package im.vector.app.features.home.room.detail.timeline.item
import android.view.View import android.view.View
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.onClick import im.vector.app.core.epoxy.onClick
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 import im.vector.app.features.home.room.detail.RoomDetailAction
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.voicebroadcast.VoiceBroadcastPlayer import im.vector.app.features.voicebroadcast.VoiceBroadcastPlayer
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
import org.matrix.android.sdk.api.util.MatrixItem
@EpoxyModelClass @EpoxyModelClass
abstract class MessageVoiceBroadcastListeningItem : AbsMessageItem<MessageVoiceBroadcastListeningItem.Holder>() { abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem<MessageVoiceBroadcastListeningItem.Holder>() {
@EpoxyAttribute
var callback: TimelineEventController.Callback? = null
@EpoxyAttribute
var voiceBroadcastPlayer: VoiceBroadcastPlayer? = null
@EpoxyAttribute
lateinit var voiceBroadcastId: String
@EpoxyAttribute
var voiceBroadcastState: VoiceBroadcastState? = null
@EpoxyAttribute
var broadcasterName: String? = null
@EpoxyAttribute
lateinit var colorProvider: ColorProvider
@EpoxyAttribute
lateinit var drawableProvider: DrawableProvider
@EpoxyAttribute
var roomItem: MatrixItem? = null
@EpoxyAttribute
var title: String? = null
private lateinit var playerListener: VoiceBroadcastPlayer.Listener private lateinit var playerListener: VoiceBroadcastPlayer.Listener
override fun isCacheable(): Boolean = false
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
bindVoiceBroadcastItem(holder) bindVoiceBroadcastItem(holder)
@ -75,51 +38,20 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageItem<MessageVoiceB
private fun bindVoiceBroadcastItem(holder: Holder) { private fun bindVoiceBroadcastItem(holder: Holder) {
playerListener = VoiceBroadcastPlayer.Listener { state -> playerListener = VoiceBroadcastPlayer.Listener { state ->
renderState(holder, state) renderPlayingState(holder, state)
} }
voiceBroadcastPlayer?.addListener(playerListener) player.addListener(voiceBroadcastId, playerListener)
renderHeader(holder)
renderLiveIcon(holder)
} }
private fun renderHeader(holder: Holder) { override fun renderMetadata(holder: Holder) {
with(holder) { with(holder) {
roomItem?.let { broadcasterNameMetadata.value = recorderName
attributes.avatarRenderer.render(it, roomAvatarImageView) voiceBroadcastMetadata.isVisible = true
titleText.text = it.displayName listenersCountMetadata.isVisible = false
}
broadcasterNameText.text = broadcasterName
} }
} }
private fun renderLiveIcon(holder: Holder) { private fun renderPlayingState(holder: Holder, state: VoiceBroadcastPlayer.State) {
with(holder) {
when (voiceBroadcastState) {
VoiceBroadcastState.STARTED,
VoiceBroadcastState.RESUMED -> {
liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.colorError))
liveIndicator.isVisible = true
}
VoiceBroadcastState.PAUSED -> {
liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.vctr_content_quaternary))
liveIndicator.isVisible = true
}
VoiceBroadcastState.STOPPED, null -> {
liveIndicator.isVisible = false
}
}
}
}
private fun renderState(holder: Holder, state: VoiceBroadcastPlayer.State) {
if (isCurrentMediaActive()) {
renderActiveMedia(holder, state)
} else {
renderInactiveMedia(holder)
}
}
private fun renderActiveMedia(holder: Holder, state: VoiceBroadcastPlayer.State) {
with(holder) { with(holder) {
bufferingView.isVisible = state == VoiceBroadcastPlayer.State.BUFFERING bufferingView.isVisible = state == VoiceBroadcastPlayer.State.BUFFERING
playPauseButton.isVisible = state != VoiceBroadcastPlayer.State.BUFFERING playPauseButton.isVisible = state != VoiceBroadcastPlayer.State.BUFFERING
@ -127,15 +59,15 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageItem<MessageVoiceB
when (state) { when (state) {
VoiceBroadcastPlayer.State.PLAYING -> { VoiceBroadcastPlayer.State.PLAYING -> {
playPauseButton.setImageResource(R.drawable.ic_play_pause_pause) playPauseButton.setImageResource(R.drawable.ic_play_pause_pause)
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast) playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast)
playPauseButton.onClick { attributes.callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.Pause) } playPauseButton.onClick { callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.Pause) }
} }
VoiceBroadcastPlayer.State.IDLE, VoiceBroadcastPlayer.State.IDLE,
VoiceBroadcastPlayer.State.PAUSED -> { VoiceBroadcastPlayer.State.PAUSED -> {
playPauseButton.setImageResource(R.drawable.ic_play_pause_play) playPauseButton.setImageResource(R.drawable.ic_play_pause_play)
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast) playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast)
playPauseButton.onClick { playPauseButton.onClick {
attributes.callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastId)) callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastId))
} }
} }
VoiceBroadcastPlayer.State.BUFFERING -> Unit VoiceBroadcastPlayer.State.BUFFERING -> Unit
@ -143,34 +75,19 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageItem<MessageVoiceB
} }
} }
private fun renderInactiveMedia(holder: Holder) {
with(holder) {
bufferingView.isVisible = false
playPauseButton.isVisible = true
playPauseButton.setImageResource(R.drawable.ic_play_pause_play)
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast)
playPauseButton.onClick {
attributes.callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastId))
}
}
}
private fun isCurrentMediaActive() = voiceBroadcastPlayer?.currentVoiceBroadcastId == voiceBroadcastId
override fun unbind(holder: Holder) { override fun unbind(holder: Holder) {
super.unbind(holder) super.unbind(holder)
voiceBroadcastPlayer?.removeListener(playerListener) player.removeListener(voiceBroadcastId, playerListener)
} }
override fun getViewStubId() = STUB_ID override fun getViewStubId() = STUB_ID
class Holder : AbsMessageItem.Holder(STUB_ID) { class Holder : AbsMessageVoiceBroadcastItem.Holder(STUB_ID) {
val liveIndicator by bind<TextView>(R.id.liveIndicator)
val roomAvatarImageView by bind<ImageView>(R.id.roomAvatarImageView)
val titleText by bind<TextView>(R.id.titleText)
val playPauseButton by bind<ImageButton>(R.id.playPauseButton) val playPauseButton by bind<ImageButton>(R.id.playPauseButton)
val bufferingView by bind<View>(R.id.bufferingView) val bufferingView by bind<View>(R.id.bufferingView)
val broadcasterNameText by bind<TextView>(R.id.broadcasterNameText) val broadcasterNameMetadata by bind<VoiceBroadcastMetadataView>(R.id.broadcasterNameMetadata)
val voiceBroadcastMetadata by bind<VoiceBroadcastMetadataView>(R.id.voiceBroadcastMetadata)
val listenersCountMetadata by bind<VoiceBroadcastMetadataView>(R.id.listenersCountMetadata)
} }
companion object { companion object {

View File

@ -17,45 +17,19 @@
package im.vector.app.features.home.room.detail.timeline.item package im.vector.app.features.home.room.detail.timeline.item
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.onClick import im.vector.app.core.epoxy.onClick
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.RoomDetailAction.VoiceBroadcastAction
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
import org.matrix.android.sdk.api.util.MatrixItem import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
@EpoxyModelClass @EpoxyModelClass
abstract class MessageVoiceBroadcastRecordingItem : AbsMessageItem<MessageVoiceBroadcastRecordingItem.Holder>() { abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem<MessageVoiceBroadcastRecordingItem.Holder>() {
@EpoxyAttribute private var recorderListener: VoiceBroadcastRecorder.Listener? = null
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) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
@ -63,73 +37,80 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageItem<MessageVoiceB
} }
private fun bindVoiceBroadcastItem(holder: Holder) { private fun bindVoiceBroadcastItem(holder: Holder) {
if (recorder != null && recorder?.state != VoiceBroadcastRecorder.State.Idle) {
recorderListener = object : VoiceBroadcastRecorder.Listener { recorderListener = object : VoiceBroadcastRecorder.Listener {
override fun onStateUpdated(state: VoiceBroadcastRecorder.State) { override fun onStateUpdated(state: VoiceBroadcastRecorder.State) {
renderState(holder, state) renderRecordingState(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
} }
}.also { recorder?.addListener(it) }
} else {
renderVoiceBroadcastState(holder)
} }
} }
private fun renderState(holder: Holder, state: VoiceBroadcastRecorder.State) { override fun renderMetadata(holder: Holder) {
with(holder) { with(holder) {
listenersCountMetadata.isVisible = false
remainingTimeMetadata.isVisible = false
}
}
private fun renderRecordingState(holder: Holder, state: VoiceBroadcastRecorder.State) {
when (state) { when (state) {
VoiceBroadcastRecorder.State.Recording -> { VoiceBroadcastRecorder.State.Recording -> renderRecordingState(holder)
VoiceBroadcastRecorder.State.Paused -> renderPausedState(holder)
VoiceBroadcastRecorder.State.Idle -> renderStoppedState(holder)
}
}
private fun renderVoiceBroadcastState(holder: Holder) {
when (voiceBroadcastState) {
VoiceBroadcastState.STARTED,
VoiceBroadcastState.RESUMED -> renderRecordingState(holder)
VoiceBroadcastState.PAUSED -> renderPausedState(holder)
VoiceBroadcastState.STOPPED,
null -> renderStoppedState(holder)
}
}
private fun renderRecordingState(holder: Holder) = with(holder) {
stopRecordButton.isEnabled = true stopRecordButton.isEnabled = true
recordButton.isEnabled = true recordButton.isEnabled = true
liveIndicator.isVisible = true
liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.colorError))
val drawableColor = colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary) val drawableColor = colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary)
val drawable = drawableProvider.getDrawable(R.drawable.ic_play_pause_pause, drawableColor) val drawable = drawableProvider.getDrawable(R.drawable.ic_play_pause_pause, drawableColor)
recordButton.setImageDrawable(drawable) recordButton.setImageDrawable(drawable)
recordButton.contentDescription = holder.view.resources.getString(R.string.a11y_pause_voice_broadcast_record) recordButton.contentDescription = holder.view.resources.getString(R.string.a11y_pause_voice_broadcast_record)
recordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Pause) } recordButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Pause) }
stopRecordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) } stopRecordButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) }
} }
VoiceBroadcastRecorder.State.Paused -> {
private fun renderPausedState(holder: Holder) = with(holder) {
stopRecordButton.isEnabled = true stopRecordButton.isEnabled = true
recordButton.isEnabled = true recordButton.isEnabled = true
liveIndicator.isVisible = true
liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.vctr_content_quaternary))
recordButton.setImageResource(R.drawable.ic_recording_dot) recordButton.setImageResource(R.drawable.ic_recording_dot)
recordButton.contentDescription = holder.view.resources.getString(R.string.a11y_resume_voice_broadcast_record) recordButton.contentDescription = holder.view.resources.getString(R.string.a11y_resume_voice_broadcast_record)
recordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Resume) } recordButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Resume) }
stopRecordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) } stopRecordButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) }
} }
VoiceBroadcastRecorder.State.Idle -> {
private fun renderStoppedState(holder: Holder) = with(holder) {
recordButton.isEnabled = false recordButton.isEnabled = false
stopRecordButton.isEnabled = false stopRecordButton.isEnabled = false
liveIndicator.isVisible = false
}
}
}
} }
override fun unbind(holder: Holder) { override fun unbind(holder: Holder) {
super.unbind(holder) super.unbind(holder)
voiceBroadcastRecorder?.removeListener(recorderListener) recorderListener?.let { recorder?.removeListener(it) }
recorderListener = null
} }
override fun getViewStubId() = STUB_ID override fun getViewStubId() = STUB_ID
class Holder : AbsMessageItem.Holder(STUB_ID) { class Holder : AbsMessageVoiceBroadcastItem.Holder(STUB_ID) {
val liveIndicator by bind<TextView>(R.id.liveIndicator) val listenersCountMetadata by bind<VoiceBroadcastMetadataView>(R.id.listenersCountMetadata)
val roomAvatarImageView by bind<ImageView>(R.id.roomAvatarImageView) val remainingTimeMetadata by bind<VoiceBroadcastMetadataView>(R.id.remainingTimeMetadata)
val titleText by bind<TextView>(R.id.titleText)
val recordButton by bind<ImageButton>(R.id.recordButton) val recordButton by bind<ImageButton>(R.id.recordButton)
val stopRecordButton by bind<ImageButton>(R.id.stopRecordButton) val stopRecordButton by bind<ImageButton>(R.id.stopRecordButton)
} }

View File

@ -82,10 +82,17 @@ class VoiceBroadcastPlayer @Inject constructor(
set(value) { set(value) {
Timber.w("## VoiceBroadcastPlayer state: $field -> $value") Timber.w("## VoiceBroadcastPlayer state: $field -> $value")
field = value field = value
listeners.forEach { it.onStateChanged(value) } // Notify state change to all the listeners attached to the current voice broadcast id
currentVoiceBroadcastId?.let { voiceBroadcastId ->
listeners[voiceBroadcastId]?.forEach { listener -> listener.onStateChanged(value) }
}
} }
private var currentRoomId: String? = null private var currentRoomId: String? = null
private var listeners = CopyOnWriteArrayList<Listener>()
/**
* Map voiceBroadcastId to listeners.
*/
private val listeners: MutableMap<String, CopyOnWriteArrayList<Listener>> = mutableMapOf()
fun playOrResume(roomId: String, eventId: String) { fun playOrResume(roomId: String, eventId: String) {
val hasChanged = currentVoiceBroadcastId != eventId val hasChanged = currentVoiceBroadcastId != eventId
@ -133,13 +140,21 @@ class VoiceBroadcastPlayer @Inject constructor(
currentVoiceBroadcastId = null currentVoiceBroadcastId = null
} }
fun addListener(listener: Listener) { /**
listeners.add(listener) * Add a [Listener] to the given voice broadcast id.
listener.onStateChanged(state) */
fun addListener(voiceBroadcastId: String, listener: Listener) {
listeners[voiceBroadcastId]?.add(listener) ?: run {
listeners[voiceBroadcastId] = CopyOnWriteArrayList<Listener>().apply { add(listener) }
}
if (voiceBroadcastId == currentVoiceBroadcastId) listener.onStateChanged(state) else listener.onStateChanged(State.IDLE)
} }
fun removeListener(listener: Listener) { /**
listeners.remove(listener) * Remove a [Listener] from the given voice broadcast id.
*/
fun removeListener(voiceBroadcastId: String, listener: Listener) {
listeners[voiceBroadcastId]?.remove(listener)
} }
private fun startPlayback(roomId: String, eventId: String) { private fun startPlayback(roomId: String, eventId: String) {

View File

@ -0,0 +1,66 @@
/*
* 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.voicebroadcast.views
import android.content.Context
import android.content.res.TypedArray
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import androidx.core.content.res.use
import im.vector.app.R
import im.vector.app.databinding.ViewVoiceBroadcastMetadataBinding
class VoiceBroadcastMetadataView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
private val views = ViewVoiceBroadcastMetadataBinding.inflate(
LayoutInflater.from(context),
this
)
var value: String
get() = views.metadataValue.text.toString()
set(newValue) {
views.metadataValue.text = newValue
}
init {
context.obtainStyledAttributes(
attrs,
R.styleable.VoiceBroadcastMetadataView,
0,
0
).use {
setIcon(it)
setValue(it)
}
}
private fun setIcon(typedArray: TypedArray) {
val icon = typedArray.getDrawable(R.styleable.VoiceBroadcastMetadataView_metadataIcon)
views.metadataIcon.setImageDrawable(icon)
}
private fun setValue(typedArray: TypedArray) {
val value = typedArray.getString(R.styleable.VoiceBroadcastMetadataView_metadataValue)
views.metadataValue.text = value
}
}

View File

@ -0,0 +1,9 @@
<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="M10,1H6V2.333H10V1ZM7.333,9.667H8.667V5.667H7.333V9.667ZM12.687,5.26L13.633,4.313C13.347,3.973 13.033,3.653 12.693,3.373L11.747,4.32C10.713,3.493 9.413,3 8,3C4.687,3 2,5.687 2,9C2,12.313 4.68,15 8,15C11.32,15 14,12.313 14,9C14,7.587 13.507,6.287 12.687,5.26ZM8,13.667C5.42,13.667 3.333,11.58 3.333,9C3.333,6.42 5.42,4.333 8,4.333C10.58,4.333 12.667,6.42 12.667,9C12.667,11.58 10.58,13.667 8,13.667Z"
android:fillColor="#737D8C"/>
</vector>

View File

@ -0,0 +1,12 @@
<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="M5.4,4.1C5.4,2.664 6.564,1.5 8,1.5C9.436,1.5 10.6,2.664 10.6,4.1V7.988C10.6,9.424 9.436,10.588 8,10.588C6.564,10.588 5.4,9.424 5.4,7.988V4.1Z"
android:fillColor="#737D8C"/>
<path
android:pathData="M3.45,7.158C3.91,7.158 4.283,7.531 4.283,7.992C4.283,10.037 5.941,11.697 7.99,11.703C7.993,11.703 7.996,11.703 8,11.703C8.003,11.703 8.006,11.703 8.01,11.703C10.059,11.697 11.716,10.037 11.716,7.992C11.716,7.531 12.089,7.158 12.55,7.158C13.01,7.158 13.383,7.531 13.383,7.992C13.383,10.679 11.41,12.905 8.833,13.305V13.834C8.833,14.294 8.46,14.667 8,14.667C7.539,14.667 7.166,14.294 7.166,13.834V13.305C4.59,12.905 2.616,10.679 2.616,7.992C2.616,7.531 2.989,7.158 3.45,7.158Z"
android:fillColor="#737D8C"/>
</vector>

View File

@ -7,25 +7,14 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/rounded_rect_shape_8" android:background="@drawable/rounded_rect_shape_8"
android:backgroundTint="?vctr_content_quinary" android:backgroundTint="?vctr_content_quinary"
android:padding="@dimen/layout_vertical_margin" android:padding="@dimen/layout_vertical_margin">
tools:viewBindingIgnore="true">
<TextView <TextView
android:id="@+id/liveIndicator" android:id="@+id/liveIndicator"
android:layout_width="wrap_content" style="@style/VoiceBroadcastLiveIndicator"
android:layout_height="20dp"
android:background="@drawable/rounded_rect_shape_2" android:background="@drawable/rounded_rect_shape_2"
android:backgroundTint="?colorError"
android:drawablePadding="4dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxWidth="100dp"
android:paddingHorizontal="4dp"
android:singleLine="true"
android:text="@string/voice_broadcast_live" android:text="@string/voice_broadcast_live"
android:textColor="?colorOnError" app:drawableStartCompat="@drawable/ic_voice_broadcast"
app:drawableStartCompat="@drawable/ic_voice_broadcast_16"
app:drawableTint="?colorOnError"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
@ -54,61 +43,41 @@
android:contentDescription="@string/avatar" android:contentDescription="@string/avatar"
app:layout_constraintStart_toEndOf="@id/avatarRightBarrier" app:layout_constraintStart_toEndOf="@id/avatarRightBarrier"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:src="@sample/rooms.json/data/name" /> tools:text="@sample/rooms.json/data/name" />
<LinearLayout <androidx.constraintlayout.helper.widget.Flow
android:id="@+id/broadcasterViewGroup" android:id="@+id/metadataFlow"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:gravity="center_vertical" android:orientation="vertical"
android:orientation="horizontal" app:constraint_referenced_ids="broadcasterNameMetadata,voiceBroadcastMetadata,listenersCountMetadata"
app:flow_horizontalAlign="start"
app:flow_verticalGap="4dp"
app:layout_constraintStart_toEndOf="@id/avatarRightBarrier" app:layout_constraintStart_toEndOf="@id/avatarRightBarrier"
app:layout_constraintTop_toBottomOf="@id/titleText"> app:layout_constraintTop_toBottomOf="@id/titleText" />
<ImageView <im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
android:id="@+id/broadcasterIcon" android:id="@+id/broadcasterNameMetadata"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginEnd="5dp"
android:contentDescription="@null"
android:src="@drawable/ic_microphone"
app:tint="?vctr_content_secondary" />
<TextView
android:id="@+id/broadcasterNameText"
style="@style/Widget.Vector.TextView.Caption"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
tools:text="@sample/users.json/data/displayName" /> app:metadataIcon="@drawable/ic_voice_broadcast_mic"
</LinearLayout> tools:metadataValue="@sample/users.json/data/displayName" />
<LinearLayout <im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
android:id="@+id/voiceBroadcastViewGroup" android:id="@+id/voiceBroadcastMetadata"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="4dp" app:metadataIcon="@drawable/ic_voice_broadcast"
android:gravity="center_vertical" app:metadataValue="@string/attachment_type_voice_broadcast" />
android:orientation="horizontal"
app:layout_constraintStart_toEndOf="@id/avatarRightBarrier"
app:layout_constraintTop_toBottomOf="@id/broadcasterViewGroup">
<ImageView <im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
android:id="@+id/voiceBroadcastIcon" android:id="@+id/listenersCountMetadata"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginEnd="5dp"
android:contentDescription="@null"
android:src="@drawable/ic_voice_broadcast_16"
app:tint="?vctr_content_secondary" />
<TextView
android:id="@+id/voiceBroadcastText"
style="@style/Widget.Vector.TextView.Caption"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/attachment_type_voice_broadcast" /> app:metadataIcon="@drawable/ic_member_small"
</LinearLayout> app:metadataValue="@string/no_value_placeholder"
tools:metadataValue="5 listeners" />
<androidx.constraintlayout.widget.Barrier <androidx.constraintlayout.widget.Barrier
android:id="@+id/headerBottomBarrier" android:id="@+id/headerBottomBarrier"
@ -116,7 +85,16 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:barrierDirection="bottom" app:barrierDirection="bottom"
app:barrierMargin="12dp" app:barrierMargin="12dp"
app:constraint_referenced_ids="roomAvatarImageView,titleText,broadcasterViewGroup,voiceBroadcastViewGroup" /> app:constraint_referenced_ids="roomAvatarImageView,titleText,metadataFlow" />
<androidx.constraintlayout.helper.widget.Flow
android:id="@+id/controllerButtonsFlow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
app:constraint_referenced_ids="playPauseButton,bufferingView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/headerBottomBarrier" />
<ImageButton <ImageButton
android:id="@+id/playPauseButton" android:id="@+id/playPauseButton"
@ -126,24 +104,14 @@
android:backgroundTint="?vctr_system" android:backgroundTint="?vctr_system"
android:contentDescription="@string/a11y_play_voice_broadcast" android:contentDescription="@string/a11y_play_voice_broadcast"
android:src="@drawable/ic_play_pause_play" android:src="@drawable/ic_play_pause_play"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/headerBottomBarrier"
app:tint="?vctr_content_secondary" /> app:tint="?vctr_content_secondary" />
<ProgressBar <ProgressBar
android:id="@+id/bufferingView" android:id="@+id/bufferingView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:contentDescription="@string/a11y_voice_broadcast_buffering" android:contentDescription="@string/a11y_voice_broadcast_buffering"
android:indeterminate="true" android:indeterminate="true"
android:indeterminateTint="?vctr_content_secondary" android:indeterminateTint="?vctr_content_secondary" />
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/headerBottomBarrier" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -7,25 +7,14 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/rounded_rect_shape_8" android:background="@drawable/rounded_rect_shape_8"
android:backgroundTint="?vctr_content_quinary" android:backgroundTint="?vctr_content_quinary"
android:padding="@dimen/layout_vertical_margin" android:padding="@dimen/layout_vertical_margin">
tools:viewBindingIgnore="true">
<TextView <TextView
android:id="@+id/liveIndicator" android:id="@+id/liveIndicator"
android:layout_width="wrap_content" style="@style/VoiceBroadcastLiveIndicator"
android:layout_height="20dp"
android:background="@drawable/rounded_rect_shape_2" android:background="@drawable/rounded_rect_shape_2"
android:backgroundTint="?colorError"
android:drawablePadding="4dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxWidth="100dp"
android:paddingHorizontal="4dp"
android:singleLine="true"
android:text="@string/voice_broadcast_live" android:text="@string/voice_broadcast_live"
android:textColor="?colorOnError" app:drawableStartCompat="@drawable/ic_voice_broadcast"
app:drawableStartCompat="@drawable/ic_voice_broadcast_16"
app:drawableTint="?colorOnError"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
@ -54,7 +43,34 @@
android:contentDescription="@string/avatar" android:contentDescription="@string/avatar"
app:layout_constraintStart_toEndOf="@id/avatarRightBarrier" app:layout_constraintStart_toEndOf="@id/avatarRightBarrier"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:src="@sample/users.json/data/displayName" /> tools:text="@sample/users.json/data/displayName" />
<androidx.constraintlayout.helper.widget.Flow
android:id="@+id/metadataFlow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:orientation="vertical"
app:constraint_referenced_ids="listenersCountMetadata,remainingTimeMetadata"
app:flow_horizontalAlign="start"
app:flow_verticalGap="4dp"
app:layout_constraintStart_toEndOf="@id/avatarRightBarrier"
app:layout_constraintTop_toBottomOf="@id/titleText" />
<im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
android:id="@+id/listenersCountMetadata"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:metadataIcon="@drawable/ic_member_small"
app:metadataValue="@string/no_value_placeholder"
tools:metadataValue="5 listening" />
<im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
android:id="@+id/remainingTimeMetadata"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:metadataIcon="@drawable/ic_timer"
tools:metadataValue="3h 2m 50s left" />
<androidx.constraintlayout.widget.Barrier <androidx.constraintlayout.widget.Barrier
android:id="@+id/headerBottomBarrier" android:id="@+id/headerBottomBarrier"
@ -62,7 +78,16 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:barrierDirection="bottom" app:barrierDirection="bottom"
app:barrierMargin="12dp" app:barrierMargin="12dp"
app:constraint_referenced_ids="roomAvatarImageView,titleText" /> app:constraint_referenced_ids="roomAvatarImageView,titleText,metadataFlow" />
<androidx.constraintlayout.helper.widget.Flow
android:id="@+id/controllerButtonsFlow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
app:constraint_referenced_ids="recordButton,stopRecordButton"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/headerBottomBarrier" />
<ImageButton <ImageButton
android:id="@+id/recordButton" android:id="@+id/recordButton"
@ -71,11 +96,7 @@
android:background="@drawable/bg_rounded_button" android:background="@drawable/bg_rounded_button"
android:backgroundTint="?vctr_system" android:backgroundTint="?vctr_system"
android:contentDescription="@string/a11y_resume_voice_broadcast_record" android:contentDescription="@string/a11y_resume_voice_broadcast_record"
android:src="@drawable/ic_recording_dot" android:src="@drawable/ic_recording_dot" />
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/stopRecordButton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/headerBottomBarrier" />
<ImageButton <ImageButton
android:id="@+id/stopRecordButton" android:id="@+id/stopRecordButton"
@ -84,10 +105,6 @@
android:background="@drawable/bg_rounded_button" android:background="@drawable/bg_rounded_button"
android:backgroundTint="?vctr_system" android:backgroundTint="?vctr_system"
android:contentDescription="@string/a11y_stop_voice_broadcast_record" android:contentDescription="@string/a11y_stop_voice_broadcast_record"
android:src="@drawable/ic_stop" android:src="@drawable/ic_stop" />
app:layout_constraintBottom_toBottomOf="@id/recordButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/recordButton"
app:layout_constraintTop_toTopOf="@id/recordButton" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
tools:parentTag="android.widget.LinearLayout">
<ImageView
android:id="@+id/metadataIcon"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginEnd="4dp"
android:contentDescription="@null"
app:tint="?vctr_content_secondary"
tools:src="@drawable/ic_voice_broadcast" />
<TextView
android:id="@+id/metadata_value"
style="@style/Widget.Vector.TextView.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/no_value_placeholder"
tools:text="@string/attachment_type_voice_broadcast" />
</merge>