Merge pull request #7273 from vector-im/feature/fre/voice_broadcast_state_event

Voice Broadcast - Send state events
This commit is contained in:
Florian Renaud 2022-10-07 14:53:30 +02:00 committed by GitHub
commit 48d2cc4745
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 1396 additions and 33 deletions

1
changelog.d/7273.wip Normal file
View file

@ -0,0 +1 @@
[Voice Broadcast] Add the "io.element.voice_broadcast_info" state event with a minimalist timeline widget

View file

@ -43,4 +43,7 @@ object MessageType {
// Fake message types for live location events to be able to inherit them from MessageContent
const val MSGTYPE_BEACON_INFO = "org.matrix.android.sdk.beacon.info"
const val MSGTYPE_BEACON_LOCATION_DATA = "org.matrix.android.sdk.beacon.location.data"
// Fake message types for voice broadcast events to be able to inherit them from MessageContent
const val MSGTYPE_VOICE_BROADCAST_INFO = "io.element.voicebroadcast.info"
}

View file

@ -16,9 +16,14 @@
package im.vector.app.core.extensions
import im.vector.app.features.voicebroadcast.STATE_ROOM_VOICE_BROADCAST_INFO
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
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.message.MessageContent
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
fun TimelineEvent.canReact(): Boolean {
// Only event of type EventType.MESSAGE, EventType.STICKER and EventType.POLL_START are supported for the moment
@ -26,3 +31,15 @@ fun TimelineEvent.canReact(): Boolean {
root.sendState == SendState.SYNCED &&
!root.isRedacted()
}
/**
* Get last MessageContent, after a possible edition.
* This method iterate on the vector event types and fallback to [getLastMessageContent] from the matrix sdk for the other types.
*/
fun TimelineEvent.getVectorLastMessageContent(): MessageContent? {
// Iterate on event types which are not part of the matrix sdk, otherwise fallback to the sdk method
return when (root.getClearType()) {
STATE_ROOM_VOICE_BROADCAST_INFO -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessageVoiceBroadcastInfoContent>()
else -> getLastMessageContent()
}
}

View file

@ -79,7 +79,6 @@ sealed class RoomDetailAction : VectorViewModelAction {
data class ReRequestKeys(val eventId: String) : RoomDetailAction()
object SelectStickerAttachment : RoomDetailAction()
object StartVoiceBroadcast : RoomDetailAction()
object OpenIntegrationManager : RoomDetailAction()
object ManageIntegrations : RoomDetailAction()
data class AddJitsiWidget(val withVideo: Boolean) : RoomDetailAction()
@ -120,4 +119,11 @@ sealed class RoomDetailAction : VectorViewModelAction {
object StopLiveLocationSharing : RoomDetailAction()
object OpenElementCallWidget : RoomDetailAction()
sealed class VoiceBroadcastAction : RoomDetailAction() {
object Start : VoiceBroadcastAction()
object Pause : VoiceBroadcastAction()
object Resume : VoiceBroadcastAction()
object Stop : VoiceBroadcastAction()
}
}

View file

@ -65,6 +65,7 @@ import im.vector.app.features.raw.wellknown.withElementWellKnown
import im.vector.app.features.session.coroutineScope
import im.vector.app.features.settings.VectorDataStore
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.voicebroadcast.VoiceBroadcastHelper
import im.vector.lib.core.utils.flow.chunk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
@ -149,6 +150,7 @@ class TimelineViewModel @AssistedInject constructor(
buildMeta: BuildMeta,
timelineFactory: TimelineFactory,
private val spaceStateHandler: SpaceStateHandler,
private val voiceBroadcastHelper: VoiceBroadcastHelper,
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener, LocationSharingServiceConnection.Callback {
@ -456,7 +458,7 @@ class TimelineViewModel @AssistedInject constructor(
is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action)
is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action)
is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment()
is RoomDetailAction.StartVoiceBroadcast -> handleStartVoiceBroadcast()
is RoomDetailAction.VoiceBroadcastAction -> handleVoiceBroadcastAction(action)
is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager()
is RoomDetailAction.StartCall -> handleStartCall(action)
is RoomDetailAction.AcceptCall -> handleAcceptCall(action)
@ -598,9 +600,16 @@ class TimelineViewModel @AssistedInject constructor(
}
}
private fun handleStartVoiceBroadcast() {
// Todo implement start voice broadcast action
Timber.d("Start voice broadcast clicked")
private fun handleVoiceBroadcastAction(action: RoomDetailAction.VoiceBroadcastAction) {
if (room == null) return
viewModelScope.launch {
when (action) {
RoomDetailAction.VoiceBroadcastAction.Start -> voiceBroadcastHelper.startVoiceBroadcast(room.roomId)
RoomDetailAction.VoiceBroadcastAction.Pause -> voiceBroadcastHelper.pauseVoiceBroadcast(room.roomId)
RoomDetailAction.VoiceBroadcastAction.Resume -> voiceBroadcastHelper.resumeVoiceBroadcast(room.roomId)
RoomDetailAction.VoiceBroadcastAction.Stop -> voiceBroadcastHelper.stopVoiceBroadcast(room.roomId)
}
}
}
private fun handleOpenIntegrationManager() {

View file

@ -48,6 +48,7 @@ import com.vanniktech.emoji.EmojiPopup
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.core.error.fatalError
import im.vector.app.core.extensions.getVectorLastMessageContent
import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.extensions.showKeyboard
import im.vector.app.core.glide.GlideApp
@ -73,6 +74,7 @@ import im.vector.app.features.command.ParsedCommand
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.AutoCompleter
import im.vector.app.features.home.room.detail.RoomDetailAction
import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction
import im.vector.app.features.home.room.detail.TimelineViewModel
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView
import im.vector.app.features.home.room.detail.timeline.action.MessageSharedActionViewModel
@ -102,7 +104,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageFormat
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem
import reactivecircus.flowbinding.android.view.focusChanges
@ -355,7 +356,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
setTextColor(matrixItemColorProvider.getColor(MatrixItem.UserItem(event.root.senderId ?: "@")))
}
val messageContent: MessageContent? = event.getLastMessageContent()
val messageContent: MessageContent? = event.getVectorLastMessageContent()
val nonFormattedBody = when (messageContent) {
is MessageAudioContent -> getAudioContentBodyText(messageContent)
is MessagePollContent -> messageContent.getBestPollCreationInfo()?.question?.getBestQuestion()
@ -653,7 +654,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
locationOwnerId = session.myUserId
)
}
AttachmentTypeSelectorView.Type.VOICE_BROADCAST -> timelineViewModel.handle(RoomDetailAction.StartVoiceBroadcast)
AttachmentTypeSelectorView.Type.VOICE_BROADCAST -> timelineViewModel.handle(VoiceBroadcastAction.Start)
}
}

View file

@ -23,6 +23,7 @@ import dagger.assisted.AssistedInject
import im.vector.app.R
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.extensions.getVectorLastMessageContent
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.analytics.AnalyticsTracker
@ -62,7 +63,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.model.relation.shouldRenderInThread
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.send.UserDraft
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
import org.matrix.android.sdk.api.session.room.timeline.getRelationContent
import org.matrix.android.sdk.api.session.room.timeline.getTextEditableContent
import org.matrix.android.sdk.api.session.space.CreateSpaceParams
@ -513,7 +513,7 @@ class MessageComposerViewModel @AssistedInject constructor(
room.relationService().editReply(state.sendMode.timelineEvent, it, action.text.toString())
}
} else {
val messageContent = state.sendMode.timelineEvent.getLastMessageContent()
val messageContent = state.sendMode.timelineEvent.getVectorLastMessageContent()
val existingBody = messageContent?.body ?: ""
if (existingBody != action.text) {
room.relationService().editTextMessage(

View file

@ -17,6 +17,7 @@
package im.vector.app.features.home.room.detail.timeline.action
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.features.voicebroadcast.STATE_ROOM_VOICE_BROADCAST_INFO
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import javax.inject.Inject
@ -27,8 +28,13 @@ class CheckIfCanRedactEventUseCase @Inject constructor(
fun execute(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean {
// Only some event types are supported for the moment
val canRedactEventTypes = listOf(EventType.MESSAGE, EventType.STICKER) +
EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO
val canRedactEventTypes: List<String> = listOf(
EventType.MESSAGE,
EventType.STICKER,
STATE_ROOM_VOICE_BROADCAST_INFO,
) +
EventType.POLL_START +
EventType.STATE_ROOM_BEACON_INFO
return event.root.getClearType() in canRedactEventTypes &&
// Message sent by the current user can always be redacted, else check permission for messages sent by other users

View file

@ -25,6 +25,7 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.canReact
import im.vector.app.core.extensions.getVectorLastMessageContent
import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
@ -60,7 +61,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachme
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
import org.matrix.android.sdk.api.session.room.timeline.hasBeenEdited
import org.matrix.android.sdk.api.session.room.timeline.isPoll
import org.matrix.android.sdk.api.session.room.timeline.isRootThread
@ -187,7 +187,7 @@ class MessageActionsViewModel @AssistedInject constructor(
when (timelineEvent.root.getClearType()) {
EventType.MESSAGE,
EventType.STICKER -> {
val messageContent: MessageContent? = timelineEvent.getLastMessageContent()
val messageContent: MessageContent? = timelineEvent.getVectorLastMessageContent()
if (messageContent is MessageTextContent && messageContent.format == MessageFormat.FORMAT_MATRIX_HTML) {
val html = messageContent.formattedBody
?.takeIf { it.isNotBlank() }
@ -253,7 +253,7 @@ class MessageActionsViewModel @AssistedInject constructor(
}
private fun actionsForEvent(timelineEvent: TimelineEvent, actionPermissions: ActionPermissions): List<EventSharedAction> {
val messageContent = timelineEvent.getLastMessageContent()
val messageContent = timelineEvent.getVectorLastMessageContent()
val msgType = messageContent?.msgType
return arrayListOf<EventSharedAction>().apply {

View file

@ -28,6 +28,7 @@ import dagger.Lazy
import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.extensions.getVectorLastMessageContent
import im.vector.app.core.files.LocalFilesHelper
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider
@ -55,6 +56,8 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageLocationItem
import im.vector.app.features.home.room.detail.timeline.item.MessageLocationItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem
import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem_
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.MessageVoiceItem
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem_
import im.vector.app.features.home.room.detail.timeline.item.PollItem
@ -77,6 +80,7 @@ import im.vector.app.features.media.ImageContentRenderer
import im.vector.app.features.media.VideoContentRenderer
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.voice.AudioWaveformView
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import me.gujun.android.span.span
import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl
@ -102,7 +106,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageVerification
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
import org.matrix.android.sdk.api.settings.LightweightSettingsStorage
import org.matrix.android.sdk.api.util.MimeTypes
import javax.inject.Inject
@ -163,7 +166,7 @@ class MessageItemFactory @Inject constructor(
return buildRedactedItem(attributes, highlight)
}
val messageContent = event.getLastMessageContent()
val messageContent = event.getVectorLastMessageContent()
if (messageContent == null) {
val malformedText = stringProvider.getString(R.string.malformed_message)
return defaultItemFactory.create(malformedText, informationData, highlight, callback)
@ -197,6 +200,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 -> buildVoiceBroadcastItem(messageContent, highlight, callback, attributes)
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
}
return messageItem?.apply {
@ -706,6 +710,20 @@ class MessageItemFactory @Inject constructor(
.highlighted(highlight)
}
private fun buildVoiceBroadcastItem(
messageContent: MessageVoiceBroadcastInfoContent,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes,
): MessageVoiceBroadcastItem? {
return MessageVoiceBroadcastItem_()
.attributes(attributes)
.highlighted(highlight)
.voiceBroadcastState(messageContent.voiceBroadcastState)
.leftGuideline(avatarSizeProvider.leftGuideline)
.callback(callback)
}
private fun List<Int?>?.toFft(): List<Int>? {
return this
?.filterNotNull()

View file

@ -21,6 +21,7 @@ import im.vector.app.core.epoxy.TimelineEmptyItem_
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.features.analytics.DecryptionFailureTracker
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper
import im.vector.app.features.voicebroadcast.STATE_ROOM_VOICE_BROADCAST_INFO
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import timber.log.Timber
@ -88,6 +89,7 @@ class TimelineItemFactory @Inject constructor(
// State room create
EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(params)
in EventType.STATE_ROOM_BEACON_INFO -> messageItemFactory.create(params)
STATE_ROOM_VOICE_BROADCAST_INFO -> messageItemFactory.create(params)
// Unhandled state event types
else -> {
// Should only happen when shouldShowHiddenEvents() settings is ON

View file

@ -19,6 +19,7 @@ package im.vector.app.features.home.room.detail.timeline.format
import dagger.Lazy
import im.vector.app.EmojiSpanify
import im.vector.app.R
import im.vector.app.core.extensions.getVectorLastMessageContent
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.html.EventHtmlRenderer
@ -34,7 +35,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
import org.matrix.android.sdk.api.session.room.timeline.getTextDisplayableContent
import javax.inject.Inject
@ -60,7 +60,7 @@ class DisplayableEventFormatter @Inject constructor(
return when (timelineEvent.root.getClearType()) {
EventType.MESSAGE -> {
timelineEvent.getLastMessageContent()?.let { messageContent ->
timelineEvent.getVectorLastMessageContent()?.let { messageContent ->
when (messageContent.msgType) {
MessageType.MSGTYPE_TEXT -> {
val body = messageContent.getTextDisplayableContent()

View file

@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail.timeline.helper
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.extensions.getVectorLastMessageContent
import im.vector.app.core.extensions.localDateTime
import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactoryParams
import im.vector.app.features.home.room.detail.timeline.item.E2EDecoration
@ -41,7 +42,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
import org.matrix.android.sdk.api.session.room.timeline.hasBeenEdited
import javax.inject.Inject
@ -123,7 +123,11 @@ class MessageInformationDataFactory @Inject constructor(
isLastFromThisSender = isLastFromThisSender,
e2eDecoration = e2eDecoration,
sendStateDecoration = sendStateDecoration,
messageType = if (event.root.isSticker()) { MessageType.MSGTYPE_STICKER_LOCAL } else { event.root.getMsgType() }
messageType = if (event.root.isSticker()) {
MessageType.MSGTYPE_STICKER_LOCAL
} else {
event.root.getMsgType()
}
)
}
@ -230,7 +234,7 @@ class MessageInformationDataFactory @Inject constructor(
EventType.KEY_VERIFICATION_DONE,
EventType.KEY_VERIFICATION_CANCEL -> true
EventType.MESSAGE -> {
event.getLastMessageContent() is MessageVerificationRequestContent
event.getVectorLastMessageContent() is MessageVerificationRequestContent
}
else -> false
}

View file

@ -16,6 +16,7 @@
package im.vector.app.features.home.room.detail.timeline.helper
import im.vector.app.features.voicebroadcast.STATE_ROOM_VOICE_BROADCAST_INFO
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
@ -24,7 +25,7 @@ object TimelineDisplayableEvents {
/**
* All types we have an item to build with. Every type not defined here will be shown as DefaultItem if forced to be shown, otherwise will be hidden.
*/
val DISPLAYABLE_TYPES = listOf(
val DISPLAYABLE_TYPES: List<String> = listOf(
EventType.MESSAGE,
EventType.STATE_ROOM_WIDGET_LEGACY,
EventType.STATE_ROOM_WIDGET,
@ -51,7 +52,11 @@ object TimelineDisplayableEvents {
EventType.STATE_ROOM_JOIN_RULES,
EventType.KEY_VERIFICATION_DONE,
EventType.KEY_VERIFICATION_CANCEL,
) + EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO + EventType.BEACON_LOCATION_DATA
STATE_ROOM_VOICE_BROADCAST_INFO,
) +
EventType.POLL_START +
EventType.STATE_ROOM_BEACON_INFO +
EventType.BEACON_LOCATION_DATA
}
fun TimelineEvent.isRoomConfiguration(roomCreatorUserId: String?): Boolean {

View file

@ -0,0 +1,70 @@
/*
* 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
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
@EpoxyModelClass
abstract class MessageVoiceBroadcastItem : AbsMessageItem<MessageVoiceBroadcastItem.Holder>() {
@EpoxyAttribute
var callback: TimelineEventController.Callback? = null
@EpoxyAttribute
var voiceBroadcastState: VoiceBroadcastState? = null
override fun bind(holder: Holder) {
super.bind(holder)
bindVoiceBroadcastItem(holder)
}
@SuppressLint("SetTextI18n") // Temporary text
private fun bindVoiceBroadcastItem(holder: Holder) {
with(holder) {
currentStateText.text = "Voice Broadcast state: ${voiceBroadcastState?.value ?: "None"}"
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(RoomDetailAction.VoiceBroadcastAction.Resume) }
pauseButton.setOnClickListener { attributes.callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Pause) }
stopButton.setOnClickListener { attributes.callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Stop) }
}
}
override fun getViewStubId() = STUB_ID
class Holder : AbsMessageLocationItem.Holder(STUB_ID) {
val currentStateText by bind<TextView>(R.id.currentStateText)
val playButton by bind<ImageButton>(R.id.playButton)
val pauseButton by bind<ImageButton>(R.id.pauseButton)
val stopButton by bind<ImageButton>(R.id.stopButton)
}
companion object {
private val STUB_ID = R.id.messageVoiceBroadcastStub
}
}

View file

@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail.timeline.style
import android.content.res.Resources
import im.vector.app.R
import im.vector.app.core.extensions.getVectorLastMessageContent
import im.vector.app.core.extensions.localDateTime
import im.vector.app.core.resources.LocaleProvider
import im.vector.app.core.resources.isRTL
@ -29,7 +30,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
import org.matrix.android.sdk.api.session.room.timeline.isEdition
import org.matrix.android.sdk.api.session.room.timeline.isRootThread
import javax.inject.Inject
@ -126,7 +126,7 @@ class TimelineMessageLayoutFactory @Inject constructor(
isLastFromThisSender = isLastFromThisSender
)
val messageContent = event.getLastMessageContent()
val messageContent = event.getVectorLastMessageContent()
TimelineMessageLayout.Bubble(
showAvatar = showInformation && !isSentByMe,
showDisplayName = showInformation && !isSentByMe,
@ -167,7 +167,7 @@ class TimelineMessageLayoutFactory @Inject constructor(
private fun TimelineEvent.shouldBuildBubbleLayout(): Boolean {
val type = root.getClearType()
if (type in EVENT_TYPES_WITH_BUBBLE_LAYOUT) {
val messageContent = getLastMessageContent()
val messageContent = getVectorLastMessageContent()
return messageContent?.msgType !in MSG_TYPES_WITHOUT_BUBBLE_LAYOUT
}
return false
@ -212,7 +212,7 @@ class TimelineMessageLayoutFactory @Inject constructor(
EventType.KEY_VERIFICATION_DONE,
EventType.KEY_VERIFICATION_CANCEL -> true
EventType.MESSAGE -> {
event.getLastMessageContent() is MessageVerificationRequestContent
event.getVectorLastMessageContent() is MessageVerificationRequestContent
}
else -> false
}

View file

@ -17,6 +17,7 @@ package im.vector.app.features.notifications
import android.net.Uri
import im.vector.app.R
import im.vector.app.core.extensions.getVectorLastMessageContent
import im.vector.app.core.extensions.takeAs
import im.vector.app.core.resources.BuildMeta
import im.vector.app.core.resources.StringProvider
@ -45,7 +46,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachme
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.getEditedEventId
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
import org.matrix.android.sdk.api.util.toMatrixItem
import timber.log.Timber
import java.util.UUID
@ -231,7 +231,7 @@ class NotifiableEventResolver @Inject constructor(
private suspend fun TimelineEvent.downloadAndExportImage(session: Session): Uri? {
return kotlin.runCatching {
getLastMessageContent()?.takeAs<MessageWithAttachmentContent>()?.let { imageMessage ->
getVectorLastMessageContent()?.takeAs<MessageWithAttachmentContent>()?.let { imageMessage ->
val fileService = session.fileService()
fileService.downloadFile(imageMessage)
fileService.getTemporarySharableURI(imageMessage)

View file

@ -22,6 +22,7 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.extensions.getVectorLastMessageContent
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.poll.PollMode
import org.matrix.android.sdk.api.session.Session
@ -29,7 +30,6 @@ import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.getTimelineEvent
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.PollType
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
class CreatePollViewModel @AssistedInject constructor(
@Assisted private val initialState: CreatePollViewState,
@ -72,7 +72,7 @@ class CreatePollViewModel @AssistedInject constructor(
private fun initializeEditedPoll(eventId: String) {
val event = room.getTimelineEvent(eventId) ?: return
val content = event.getLastMessageContent() as? MessagePollContent ?: return
val content = event.getVectorLastMessageContent() as? MessagePollContent ?: return
val pollCreationInfo = content.getBestPollCreationInfo()
val pollType = pollCreationInfo?.kind ?: PollType.DISCLOSED_UNSTABLE

View file

@ -0,0 +1,20 @@
/*
* 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
/** Voice Broadcast State Event. */
const val STATE_ROOM_VOICE_BROADCAST_INFO = "io.element.voice_broadcast_info"

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2021 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
import im.vector.app.features.voicebroadcast.usecase.PauseVoiceBroadcastUseCase
import im.vector.app.features.voicebroadcast.usecase.ResumeVoiceBroadcastUseCase
import im.vector.app.features.voicebroadcast.usecase.StartVoiceBroadcastUseCase
import im.vector.app.features.voicebroadcast.usecase.StopVoiceBroadcastUseCase
import javax.inject.Inject
/**
* Helper class to record voice broadcast.
*/
class VoiceBroadcastHelper @Inject constructor(
private val startVoiceBroadcastUseCase: StartVoiceBroadcastUseCase,
private val pauseVoiceBroadcastUseCase: PauseVoiceBroadcastUseCase,
private val resumeVoiceBroadcastUseCase: ResumeVoiceBroadcastUseCase,
private val stopVoiceBroadcastUseCase: StopVoiceBroadcastUseCase,
) {
suspend fun startVoiceBroadcast(roomId: String) = startVoiceBroadcastUseCase.execute(roomId)
suspend fun pauseVoiceBroadcast(roomId: String) = pauseVoiceBroadcastUseCase.execute(roomId)
suspend fun resumeVoiceBroadcast(roomId: String) = resumeVoiceBroadcastUseCase.execute(roomId)
suspend fun stopVoiceBroadcast(roomId: String) = stopVoiceBroadcastUseCase.execute(roomId)
}

View file

@ -0,0 +1,53 @@
/*
* 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.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.app.features.voicebroadcast.STATE_ROOM_VOICE_BROADCAST_INFO
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType.MSGTYPE_VOICE_BROADCAST_INFO
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
import timber.log.Timber
/**
* Content of the state event of type [STATE_ROOM_VOICE_BROADCAST_INFO].
*
* It contains general info related to a voice broadcast.
*/
@JsonClass(generateAdapter = true)
data class MessageVoiceBroadcastInfoContent(
/** Local message type, not from server. */
@Transient override val msgType: String = MSGTYPE_VOICE_BROADCAST_INFO,
@Json(name = "body") override val body: String = "",
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null,
/** The [VoiceBroadcastState] value. **/
@Json(name = "state") val voiceBroadcastStateStr: String = "",
/** The length of the voice chunks in seconds. **/
@Json(name = "chunk_length") val chunkLength: Long? = null,
) : MessageContent {
val voiceBroadcastState: VoiceBroadcastState? = VoiceBroadcastState.values()
.find { it.value == voiceBroadcastStateStr }
?: run {
Timber.w("Invalid value for state: `$voiceBroadcastStateStr`")
null
}
}

View file

@ -0,0 +1,55 @@
/*
* 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.model
import im.vector.app.features.voicebroadcast.STATE_ROOM_VOICE_BROADCAST_INFO
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
/**
* [Event] wrapper for [STATE_ROOM_VOICE_BROADCAST_INFO] event type.
* Provides additional fields and functions related to voice broadcast.
*/
@JvmInline
value class VoiceBroadcastEvent(val root: Event) {
/**
* Reference on the initial voice broadcast state event (ie. with [MessageVoiceBroadcastInfoContent.voiceBroadcastState]=[VoiceBroadcastState.STARTED]).
*/
val reference: RelationDefaultContent?
get() {
val voiceBroadcastInfoContent = root.content.toModel<MessageVoiceBroadcastInfoContent>()
return if (voiceBroadcastInfoContent?.voiceBroadcastState == VoiceBroadcastState.STARTED) {
RelationDefaultContent(RelationType.REFERENCE, root.eventId)
} else {
voiceBroadcastInfoContent?.relatesTo
}
}
/**
* The mapped [MessageVoiceBroadcastInfoContent] model of the event content.
*/
val content: MessageVoiceBroadcastInfoContent?
get() = root.content.toModel()
}
/**
* Map a [STATE_ROOM_VOICE_BROADCAST_INFO] state event to a [VoiceBroadcastEvent].
*/
fun Event.asVoiceBroadcastEvent() = if (type == STATE_ROOM_VOICE_BROADCAST_INFO) VoiceBroadcastEvent(this) else null

View file

@ -0,0 +1,46 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Ref: https://github.com/vector-im/element-meta/discussions/632
*/
@JsonClass(generateAdapter = false)
enum class VoiceBroadcastState(val value: String) {
/**
* The voice broadcast had been started and is currently being live.
*/
@Json(name = "started") STARTED("started"),
/**
* The voice broadcast has been paused and may be resumed at any time by the recorder.
*/
@Json(name = "paused") PAUSED("paused"),
/**
* The voice broadcast is currently being live again.
*/
@Json(name = "resumed") RESUMED("resumed"),
/**
* The voice broadcast has ended.
*/
@Json(name = "stopped") STOPPED("stopped"),
}

View file

@ -0,0 +1,65 @@
/*
* 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.usecase
import im.vector.app.features.voicebroadcast.STATE_ROOM_VOICE_BROADCAST_INFO
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.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
import timber.log.Timber
import javax.inject.Inject
class PauseVoiceBroadcastUseCase @Inject constructor(
private val session: Session,
) {
suspend fun execute(roomId: String): Result<Unit> = runCatching {
val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId")
Timber.d("## PauseVoiceBroadcastUseCase: Pause voice broadcast requested")
val lastVoiceBroadcastEvent = room.stateService().getStateEvent(
STATE_ROOM_VOICE_BROADCAST_INFO,
QueryStringValue.Equals(session.myUserId)
)?.asVoiceBroadcastEvent()
when (val voiceBroadcastState = lastVoiceBroadcastEvent?.content?.voiceBroadcastState) {
VoiceBroadcastState.STARTED,
VoiceBroadcastState.RESUMED -> pauseVoiceBroadcast(room, lastVoiceBroadcastEvent.reference)
else -> Timber.d("## PauseVoiceBroadcastUseCase: Cannot pause voice broadcast: currentState=$voiceBroadcastState")
}
}
private suspend fun pauseVoiceBroadcast(room: Room, reference: RelationDefaultContent?) {
Timber.d("## PauseVoiceBroadcastUseCase: Send new voice broadcast info state event")
room.stateService().sendStateEvent(
eventType = STATE_ROOM_VOICE_BROADCAST_INFO,
stateKey = session.myUserId,
body = MessageVoiceBroadcastInfoContent(
relatesTo = reference,
voiceBroadcastStateStr = VoiceBroadcastState.PAUSED.value,
).toContent(),
)
// TODO pause recording audio files
}
}

View file

@ -0,0 +1,70 @@
/*
* 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.usecase
import im.vector.app.features.voicebroadcast.STATE_ROOM_VOICE_BROADCAST_INFO
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.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
import timber.log.Timber
import javax.inject.Inject
class ResumeVoiceBroadcastUseCase @Inject constructor(
private val session: Session,
) {
suspend fun execute(roomId: String): Result<Unit> = runCatching {
val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId")
Timber.d("## ResumeVoiceBroadcastUseCase: Resume voice broadcast requested")
val lastVoiceBroadcastEvent = room.stateService().getStateEvent(
STATE_ROOM_VOICE_BROADCAST_INFO,
QueryStringValue.Equals(session.myUserId)
)?.asVoiceBroadcastEvent()
when (val voiceBroadcastState = lastVoiceBroadcastEvent?.content?.voiceBroadcastState) {
VoiceBroadcastState.PAUSED -> resumeVoiceBroadcast(room, lastVoiceBroadcastEvent.reference)
else -> Timber.d("## ResumeVoiceBroadcastUseCase: Cannot resume voice broadcast: currentState=$voiceBroadcastState")
}
}
/**
* Resume a paused voice broadcast in the given room.
*
* @param room the room related to the voice broadcast
* @param reference reference on the initial voice broadcast state event (ie. state=STARTED)
*/
private suspend fun resumeVoiceBroadcast(room: Room, reference: RelationDefaultContent?) {
Timber.d("## ResumeVoiceBroadcastUseCase: Send new voice broadcast info state event")
room.stateService().sendStateEvent(
eventType = STATE_ROOM_VOICE_BROADCAST_INFO,
stateKey = session.myUserId,
body = MessageVoiceBroadcastInfoContent(
relatesTo = reference,
voiceBroadcastStateStr = VoiceBroadcastState.RESUMED.value,
).toContent(),
)
// TODO resume recording audio files
}
}

View file

@ -0,0 +1,67 @@
/*
* 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.usecase
import im.vector.app.features.voicebroadcast.STATE_ROOM_VOICE_BROADCAST_INFO
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.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.Room
import timber.log.Timber
import javax.inject.Inject
class StartVoiceBroadcastUseCase @Inject constructor(
private val session: Session,
) {
suspend fun execute(roomId: String): Result<Unit> = runCatching {
val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId")
Timber.d("## StartVoiceBroadcastUseCase: Start voice broadcast requested")
val onGoingVoiceBroadcastEvents = room.stateService().getStateEvents(
setOf(STATE_ROOM_VOICE_BROADCAST_INFO),
QueryStringValue.IsNotEmpty
)
.mapNotNull { it.asVoiceBroadcastEvent() }
.filter { it.content?.voiceBroadcastState != VoiceBroadcastState.STOPPED }
if (onGoingVoiceBroadcastEvents.isEmpty()) {
startVoiceBroadcast(room)
} else {
Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: currentVoiceBroadcastEvents=$onGoingVoiceBroadcastEvents")
}
}
private suspend fun startVoiceBroadcast(room: Room) {
Timber.d("## StartVoiceBroadcastUseCase: Send new voice broadcast info state event")
room.stateService().sendStateEvent(
eventType = STATE_ROOM_VOICE_BROADCAST_INFO,
stateKey = session.myUserId,
body = MessageVoiceBroadcastInfoContent(
voiceBroadcastStateStr = VoiceBroadcastState.STARTED.value,
chunkLength = 5L, // TODO Get length from voice broadcast settings
).toContent()
)
// TODO start recording audio files
}
}

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.usecase
import im.vector.app.features.voicebroadcast.STATE_ROOM_VOICE_BROADCAST_INFO
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.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
import timber.log.Timber
import javax.inject.Inject
class StopVoiceBroadcastUseCase @Inject constructor(
private val session: Session,
) {
suspend fun execute(roomId: String): Result<Unit> = runCatching {
val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId")
Timber.d("## StopVoiceBroadcastUseCase: Stop voice broadcast requested")
val lastVoiceBroadcastEvent = room.stateService().getStateEvent(
STATE_ROOM_VOICE_BROADCAST_INFO,
QueryStringValue.Equals(session.myUserId)
)?.asVoiceBroadcastEvent()
when (val voiceBroadcastState = lastVoiceBroadcastEvent?.content?.voiceBroadcastState) {
VoiceBroadcastState.STARTED,
VoiceBroadcastState.PAUSED,
VoiceBroadcastState.RESUMED -> stopVoiceBroadcast(room, lastVoiceBroadcastEvent.reference)
else -> Timber.d("## StopVoiceBroadcastUseCase: Cannot stop voice broadcast: currentState=$voiceBroadcastState")
}
}
private suspend fun stopVoiceBroadcast(room: Room, reference: RelationDefaultContent?) {
Timber.d("## StopVoiceBroadcastUseCase: Send new voice broadcast info state event")
room.stateService().sendStateEvent(
eventType = STATE_ROOM_VOICE_BROADCAST_INFO,
stateKey = session.myUserId,
body = MessageVoiceBroadcastInfoContent(
relatesTo = reference,
voiceBroadcastStateStr = VoiceBroadcastState.STOPPED.value,
).toContent(),
)
// TODO stop recording audio files
}
}

View file

@ -47,6 +47,13 @@
android:layout="@layout/item_timeline_event_audio_stub"
tools:visibility="gone" />
<ViewStub
android:id="@+id/messageVoiceBroadcastStub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/item_timeline_event_voice_broadcast_stub"
tools:visibility="gone" />
<ViewStub
android:id="@+id/messageContentPollStub"
android:layout_width="match_parent"

View file

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/messageRootLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/layout_vertical_margin"
tools:viewBindingIgnore="true">
<TextView
android:id="@+id/currentStateText"
style="@style/Widget.Vector.TextView.HeadlineMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@id/playButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Voice Broadcast state: STARTED" />
<ImageButton
android:id="@+id/playButton"
android:layout_width="@dimen/item_event_message_media_button_size"
android:layout_height="@dimen/item_event_message_media_button_size"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/a11y_play_voice_message"
android:src="@drawable/ic_play_pause_play"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/pauseButton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/currentStateText"
app:tint="@color/vector_content_primary_tint_selector" />
<ImageButton
android:id="@+id/pauseButton"
android:layout_width="@dimen/item_event_message_media_button_size"
android:layout_height="@dimen/item_event_message_media_button_size"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/a11y_pause_voice_message"
android:src="@drawable/ic_play_pause_pause"
app:layout_constraintBottom_toBottomOf="@id/playButton"
app:layout_constraintEnd_toStartOf="@id/stopButton"
app:layout_constraintStart_toEndOf="@id/playButton"
app:layout_constraintTop_toTopOf="@id/playButton"
app:tint="@color/vector_content_primary_tint_selector" />
<ImageButton
android:id="@+id/stopButton"
android:layout_width="@dimen/item_event_message_media_button_size"
android:layout_height="@dimen/item_event_message_media_button_size"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/a11y_stop_voice_message"
android:src="@drawable/ic_close_24dp"
app:layout_constraintBottom_toBottomOf="@id/pauseButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/pauseButton"
app:layout_constraintTop_toTopOf="@id/playButton"
app:tint="@color/vector_content_primary_tint_selector" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -16,6 +16,7 @@
package im.vector.app.features.home.room.detail.timeline.action
import im.vector.app.features.voicebroadcast.STATE_ROOM_VOICE_BROADCAST_INFO
import im.vector.app.test.fakes.FakeActiveSessionHolder
import io.mockk.mockk
import org.amshove.kluent.shouldBe
@ -34,7 +35,7 @@ class CheckIfCanRedactEventUseCaseTest {
@Test
fun `given an event which can be redacted and owned by user when use case executes then the result is true`() {
val canRedactEventTypes = listOf(EventType.MESSAGE, EventType.STICKER) +
val canRedactEventTypes = listOf(EventType.MESSAGE, EventType.STICKER, STATE_ROOM_VOICE_BROADCAST_INFO) +
EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO
canRedactEventTypes.forEach { eventType ->

View file

@ -0,0 +1,123 @@
/*
* 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.model
import im.vector.app.features.voicebroadcast.STATE_ROOM_VOICE_BROADCAST_INFO
import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldBeNull
import org.amshove.kluent.shouldNotBeNull
import org.junit.Test
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.room.model.message.AudioInfo
import org.matrix.android.sdk.api.session.room.model.message.AudioWaveformInfo
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
import org.matrix.android.sdk.api.session.room.model.relation.ReplyToContent
private const val AN_EVENT_ID = "event_id"
private const val A_REFERENCED_EVENT_ID = "event_id_ref"
private const val A_CHUNK_LENGTH = 3_600L
class VoiceBroadcastEventTest {
@Test
fun `given a started Voice Broadcast Event, when mapping to VoiceBroadcastEvent, then return expected object`() {
// Given
val content = MessageVoiceBroadcastInfoContent(
voiceBroadcastStateStr = VoiceBroadcastState.STARTED.value,
chunkLength = A_CHUNK_LENGTH,
relatesTo = RelationDefaultContent(RelationType.REFERENCE, A_REFERENCED_EVENT_ID),
)
val event = Event(
eventId = AN_EVENT_ID,
type = STATE_ROOM_VOICE_BROADCAST_INFO,
content = content.toContent(),
)
val expectedReference = RelationDefaultContent(RelationType.REFERENCE, event.eventId)
// When
val result = event.asVoiceBroadcastEvent()
// Then
result.shouldNotBeNull()
result.content shouldBeEqualTo content
result.reference shouldBeEqualTo expectedReference
}
@Test
fun `given a not started Voice Broadcast Event, when mapping to VoiceBroadcastEvent, then return expected object`() {
// Given
val content = MessageVoiceBroadcastInfoContent(
voiceBroadcastStateStr = VoiceBroadcastState.PAUSED.value,
chunkLength = A_CHUNK_LENGTH,
relatesTo = RelationDefaultContent(RelationType.REFERENCE, A_REFERENCED_EVENT_ID),
)
val event = Event(
type = STATE_ROOM_VOICE_BROADCAST_INFO,
content = content.toContent(),
)
val expectedReference = content.relatesTo
// When
val result = event.asVoiceBroadcastEvent()
// Then
result.shouldNotBeNull()
result.content shouldBeEqualTo content
result.reference shouldBeEqualTo expectedReference
}
@Test
fun `given a non Voice Broadcast Event, when mapping to VoiceBroadcastEvent, then return null`() {
// Given
val content = MessageAudioContent(
msgType = MessageType.MSGTYPE_AUDIO,
body = "audio",
audioInfo = AudioInfo(
duration = 300,
mimeType = "",
size = 500L
),
url = "a_url",
audioWaveformInfo = AudioWaveformInfo(
duration = 300,
waveform = null
),
voiceMessageIndicator = emptyMap(),
relatesTo = RelationDefaultContent(
type = RelationType.THREAD,
eventId = AN_EVENT_ID,
isFallingBack = true,
inReplyTo = ReplyToContent(eventId = A_REFERENCED_EVENT_ID)
)
)
val event = Event(
type = EventType.MESSAGE,
content = content.toContent(),
)
// When
val result = event.asVoiceBroadcastEvent()
// Then
result.shouldBeNull()
}
}

View file

@ -0,0 +1,129 @@
/*
* 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.usecase
import im.vector.app.features.voicebroadcast.STATE_ROOM_VOICE_BROADCAST_INFO
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
import im.vector.app.test.fakes.FakeRoom
import im.vector.app.test.fakes.FakeRoomService
import im.vector.app.test.fakes.FakeSession
import io.mockk.clearAllMocks
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.slot
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBe
import org.junit.Test
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
private const val A_ROOM_ID = "room_id"
private const val AN_EVENT_ID = "event_id"
private const val A_STARTED_VOICE_BROADCAST_EVENT_ID = "a_started_voice_broadcast_event_id"
class PauseVoiceBroadcastUseCaseTest {
private val fakeRoom = FakeRoom()
private val fakeSession = FakeSession(fakeRoomService = FakeRoomService(fakeRoom))
private val pauseVoiceBroadcastUseCase = PauseVoiceBroadcastUseCase(fakeSession)
@Test
fun `given a room id with a potential existing voice broadcast state when calling execute then the voice broadcast is paused or not`() = runTest {
val cases = listOf<VoiceBroadcastState?>(null).plus(VoiceBroadcastState.values()).map {
when (it) {
VoiceBroadcastState.STARTED,
VoiceBroadcastState.RESUMED -> Case(it, true)
VoiceBroadcastState.STOPPED,
VoiceBroadcastState.PAUSED,
null -> Case(it, false)
}
}
cases.forEach { case ->
if (case.canPauseVoiceBroadcast) {
testVoiceBroadcastPaused(case.previousState)
} else {
testVoiceBroadcastNotPaused(case.previousState)
}
}
}
private suspend fun testVoiceBroadcastPaused(previousState: VoiceBroadcastState?) {
// Given
clearAllMocks()
givenAVoiceBroadcastState(previousState)
val voiceBroadcastInfoContentInterceptor = slot<Content>()
coEvery { fakeRoom.stateService().sendStateEvent(any(), any(), capture(voiceBroadcastInfoContentInterceptor)) } coAnswers { AN_EVENT_ID }
// When
pauseVoiceBroadcastUseCase.execute(A_ROOM_ID)
// Then
coVerify {
fakeRoom.stateService().sendStateEvent(
eventType = STATE_ROOM_VOICE_BROADCAST_INFO,
stateKey = fakeSession.myUserId,
body = any(),
)
}
val voiceBroadcastInfoContent = voiceBroadcastInfoContentInterceptor.captured.toModel<MessageVoiceBroadcastInfoContent>()
voiceBroadcastInfoContent?.voiceBroadcastState shouldBe VoiceBroadcastState.PAUSED
voiceBroadcastInfoContent?.relatesTo?.type shouldBe RelationType.REFERENCE
voiceBroadcastInfoContent?.relatesTo?.eventId shouldBe A_STARTED_VOICE_BROADCAST_EVENT_ID
}
private suspend fun testVoiceBroadcastNotPaused(previousState: VoiceBroadcastState?) {
// Given
clearAllMocks()
givenAVoiceBroadcastState(previousState)
// When
pauseVoiceBroadcastUseCase.execute(A_ROOM_ID)
// Then
coVerify(exactly = 0) { fakeRoom.stateService().sendStateEvent(any(), any(), any()) }
}
private fun givenAVoiceBroadcastState(state: VoiceBroadcastState?) {
val relatesTo = when (state) {
VoiceBroadcastState.STARTED,
null -> null
VoiceBroadcastState.PAUSED,
VoiceBroadcastState.RESUMED,
VoiceBroadcastState.STOPPED -> RelationDefaultContent(RelationType.REFERENCE, A_STARTED_VOICE_BROADCAST_EVENT_ID)
}
val event = state?.let {
Event(
eventId = if (state == VoiceBroadcastState.STARTED) A_STARTED_VOICE_BROADCAST_EVENT_ID else AN_EVENT_ID,
type = STATE_ROOM_VOICE_BROADCAST_INFO,
stateKey = fakeSession.myUserId,
content = MessageVoiceBroadcastInfoContent(
voiceBroadcastStateStr = state.value,
relatesTo = relatesTo
).toContent()
)
}
fakeRoom.stateService().givenGetStateEvent(event)
}
private data class Case(val previousState: VoiceBroadcastState?, val canPauseVoiceBroadcast: Boolean)
}

View file

@ -0,0 +1,129 @@
/*
* 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.usecase
import im.vector.app.features.voicebroadcast.STATE_ROOM_VOICE_BROADCAST_INFO
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
import im.vector.app.test.fakes.FakeRoom
import im.vector.app.test.fakes.FakeRoomService
import im.vector.app.test.fakes.FakeSession
import io.mockk.clearAllMocks
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.slot
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBe
import org.junit.Test
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
private const val A_ROOM_ID = "room_id"
private const val AN_EVENT_ID = "event_id"
private const val A_STARTED_VOICE_BROADCAST_EVENT_ID = "a_started_voice_broadcast_event_id"
class ResumeVoiceBroadcastUseCaseTest {
private val fakeRoom = FakeRoom()
private val fakeSession = FakeSession(fakeRoomService = FakeRoomService(fakeRoom))
private val resumeVoiceBroadcastUseCase = ResumeVoiceBroadcastUseCase(fakeSession)
@Test
fun `given a room id with a potential existing voice broadcast state when calling execute then the voice broadcast is resumed or not`() = runTest {
val cases = listOf<VoiceBroadcastState?>(null).plus(VoiceBroadcastState.values()).map {
when (it) {
VoiceBroadcastState.PAUSED -> Case(it, true)
VoiceBroadcastState.STARTED,
VoiceBroadcastState.RESUMED,
VoiceBroadcastState.STOPPED,
null -> Case(it, false)
}
}
cases.forEach { case ->
if (case.canResumeVoiceBroadcast) {
testVoiceBroadcastResumed(case.previousState)
} else {
testVoiceBroadcastNotResumed(case.previousState)
}
}
}
private suspend fun testVoiceBroadcastResumed(previousState: VoiceBroadcastState?) {
// Given
clearAllMocks()
givenAVoiceBroadcastState(previousState)
val voiceBroadcastInfoContentInterceptor = slot<Content>()
coEvery { fakeRoom.stateService().sendStateEvent(any(), any(), capture(voiceBroadcastInfoContentInterceptor)) } coAnswers { AN_EVENT_ID }
// When
resumeVoiceBroadcastUseCase.execute(A_ROOM_ID)
// Then
coVerify {
fakeRoom.stateService().sendStateEvent(
eventType = STATE_ROOM_VOICE_BROADCAST_INFO,
stateKey = fakeSession.myUserId,
body = any(),
)
}
val voiceBroadcastInfoContent = voiceBroadcastInfoContentInterceptor.captured.toModel<MessageVoiceBroadcastInfoContent>()
voiceBroadcastInfoContent?.voiceBroadcastState shouldBe VoiceBroadcastState.RESUMED
voiceBroadcastInfoContent?.relatesTo?.type shouldBe RelationType.REFERENCE
voiceBroadcastInfoContent?.relatesTo?.eventId shouldBe A_STARTED_VOICE_BROADCAST_EVENT_ID
}
private suspend fun testVoiceBroadcastNotResumed(previousState: VoiceBroadcastState?) {
// Given
clearAllMocks()
givenAVoiceBroadcastState(previousState)
// When
resumeVoiceBroadcastUseCase.execute(A_ROOM_ID)
// Then
coVerify(exactly = 0) { fakeRoom.stateService().sendStateEvent(any(), any(), any()) }
}
private fun givenAVoiceBroadcastState(state: VoiceBroadcastState?) {
val relatesTo = when (state) {
VoiceBroadcastState.STARTED,
null -> null
VoiceBroadcastState.PAUSED,
VoiceBroadcastState.RESUMED,
VoiceBroadcastState.STOPPED -> RelationDefaultContent(RelationType.REFERENCE, A_STARTED_VOICE_BROADCAST_EVENT_ID)
}
val event = state?.let {
Event(
eventId = if (state == VoiceBroadcastState.STARTED) A_STARTED_VOICE_BROADCAST_EVENT_ID else AN_EVENT_ID,
type = STATE_ROOM_VOICE_BROADCAST_INFO,
stateKey = fakeSession.myUserId,
content = MessageVoiceBroadcastInfoContent(
voiceBroadcastStateStr = state.value,
relatesTo = relatesTo
).toContent()
)
}
fakeRoom.stateService().givenGetStateEvent(event)
}
private data class Case(val previousState: VoiceBroadcastState?, val canResumeVoiceBroadcast: Boolean)
}

View file

@ -0,0 +1,121 @@
/*
* 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.usecase
import im.vector.app.features.voicebroadcast.STATE_ROOM_VOICE_BROADCAST_INFO
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
import im.vector.app.test.fakes.FakeRoom
import im.vector.app.test.fakes.FakeRoomService
import im.vector.app.test.fakes.FakeSession
import io.mockk.clearAllMocks
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.slot
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBe
import org.amshove.kluent.shouldBeNull
import org.junit.Test
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
private const val A_ROOM_ID = "room_id"
private const val AN_EVENT_ID = "event_id"
private const val A_USER_ID = "user_id"
class StartVoiceBroadcastUseCaseTest {
private val fakeRoom = FakeRoom()
private val fakeSession = FakeSession(fakeRoomService = FakeRoomService(fakeRoom))
private val startVoiceBroadcastUseCase = StartVoiceBroadcastUseCase(fakeSession)
@Test
fun `given a room id with potential several existing voice broadcast states when calling execute then the voice broadcast is started or not`() = runTest {
val cases = VoiceBroadcastState.values()
.flatMap { first ->
VoiceBroadcastState.values().map { second ->
Case(
voiceBroadcasts = listOf(VoiceBroadcast(fakeSession.myUserId, first), VoiceBroadcast(A_USER_ID, second)),
canStartVoiceBroadcast = first == VoiceBroadcastState.STOPPED && second == VoiceBroadcastState.STOPPED
)
}
}
.plus(Case(emptyList(), true))
cases.forEach { case ->
if (case.canStartVoiceBroadcast) {
testVoiceBroadcastStarted(case.voiceBroadcasts)
} else {
testVoiceBroadcastNotStarted(case.voiceBroadcasts)
}
}
}
private suspend fun testVoiceBroadcastStarted(voiceBroadcasts: List<VoiceBroadcast>) {
// Given
clearAllMocks()
givenAVoiceBroadcasts(voiceBroadcasts)
val voiceBroadcastInfoContentInterceptor = slot<Content>()
coEvery { fakeRoom.stateService().sendStateEvent(any(), any(), capture(voiceBroadcastInfoContentInterceptor)) } coAnswers { AN_EVENT_ID }
// When
startVoiceBroadcastUseCase.execute(A_ROOM_ID)
// Then
coVerify {
fakeRoom.stateService().sendStateEvent(
eventType = STATE_ROOM_VOICE_BROADCAST_INFO,
stateKey = fakeSession.myUserId,
body = any(),
)
}
val voiceBroadcastInfoContent = voiceBroadcastInfoContentInterceptor.captured.toModel<MessageVoiceBroadcastInfoContent>()
voiceBroadcastInfoContent?.voiceBroadcastState shouldBe VoiceBroadcastState.STARTED
voiceBroadcastInfoContent?.relatesTo.shouldBeNull()
}
private suspend fun testVoiceBroadcastNotStarted(voiceBroadcasts: List<VoiceBroadcast>) {
// Given
clearAllMocks()
givenAVoiceBroadcasts(voiceBroadcasts)
// When
startVoiceBroadcastUseCase.execute(A_ROOM_ID)
// Then
coVerify(exactly = 0) { fakeRoom.stateService().sendStateEvent(any(), any(), any()) }
}
private fun givenAVoiceBroadcasts(voiceBroadcasts: List<VoiceBroadcast>) {
val events = voiceBroadcasts.map {
Event(
type = STATE_ROOM_VOICE_BROADCAST_INFO,
stateKey = it.userId,
content = MessageVoiceBroadcastInfoContent(
voiceBroadcastStateStr = it.state.value
).toContent()
)
}
fakeRoom.stateService().givenGetStateEvents(QueryStringValue.IsNotEmpty, events)
}
private data class VoiceBroadcast(val userId: String, val state: VoiceBroadcastState)
private data class Case(val voiceBroadcasts: List<VoiceBroadcast>, val canStartVoiceBroadcast: Boolean)
}

View file

@ -0,0 +1,129 @@
/*
* 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.usecase
import im.vector.app.features.voicebroadcast.STATE_ROOM_VOICE_BROADCAST_INFO
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
import im.vector.app.test.fakes.FakeRoom
import im.vector.app.test.fakes.FakeRoomService
import im.vector.app.test.fakes.FakeSession
import io.mockk.clearAllMocks
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.slot
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBe
import org.junit.Test
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
private const val A_ROOM_ID = "room_id"
private const val AN_EVENT_ID = "event_id"
private const val A_STARTED_VOICE_BROADCAST_EVENT_ID = "a_started_voice_broadcast_event_id"
class StopVoiceBroadcastUseCaseTest {
private val fakeRoom = FakeRoom()
private val fakeSession = FakeSession(fakeRoomService = FakeRoomService(fakeRoom))
private val stopVoiceBroadcastUseCase = StopVoiceBroadcastUseCase(fakeSession)
@Test
fun `given a room id with a potential existing voice broadcast state when calling execute then the voice broadcast is stopped or not`() = runTest {
val cases = listOf<VoiceBroadcastState?>(null).plus(VoiceBroadcastState.values()).map {
when (it) {
VoiceBroadcastState.STARTED,
VoiceBroadcastState.RESUMED,
VoiceBroadcastState.PAUSED -> Case(it, true)
VoiceBroadcastState.STOPPED,
null -> Case(it, false)
}
}
cases.forEach { case ->
if (case.canStopVoiceBroadcast) {
testVoiceBroadcastStopped(case.previousState)
} else {
testVoiceBroadcastNotStopped(case.previousState)
}
}
}
private suspend fun testVoiceBroadcastStopped(previousState: VoiceBroadcastState?) {
// Given
clearAllMocks()
givenAVoiceBroadcastState(previousState)
val voiceBroadcastInfoContentInterceptor = slot<Content>()
coEvery { fakeRoom.stateService().sendStateEvent(any(), any(), capture(voiceBroadcastInfoContentInterceptor)) } coAnswers { AN_EVENT_ID }
// When
stopVoiceBroadcastUseCase.execute(A_ROOM_ID)
// Then
coVerify {
fakeRoom.stateService().sendStateEvent(
eventType = STATE_ROOM_VOICE_BROADCAST_INFO,
stateKey = fakeSession.myUserId,
body = any(),
)
}
val voiceBroadcastInfoContent = voiceBroadcastInfoContentInterceptor.captured.toModel<MessageVoiceBroadcastInfoContent>()
voiceBroadcastInfoContent?.voiceBroadcastState shouldBe VoiceBroadcastState.STOPPED
voiceBroadcastInfoContent?.relatesTo?.type shouldBe RelationType.REFERENCE
voiceBroadcastInfoContent?.relatesTo?.eventId shouldBe A_STARTED_VOICE_BROADCAST_EVENT_ID
}
private suspend fun testVoiceBroadcastNotStopped(previousState: VoiceBroadcastState?) {
// Given
clearAllMocks()
givenAVoiceBroadcastState(previousState)
// When
stopVoiceBroadcastUseCase.execute(A_ROOM_ID)
// Then
coVerify(exactly = 0) { fakeRoom.stateService().sendStateEvent(any(), any(), any()) }
}
private fun givenAVoiceBroadcastState(state: VoiceBroadcastState?) {
val relatesTo = when (state) {
VoiceBroadcastState.STARTED,
null -> null
VoiceBroadcastState.PAUSED,
VoiceBroadcastState.RESUMED,
VoiceBroadcastState.STOPPED -> RelationDefaultContent(RelationType.REFERENCE, A_STARTED_VOICE_BROADCAST_EVENT_ID)
}
val event = state?.let {
Event(
eventId = if (state == VoiceBroadcastState.STARTED) A_STARTED_VOICE_BROADCAST_EVENT_ID else AN_EVENT_ID,
type = STATE_ROOM_VOICE_BROADCAST_INFO,
stateKey = fakeSession.myUserId,
content = MessageVoiceBroadcastInfoContent(
voiceBroadcastStateStr = state.value,
relatesTo = relatesTo
).toContent()
)
}
fakeRoom.stateService().givenGetStateEvent(event)
}
private data class Case(val previousState: VoiceBroadcastState?, val canStopVoiceBroadcast: Boolean)
}

View file

@ -24,6 +24,7 @@ class FakeRoom(
private val fakeSendService: FakeSendService = FakeSendService(),
private val fakeTimelineService: FakeTimelineService = FakeTimelineService(),
private val fakeRelationService: FakeRelationService = FakeRelationService(),
private val fakeStateService: FakeStateService = FakeStateService(),
) : Room by mockk() {
override fun locationSharingService() = fakeLocationSharingService
@ -33,4 +34,6 @@ class FakeRoom(
override fun timelineService() = fakeTimelineService
override fun relationService() = fakeRelationService
override fun stateService() = fakeStateService
}

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2021 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.test.fakes
import io.mockk.every
import io.mockk.mockk
import org.matrix.android.sdk.api.query.QueryStateEventValue
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.state.StateService
class FakeStateService : StateService by mockk(relaxed = true) {
fun givenGetStateEvents(stateKey: QueryStateEventValue, result: List<Event>) {
every { getStateEvents(any(), stateKey) } returns result
}
fun givenGetStateEvent(event: Event?) {
every { getStateEvent(any(), any()) } returns event
}
}