Skip to content

Voice Broadcast - Improve timeline rendering code #7448

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
1 change: 1 addition & 0 deletions changelog.d/7448.wip
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[Voice Broadcast] Improve timeline items factory and handle bad recording state display
1 change: 1 addition & 0 deletions library/ui-strings/src/main/res/values/donottranslate.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<resources>

<string name="ellipsis" translatable="false">…</string>
<string name="no_value_placeholder" translatable="false">–</string>

<!-- Temporary string -->
<string name="not_implemented" translatable="false">Not implemented yet in ${app_name}</string>
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
19 changes: 19 additions & 0 deletions library/ui-styles/src/main/res/values/styles_voice_broadcast.xml
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
Expand Up @@ -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(params, messageContent, highlight, callback, attributes)
is MessageVoiceBroadcastInfoContent -> voiceBroadcastItemFactory.create(params, messageContent, highlight, attributes)
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
}
return messageItem?.apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,13 @@
*/
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.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.VoiceBroadcastEventsGroup
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.MessageVoiceBroadcastRecordingItem
Expand All @@ -34,7 +33,7 @@ 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.session.getUser
import org.matrix.android.sdk.api.session.getUserOrDefault
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject

Expand All @@ -51,81 +50,59 @@ class VoiceBroadcastItemFactory @Inject constructor(
params: TimelineItemFactoryParams,
messageContent: MessageVoiceBroadcastInfoContent,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes,
): VectorEpoxyModel<out VectorEpoxyHolder>? {
): AbsMessageVoiceBroadcastItem<*>? {
// Only display item of the initial event with updated data
if (messageContent.voiceBroadcastState != VoiceBroadcastState.STARTED) return null
val eventsGroup = params.eventsGroup ?: return null
val voiceBroadcastEventsGroup = VoiceBroadcastEventsGroup(eventsGroup)
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
val recorderName = mostRecentTimelineEvent.root.stateKey?.let { session.getUser(it) }?.displayName ?: mostRecentTimelineEvent.root.stateKey

val voiceBroadcastEventsGroup = params.eventsGroup?.let { VoiceBroadcastEventsGroup(it) } ?: return null
val voiceBroadcastEvent = voiceBroadcastEventsGroup.getLastDisplayableEvent().root.asVoiceBroadcastEvent() ?: return null
val voiceBroadcastContent = voiceBroadcastEvent.content ?: return null
val voiceBroadcastId = voiceBroadcastEventsGroup.voiceBroadcastId

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(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we inject ActiveSessionHolder instead of Session here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so, the factory is not a singleton and we should not be able to display a timeline if there is no session, it will fail in a previous step in that case

recorder = voiceBroadcastRecorder,
player = voiceBroadcastPlayer,
roomItem = session.getRoom(params.event.roomId)?.roomSummary()?.toMatrixItem(),
colorProvider = colorProvider,
drawableProvider = drawableProvider,
)

return if (isRecording) {
createRecordingItem(
params.event.roomId,
eventsGroup.groupId,
highlight,
callback,
attributes
)
createRecordingItem(highlight, attributes, voiceBroadcastAttributes)
} else {
createListeningItem(
params.event.roomId,
eventsGroup.groupId,
mostRecentMessageContent.voiceBroadcastState,
recorderName,
highlight,
callback,
attributes
)
createListeningItem(highlight, attributes, voiceBroadcastAttributes)
}
}

private fun createRecordingItem(
roomId: String,
voiceBroadcastId: String,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes,
voiceBroadcastAttributes: AbsMessageVoiceBroadcastItem.Attributes,
): MessageVoiceBroadcastRecordingItem {
val roomSummary = session.getRoom(roomId)?.roomSummary()
return MessageVoiceBroadcastRecordingItem_()
.id("voice_broadcast_$voiceBroadcastId")
.id("voice_broadcast_${voiceBroadcastAttributes.voiceBroadcastId}")
.attributes(attributes)
.voiceBroadcastAttributes(voiceBroadcastAttributes)
.highlighted(highlight)
.roomItem(roomSummary?.toMatrixItem())
.colorProvider(colorProvider)
.drawableProvider(drawableProvider)
.voiceBroadcastRecorder(voiceBroadcastRecorder)
.leftGuideline(avatarSizeProvider.leftGuideline)
.callback(callback)
}

private fun createListeningItem(
roomId: String,
voiceBroadcastId: String,
voiceBroadcastState: VoiceBroadcastState?,
broadcasterName: String?,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes,
voiceBroadcastAttributes: AbsMessageVoiceBroadcastItem.Attributes,
): MessageVoiceBroadcastListeningItem {
val roomSummary = session.getRoom(roomId)?.roomSummary()
return MessageVoiceBroadcastListeningItem_()
.id("voice_broadcast_$voiceBroadcastId")
.id("voice_broadcast_${voiceBroadcastAttributes.voiceBroadcastId}")
.attributes(attributes)
.voiceBroadcastAttributes(voiceBroadcastAttributes)
.highlighted(highlight)
.roomItem(roomSummary?.toMatrixItem())
.colorProvider(colorProvider)
.drawableProvider(drawableProvider)
.voiceBroadcastPlayer(voiceBroadcastPlayer)
.voiceBroadcastId(voiceBroadcastId)
.voiceBroadcastState(voiceBroadcastState)
.broadcasterName(broadcasterName)
.leftGuideline(avatarSizeProvider.leftGuideline)
.callback(callback)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ class CallSignalingEventsGroup(private val group: TimelineEventsGroup) {
}

class VoiceBroadcastEventsGroup(private val group: TimelineEventsGroup) {

val voiceBroadcastId = group.groupId

fun getLastDisplayableEvent(): TimelineEvent {
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 }
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
)
}
Loading