Merge pull request #7455 from vector-im/resilience-rc
Merge branch resilience-rc into develop
This commit is contained in:
commit
98e0397afd
1
changelog.d/7431.bugfix
Normal file
1
changelog.d/7431.bugfix
Normal file
@ -0,0 +1 @@
|
|||||||
|
[Voice Broadcast] Do not display the recorder view for a live broadcast started from another session
|
1
changelog.d/7436.feature
Normal file
1
changelog.d/7436.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
Rich text editor: add full screen mode.
|
1
changelog.d/7448.wip
Normal file
1
changelog.d/7448.wip
Normal file
@ -0,0 +1 @@
|
|||||||
|
[Voice Broadcast] Improve timeline items factory and handle bad recording state display
|
1
changelog.d/7450.wip
Normal file
1
changelog.d/7450.wip
Normal file
@ -0,0 +1 @@
|
|||||||
|
[Voice Broadcast] Stop recording when opening the room after an app restart
|
1
changelog.d/7452.feature
Normal file
1
changelog.d/7452.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
[Rich text editor] Add plain text mode
|
1
changelog.d/7478.wip
Normal file
1
changelog.d/7478.wip
Normal file
@ -0,0 +1 @@
|
|||||||
|
[Voice Broadcast] Improve playlist fetching and player codebase
|
1
changelog.d/7485.wip
Normal file
1
changelog.d/7485.wip
Normal file
@ -0,0 +1 @@
|
|||||||
|
[Voice Broadcast] Display an error dialog if the user fails to start a voice broadcast
|
1
changelog.d/7491.bugfix
Normal file
1
changelog.d/7491.bugfix
Normal file
@ -0,0 +1 @@
|
|||||||
|
Fix rich text editor textfield not growing to fill parent on full screen.
|
1
changelog.d/7502.bugfix
Normal file
1
changelog.d/7502.bugfix
Normal file
@ -0,0 +1 @@
|
|||||||
|
Voice Broadcast - Fix duplicated voice messages in the internal playlist
|
@ -98,7 +98,7 @@ ext.libs = [
|
|||||||
],
|
],
|
||||||
element : [
|
element : [
|
||||||
'opusencoder' : "io.element.android:opusencoder:1.1.0",
|
'opusencoder' : "io.element.android:opusencoder:1.1.0",
|
||||||
'wysiwyg' : "io.element.android:wysiwyg:0.2.1"
|
'wysiwyg' : "io.element.android:wysiwyg:0.4.0"
|
||||||
],
|
],
|
||||||
squareup : [
|
squareup : [
|
||||||
'moshi' : "com.squareup.moshi:moshi:$moshi",
|
'moshi' : "com.squareup.moshi:moshi:$moshi",
|
||||||
|
@ -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>
|
||||||
|
@ -3094,6 +3094,10 @@
|
|||||||
<string name="a11y_play_voice_broadcast">Play or resume voice broadcast</string>
|
<string name="a11y_play_voice_broadcast">Play or resume voice broadcast</string>
|
||||||
<string name="a11y_pause_voice_broadcast">Pause voice broadcast</string>
|
<string name="a11y_pause_voice_broadcast">Pause voice broadcast</string>
|
||||||
<string name="a11y_voice_broadcast_buffering">Buffering</string>
|
<string name="a11y_voice_broadcast_buffering">Buffering</string>
|
||||||
|
<string name="error_voice_broadcast_unauthorized_title">Can’t start a new voice broadcast</string>
|
||||||
|
<string name="error_voice_broadcast_permission_denied_message">You don’t have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.</string>
|
||||||
|
<string name="error_voice_broadcast_blocked_by_someone_else_message">Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.</string>
|
||||||
|
<string name="error_voice_broadcast_already_in_progress_message">You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.</string>
|
||||||
|
|
||||||
<string name="upgrade_room_for_restricted">Anyone in %s will be able to find and join this room - no need to manually invite everyone. You’ll be able to change this in room settings anytime.</string>
|
<string name="upgrade_room_for_restricted">Anyone in %s will be able to find and join this room - no need to manually invite everyone. You’ll be able to change this in room settings anytime.</string>
|
||||||
<string name="upgrade_room_for_restricted_no_param">Anyone in a parent space will be able to find and join this room - no need to manually invite everyone. You’ll be able to change this in room settings anytime.</string>
|
<string name="upgrade_room_for_restricted_no_param">Anyone in a parent space will be able to find and join this room - no need to manually invite everyone. You’ll be able to change this in room settings anytime.</string>
|
||||||
@ -3222,6 +3226,7 @@
|
|||||||
<string name="attachment_type_selector_location">Location</string>
|
<string name="attachment_type_selector_location">Location</string>
|
||||||
<string name="attachment_type_selector_camera">Camera</string>
|
<string name="attachment_type_selector_camera">Camera</string>
|
||||||
<string name="attachment_type_selector_contact">Contact</string>
|
<string name="attachment_type_selector_contact">Contact</string>
|
||||||
|
<string name="attachment_type_selector_text_formatting">Text formatting</string>
|
||||||
|
|
||||||
<string name="message_reaction_show_less">Show less</string>
|
<string name="message_reaction_show_less">Show less</string>
|
||||||
<plurals name="message_reaction_show_more">
|
<plurals name="message_reaction_show_more">
|
||||||
@ -3442,5 +3447,6 @@
|
|||||||
<string name="rich_text_editor_format_italic">Apply italic format</string>
|
<string name="rich_text_editor_format_italic">Apply italic format</string>
|
||||||
<string name="rich_text_editor_format_strikethrough">Apply strikethrough format</string>
|
<string name="rich_text_editor_format_strikethrough">Apply strikethrough format</string>
|
||||||
<string name="rich_text_editor_format_underline">Apply underline format</string>
|
<string name="rich_text_editor_format_underline">Apply underline format</string>
|
||||||
|
<string name="rich_text_editor_full_screen_toggle">Toggle full screen mode</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -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>
|
@ -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>
|
@ -150,7 +150,8 @@
|
|||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".features.home.room.detail.RoomDetailActivity"
|
android:name=".features.home.room.detail.RoomDetailActivity"
|
||||||
android:parentActivityName=".features.home.HomeActivity">
|
android:parentActivityName=".features.home.HomeActivity"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value=".features.home.HomeActivity" />
|
android:value=".features.home.HomeActivity" />
|
||||||
|
@ -18,24 +18,33 @@ package im.vector.app.core.di
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
|
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
|
||||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorderQ
|
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayerImpl
|
||||||
|
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
|
||||||
|
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorderQ
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
object VoiceModule {
|
@Module
|
||||||
@Provides
|
abstract class VoiceModule {
|
||||||
@Singleton
|
|
||||||
fun providesVoiceBroadcastRecorder(context: Context): VoiceBroadcastRecorder? {
|
companion object {
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
@Provides
|
||||||
VoiceBroadcastRecorderQ(context)
|
@Singleton
|
||||||
} else {
|
fun providesVoiceBroadcastRecorder(context: Context): VoiceBroadcastRecorder? {
|
||||||
null
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
VoiceBroadcastRecorderQ(context)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindVoiceBroadcastPlayer(player: VoiceBroadcastPlayerImpl): VoiceBroadcastPlayer
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,8 @@ import im.vector.app.R
|
|||||||
import im.vector.app.core.resources.StringProvider
|
import im.vector.app.core.resources.StringProvider
|
||||||
import im.vector.app.features.call.dialpad.DialPadLookup
|
import im.vector.app.features.call.dialpad.DialPadLookup
|
||||||
import im.vector.app.features.voice.VoiceFailure
|
import im.vector.app.features.voice.VoiceFailure
|
||||||
|
import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure
|
||||||
|
import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure.RecordingError
|
||||||
import org.matrix.android.sdk.api.failure.Failure
|
import org.matrix.android.sdk.api.failure.Failure
|
||||||
import org.matrix.android.sdk.api.failure.MatrixError
|
import org.matrix.android.sdk.api.failure.MatrixError
|
||||||
import org.matrix.android.sdk.api.failure.MatrixIdFailure
|
import org.matrix.android.sdk.api.failure.MatrixIdFailure
|
||||||
@ -135,6 +137,7 @@ class DefaultErrorFormatter @Inject constructor(
|
|||||||
is MatrixIdFailure.InvalidMatrixId ->
|
is MatrixIdFailure.InvalidMatrixId ->
|
||||||
stringProvider.getString(R.string.login_signin_matrix_id_error_invalid_matrix_id)
|
stringProvider.getString(R.string.login_signin_matrix_id_error_invalid_matrix_id)
|
||||||
is VoiceFailure -> voiceMessageError(throwable)
|
is VoiceFailure -> voiceMessageError(throwable)
|
||||||
|
is VoiceBroadcastFailure -> voiceBroadcastMessageError(throwable)
|
||||||
is ActivityNotFoundException ->
|
is ActivityNotFoundException ->
|
||||||
stringProvider.getString(R.string.error_no_external_application_found)
|
stringProvider.getString(R.string.error_no_external_application_found)
|
||||||
else -> throwable.localizedMessage
|
else -> throwable.localizedMessage
|
||||||
@ -149,6 +152,14 @@ class DefaultErrorFormatter @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun voiceBroadcastMessageError(throwable: VoiceBroadcastFailure): String {
|
||||||
|
return when (throwable) {
|
||||||
|
RecordingError.BlockedBySomeoneElse -> stringProvider.getString(R.string.error_voice_broadcast_blocked_by_someone_else_message)
|
||||||
|
RecordingError.NoPermission -> stringProvider.getString(R.string.error_voice_broadcast_permission_denied_message)
|
||||||
|
RecordingError.UserAlreadyBroadcasting -> stringProvider.getString(R.string.error_voice_broadcast_already_in_progress_message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun limitExceededError(error: MatrixError): String {
|
private fun limitExceededError(error: MatrixError): String {
|
||||||
val delay = error.retryAfterMillis
|
val delay = error.retryAfterMillis
|
||||||
|
|
||||||
|
@ -29,7 +29,13 @@ import androidx.appcompat.widget.SearchView
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.graphics.drawable.DrawableCompat
|
import androidx.core.graphics.drawable.DrawableCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.transition.ChangeBounds
|
||||||
|
import androidx.transition.Fade
|
||||||
|
import androidx.transition.Transition
|
||||||
|
import androidx.transition.TransitionManager
|
||||||
|
import androidx.transition.TransitionSet
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.animations.SimpleTransitionListener
|
||||||
import im.vector.app.features.themes.ThemeUtils
|
import im.vector.app.features.themes.ThemeUtils
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -90,3 +96,18 @@ fun View.setAttributeBackground(@AttrRes attributeId: Int) {
|
|||||||
val attribute = ThemeUtils.getAttribute(context, attributeId)!!
|
val attribute = ThemeUtils.getAttribute(context, attributeId)!!
|
||||||
setBackgroundResource(attribute.resourceId)
|
setBackgroundResource(attribute.resourceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ViewGroup.animateLayoutChange(animationDuration: Long, transitionComplete: (() -> Unit)? = null) {
|
||||||
|
val transition = TransitionSet().apply {
|
||||||
|
ordering = TransitionSet.ORDERING_SEQUENTIAL
|
||||||
|
addTransition(ChangeBounds())
|
||||||
|
addTransition(Fade(Fade.IN))
|
||||||
|
duration = animationDuration
|
||||||
|
addListener(object : SimpleTransitionListener() {
|
||||||
|
override fun onTransitionEnd(transition: Transition) {
|
||||||
|
transitionComplete?.invoke()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
TransitionManager.beginDelayedTransition((parent as? ViewGroup ?: this), transition)
|
||||||
|
}
|
||||||
|
@ -23,10 +23,10 @@ import android.view.ViewGroup
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
|
||||||
import com.airbnb.mvrx.parentFragmentViewModel
|
import com.airbnb.mvrx.parentFragmentViewModel
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import im.vector.app.R
|
||||||
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
|
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
|
||||||
import im.vector.app.databinding.BottomSheetAttachmentTypeSelectorBinding
|
import im.vector.app.databinding.BottomSheetAttachmentTypeSelectorBinding
|
||||||
import im.vector.app.features.home.room.detail.TimelineViewModel
|
import im.vector.app.features.home.room.detail.TimelineViewModel
|
||||||
@ -34,7 +34,7 @@ import im.vector.app.features.home.room.detail.TimelineViewModel
|
|||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class AttachmentTypeSelectorBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetAttachmentTypeSelectorBinding>() {
|
class AttachmentTypeSelectorBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetAttachmentTypeSelectorBinding>() {
|
||||||
|
|
||||||
private val viewModel: AttachmentTypeSelectorViewModel by fragmentViewModel()
|
private val viewModel: AttachmentTypeSelectorViewModel by parentFragmentViewModel()
|
||||||
private val timelineViewModel: TimelineViewModel by parentFragmentViewModel()
|
private val timelineViewModel: TimelineViewModel by parentFragmentViewModel()
|
||||||
private val sharedActionViewModel: AttachmentTypeSelectorSharedActionViewModel by viewModels(
|
private val sharedActionViewModel: AttachmentTypeSelectorSharedActionViewModel by viewModels(
|
||||||
ownerProducer = { requireParentFragment() }
|
ownerProducer = { requireParentFragment() }
|
||||||
@ -51,6 +51,14 @@ class AttachmentTypeSelectorBottomSheet : VectorBaseBottomSheetDialogFragment<Bo
|
|||||||
views.location.isVisible = viewState.isLocationVisible
|
views.location.isVisible = viewState.isLocationVisible
|
||||||
views.voiceBroadcast.isVisible = viewState.isVoiceBroadcastVisible
|
views.voiceBroadcast.isVisible = viewState.isVoiceBroadcastVisible
|
||||||
views.poll.isVisible = !timelineState.isThreadTimeline()
|
views.poll.isVisible = !timelineState.isThreadTimeline()
|
||||||
|
views.textFormatting.isChecked = viewState.isTextFormattingEnabled
|
||||||
|
views.textFormatting.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||||
|
if (viewState.isTextFormattingEnabled) {
|
||||||
|
R.drawable.ic_text_formatting
|
||||||
|
} else {
|
||||||
|
R.drawable.ic_text_formatting_disabled
|
||||||
|
}, 0, 0, 0
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
@ -63,6 +71,7 @@ class AttachmentTypeSelectorBottomSheet : VectorBaseBottomSheetDialogFragment<Bo
|
|||||||
views.location.debouncedClicks { onAttachmentSelected(AttachmentType.LOCATION) }
|
views.location.debouncedClicks { onAttachmentSelected(AttachmentType.LOCATION) }
|
||||||
views.camera.debouncedClicks { onAttachmentSelected(AttachmentType.CAMERA) }
|
views.camera.debouncedClicks { onAttachmentSelected(AttachmentType.CAMERA) }
|
||||||
views.contact.debouncedClicks { onAttachmentSelected(AttachmentType.CONTACT) }
|
views.contact.debouncedClicks { onAttachmentSelected(AttachmentType.CONTACT) }
|
||||||
|
views.textFormatting.setOnCheckedChangeListener { _, isChecked -> onTextFormattingToggled(isChecked) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onAttachmentSelected(attachmentType: AttachmentType) {
|
private fun onAttachmentSelected(attachmentType: AttachmentType) {
|
||||||
@ -71,6 +80,9 @@ class AttachmentTypeSelectorBottomSheet : VectorBaseBottomSheetDialogFragment<Bo
|
|||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onTextFormattingToggled(isEnabled: Boolean) =
|
||||||
|
viewModel.handle(AttachmentTypeSelectorAction.ToggleTextFormatting(isEnabled))
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun show(fragmentManager: FragmentManager) {
|
fun show(fragmentManager: FragmentManager) {
|
||||||
val bottomSheet = AttachmentTypeSelectorBottomSheet()
|
val bottomSheet = AttachmentTypeSelectorBottomSheet()
|
||||||
|
@ -23,15 +23,17 @@ import dagger.assisted.AssistedFactory
|
|||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||||
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||||
import im.vector.app.core.platform.EmptyAction
|
|
||||||
import im.vector.app.core.platform.EmptyViewEvents
|
import im.vector.app.core.platform.EmptyViewEvents
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
|
import im.vector.app.core.platform.VectorViewModelAction
|
||||||
import im.vector.app.features.VectorFeatures
|
import im.vector.app.features.VectorFeatures
|
||||||
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
|
|
||||||
class AttachmentTypeSelectorViewModel @AssistedInject constructor(
|
class AttachmentTypeSelectorViewModel @AssistedInject constructor(
|
||||||
@Assisted initialState: AttachmentTypeSelectorViewState,
|
@Assisted initialState: AttachmentTypeSelectorViewState,
|
||||||
private val vectorFeatures: VectorFeatures,
|
private val vectorFeatures: VectorFeatures,
|
||||||
) : VectorViewModel<AttachmentTypeSelectorViewState, EmptyAction, EmptyViewEvents>(initialState) {
|
private val vectorPreferences: VectorPreferences,
|
||||||
|
) : VectorViewModel<AttachmentTypeSelectorViewState, AttachmentTypeSelectorAction, EmptyViewEvents>(initialState) {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory : MavericksAssistedViewModelFactory<AttachmentTypeSelectorViewModel, AttachmentTypeSelectorViewState> {
|
interface Factory : MavericksAssistedViewModelFactory<AttachmentTypeSelectorViewModel, AttachmentTypeSelectorViewState> {
|
||||||
override fun create(initialState: AttachmentTypeSelectorViewState): AttachmentTypeSelectorViewModel
|
override fun create(initialState: AttachmentTypeSelectorViewState): AttachmentTypeSelectorViewModel
|
||||||
@ -39,8 +41,8 @@ class AttachmentTypeSelectorViewModel @AssistedInject constructor(
|
|||||||
|
|
||||||
companion object : MavericksViewModelFactory<AttachmentTypeSelectorViewModel, AttachmentTypeSelectorViewState> by hiltMavericksViewModelFactory()
|
companion object : MavericksViewModelFactory<AttachmentTypeSelectorViewModel, AttachmentTypeSelectorViewState> by hiltMavericksViewModelFactory()
|
||||||
|
|
||||||
override fun handle(action: EmptyAction) {
|
override fun handle(action: AttachmentTypeSelectorAction) = when (action) {
|
||||||
// do nothing
|
is AttachmentTypeSelectorAction.ToggleTextFormatting -> setTextFormattingEnabled(action.isEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@ -48,6 +50,16 @@ class AttachmentTypeSelectorViewModel @AssistedInject constructor(
|
|||||||
copy(
|
copy(
|
||||||
isLocationVisible = vectorFeatures.isLocationSharingEnabled(),
|
isLocationVisible = vectorFeatures.isLocationSharingEnabled(),
|
||||||
isVoiceBroadcastVisible = vectorFeatures.isVoiceBroadcastEnabled(),
|
isVoiceBroadcastVisible = vectorFeatures.isVoiceBroadcastEnabled(),
|
||||||
|
isTextFormattingEnabled = vectorPreferences.isTextFormattingEnabled(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setTextFormattingEnabled(isEnabled: Boolean) {
|
||||||
|
vectorPreferences.setTextFormattingEnabled(isEnabled)
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
isTextFormattingEnabled = isEnabled
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -56,4 +68,9 @@ class AttachmentTypeSelectorViewModel @AssistedInject constructor(
|
|||||||
data class AttachmentTypeSelectorViewState(
|
data class AttachmentTypeSelectorViewState(
|
||||||
val isLocationVisible: Boolean = false,
|
val isLocationVisible: Boolean = false,
|
||||||
val isVoiceBroadcastVisible: Boolean = false,
|
val isVoiceBroadcastVisible: Boolean = false,
|
||||||
|
val isTextFormattingEnabled: Boolean = false,
|
||||||
) : MavericksState
|
) : MavericksState
|
||||||
|
|
||||||
|
sealed interface AttachmentTypeSelectorAction : VectorViewModelAction {
|
||||||
|
data class ToggleTextFormatting(val isEnabled: Boolean) : AttachmentTypeSelectorAction
|
||||||
|
}
|
||||||
|
@ -42,6 +42,7 @@ import im.vector.app.features.raw.wellknown.isSecureBackupRequired
|
|||||||
import im.vector.app.features.raw.wellknown.withElementWellKnown
|
import im.vector.app.features.raw.wellknown.withElementWellKnown
|
||||||
import im.vector.app.features.session.coroutineScope
|
import im.vector.app.features.session.coroutineScope
|
||||||
import im.vector.app.features.settings.VectorPreferences
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
|
import im.vector.app.features.voicebroadcast.recording.usecase.StopOngoingVoiceBroadcastUseCase
|
||||||
import im.vector.lib.core.utils.compat.getParcelableExtraCompat
|
import im.vector.lib.core.utils.compat.getParcelableExtraCompat
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@ -92,6 +93,7 @@ class HomeActivityViewModel @AssistedInject constructor(
|
|||||||
private val analyticsConfig: AnalyticsConfig,
|
private val analyticsConfig: AnalyticsConfig,
|
||||||
private val releaseNotesPreferencesStore: ReleaseNotesPreferencesStore,
|
private val releaseNotesPreferencesStore: ReleaseNotesPreferencesStore,
|
||||||
private val vectorFeatures: VectorFeatures,
|
private val vectorFeatures: VectorFeatures,
|
||||||
|
private val stopOngoingVoiceBroadcastUseCase: StopOngoingVoiceBroadcastUseCase,
|
||||||
) : VectorViewModel<HomeActivityViewState, HomeActivityViewActions, HomeActivityViewEvents>(initialState) {
|
) : VectorViewModel<HomeActivityViewState, HomeActivityViewActions, HomeActivityViewEvents>(initialState) {
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
@ -123,6 +125,7 @@ class HomeActivityViewModel @AssistedInject constructor(
|
|||||||
observeReleaseNotes()
|
observeReleaseNotes()
|
||||||
observeLocalNotificationsSilenced()
|
observeLocalNotificationsSilenced()
|
||||||
initThreadsMigration()
|
initThreadsMigration()
|
||||||
|
viewModelScope.launch { stopOngoingVoiceBroadcastUseCase.execute() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeReleaseNotes() = withState { state ->
|
private fun observeReleaseNotes() = withState { state ->
|
||||||
|
@ -34,6 +34,8 @@ class JumpToBottomViewVisibilityManager(
|
|||||||
private val layoutManager: LinearLayoutManager
|
private val layoutManager: LinearLayoutManager
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private var canShowButtonOnScroll = true
|
||||||
|
|
||||||
init {
|
init {
|
||||||
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
@ -43,7 +45,7 @@ class JumpToBottomViewVisibilityManager(
|
|||||||
|
|
||||||
if (scrollingToPast) {
|
if (scrollingToPast) {
|
||||||
jumpToBottomView.hide()
|
jumpToBottomView.hide()
|
||||||
} else {
|
} else if (canShowButtonOnScroll) {
|
||||||
maybeShowJumpToBottomViewVisibility()
|
maybeShowJumpToBottomViewVisibility()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -66,7 +68,13 @@ class JumpToBottomViewVisibilityManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hideAndPreventVisibilityChangesWithScrolling() {
|
||||||
|
jumpToBottomView.hide()
|
||||||
|
canShowButtonOnScroll = false
|
||||||
|
}
|
||||||
|
|
||||||
private fun maybeShowJumpToBottomViewVisibility() {
|
private fun maybeShowJumpToBottomViewVisibility() {
|
||||||
|
canShowButtonOnScroll = true
|
||||||
if (layoutManager.findFirstVisibleItemPosition() > 1) {
|
if (layoutManager.findFirstVisibleItemPosition() > 1) {
|
||||||
jumpToBottomView.show()
|
jumpToBottomView.show()
|
||||||
} else {
|
} else {
|
||||||
|
@ -32,7 +32,10 @@ import android.view.ViewGroup
|
|||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.activity.addCallback
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.view.menu.MenuBuilder
|
import androidx.appcompat.view.menu.MenuBuilder
|
||||||
|
import androidx.constraintlayout.widget.ConstraintSet
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.graphics.drawable.DrawableCompat
|
import androidx.core.graphics.drawable.DrawableCompat
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
@ -64,6 +67,7 @@ import im.vector.app.core.dialogs.ConfirmationDialogBuilder
|
|||||||
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
|
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
|
||||||
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelperFactory
|
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelperFactory
|
||||||
import im.vector.app.core.epoxy.LayoutManagerStateRestorer
|
import im.vector.app.core.epoxy.LayoutManagerStateRestorer
|
||||||
|
import im.vector.app.core.extensions.animateLayoutChange
|
||||||
import im.vector.app.core.extensions.cleanup
|
import im.vector.app.core.extensions.cleanup
|
||||||
import im.vector.app.core.extensions.commitTransaction
|
import im.vector.app.core.extensions.commitTransaction
|
||||||
import im.vector.app.core.extensions.containsRtLOverride
|
import im.vector.app.core.extensions.containsRtLOverride
|
||||||
@ -183,7 +187,9 @@ import im.vector.app.features.widgets.WidgetArgs
|
|||||||
import im.vector.app.features.widgets.WidgetKind
|
import im.vector.app.features.widgets.WidgetKind
|
||||||
import im.vector.app.features.widgets.permissions.RoomWidgetPermissionBottomSheet
|
import im.vector.app.features.widgets.permissions.RoomWidgetPermissionBottomSheet
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@ -337,6 +343,7 @@ class TimelineFragment :
|
|||||||
setupJumpToBottomView()
|
setupJumpToBottomView()
|
||||||
setupRemoveJitsiWidgetView()
|
setupRemoveJitsiWidgetView()
|
||||||
setupLiveLocationIndicator()
|
setupLiveLocationIndicator()
|
||||||
|
setupBackPressHandling()
|
||||||
|
|
||||||
views.includeRoomToolbar.roomToolbarContentView.debouncedClicks {
|
views.includeRoomToolbar.roomToolbarContentView.debouncedClicks {
|
||||||
navigator.openRoomProfile(requireActivity(), timelineArgs.roomId)
|
navigator.openRoomProfile(requireActivity(), timelineArgs.roomId)
|
||||||
@ -414,6 +421,31 @@ class TimelineFragment :
|
|||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
handleSpaceShare()
|
handleSpaceShare()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
views.scrim.setOnClickListener {
|
||||||
|
messageComposerViewModel.handle(MessageComposerAction.SetFullScreen(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
messageComposerViewModel.stateFlow.map { it.isFullScreen }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.onEach { isFullScreen ->
|
||||||
|
toggleFullScreenEditor(isFullScreen)
|
||||||
|
}
|
||||||
|
.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupBackPressHandling() {
|
||||||
|
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) {
|
||||||
|
withState(messageComposerViewModel) { state ->
|
||||||
|
if (state.isFullScreen) {
|
||||||
|
messageComposerViewModel.handle(MessageComposerAction.SetFullScreen(false))
|
||||||
|
} else {
|
||||||
|
remove() // Remove callback to avoid infinite loop
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
requireActivity().onBackPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupRemoveJitsiWidgetView() {
|
private fun setupRemoveJitsiWidgetView() {
|
||||||
@ -1016,7 +1048,13 @@ class TimelineFragment :
|
|||||||
override fun onLayoutCompleted(state: RecyclerView.State) {
|
override fun onLayoutCompleted(state: RecyclerView.State) {
|
||||||
super.onLayoutCompleted(state)
|
super.onLayoutCompleted(state)
|
||||||
updateJumpToReadMarkerViewVisibility()
|
updateJumpToReadMarkerViewVisibility()
|
||||||
jumpToBottomViewVisibilityManager.maybeShowJumpToBottomViewVisibilityWithDelay()
|
withState(messageComposerViewModel) { composerState ->
|
||||||
|
if (!composerState.isFullScreen) {
|
||||||
|
jumpToBottomViewVisibilityManager.maybeShowJumpToBottomViewVisibilityWithDelay()
|
||||||
|
} else {
|
||||||
|
jumpToBottomViewVisibilityManager.hideAndPreventVisibilityChangesWithScrolling()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.apply {
|
}.apply {
|
||||||
// For local rooms, pin the view's content to the top edge (the layout is reversed)
|
// For local rooms, pin the view's content to the top edge (the layout is reversed)
|
||||||
@ -1283,8 +1321,12 @@ class TimelineFragment :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun displayRoomDetailActionFailure(result: RoomDetailViewEvents.ActionFailure) {
|
private fun displayRoomDetailActionFailure(result: RoomDetailViewEvents.ActionFailure) {
|
||||||
|
@StringRes val titleResId = when (result.action) {
|
||||||
|
RoomDetailAction.VoiceBroadcastAction.Recording.Start -> R.string.error_voice_broadcast_unauthorized_title
|
||||||
|
else -> R.string.dialog_title_error
|
||||||
|
}
|
||||||
MaterialAlertDialogBuilder(requireActivity())
|
MaterialAlertDialogBuilder(requireActivity())
|
||||||
.setTitle(R.string.dialog_title_error)
|
.setTitle(titleResId)
|
||||||
.setMessage(errorFormatter.toHumanReadable(result.throwable))
|
.setMessage(errorFormatter.toHumanReadable(result.throwable))
|
||||||
.setPositiveButton(R.string.ok, null)
|
.setPositiveButton(R.string.ok, null)
|
||||||
.show()
|
.show()
|
||||||
@ -2002,6 +2044,19 @@ class TimelineFragment :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun toggleFullScreenEditor(isFullScreen: Boolean) {
|
||||||
|
views.composerContainer.animateLayoutChange(200)
|
||||||
|
|
||||||
|
val constraintSet = ConstraintSet()
|
||||||
|
val constraintSetId = if (isFullScreen) {
|
||||||
|
R.layout.fragment_timeline_fullscreen
|
||||||
|
} else {
|
||||||
|
R.layout.fragment_timeline
|
||||||
|
}
|
||||||
|
constraintSet.clone(requireContext(), constraintSetId)
|
||||||
|
constraintSet.applyTo(views.rootConstraintLayout)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the current room is a Thread room, false otherwise.
|
* Returns true if the current room is a Thread room, false otherwise.
|
||||||
*/
|
*/
|
||||||
|
@ -624,7 +624,12 @@ class TimelineViewModel @AssistedInject constructor(
|
|||||||
if (room == null) return
|
if (room == null) return
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
when (action) {
|
when (action) {
|
||||||
RoomDetailAction.VoiceBroadcastAction.Recording.Start -> voiceBroadcastHelper.startVoiceBroadcast(room.roomId)
|
RoomDetailAction.VoiceBroadcastAction.Recording.Start -> {
|
||||||
|
voiceBroadcastHelper.startVoiceBroadcast(room.roomId).fold(
|
||||||
|
{ _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action)) },
|
||||||
|
{ _viewEvents.post(RoomDetailViewEvents.ActionFailure(action, it)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
RoomDetailAction.VoiceBroadcastAction.Recording.Pause -> voiceBroadcastHelper.pauseVoiceBroadcast(room.roomId)
|
RoomDetailAction.VoiceBroadcastAction.Recording.Pause -> voiceBroadcastHelper.pauseVoiceBroadcast(room.roomId)
|
||||||
RoomDetailAction.VoiceBroadcastAction.Recording.Resume -> voiceBroadcastHelper.resumeVoiceBroadcast(room.roomId)
|
RoomDetailAction.VoiceBroadcastAction.Recording.Resume -> voiceBroadcastHelper.resumeVoiceBroadcast(room.roomId)
|
||||||
RoomDetailAction.VoiceBroadcastAction.Recording.Stop -> voiceBroadcastHelper.stopVoiceBroadcast(room.roomId)
|
RoomDetailAction.VoiceBroadcastAction.Recording.Stop -> voiceBroadcastHelper.stopVoiceBroadcast(room.roomId)
|
||||||
|
@ -34,6 +34,8 @@ sealed class MessageComposerAction : VectorViewModelAction {
|
|||||||
data class SlashCommandConfirmed(val parsedCommand: ParsedCommand) : MessageComposerAction()
|
data class SlashCommandConfirmed(val parsedCommand: ParsedCommand) : MessageComposerAction()
|
||||||
data class InsertUserDisplayName(val userId: String) : MessageComposerAction()
|
data class InsertUserDisplayName(val userId: String) : MessageComposerAction()
|
||||||
|
|
||||||
|
data class SetFullScreen(val isFullScreen: Boolean) : MessageComposerAction()
|
||||||
|
|
||||||
// Voice Message
|
// Voice Message
|
||||||
data class InitializeVoiceRecorder(val attachmentData: ContentAttachmentData) : MessageComposerAction()
|
data class InitializeVoiceRecorder(val attachmentData: ContentAttachmentData) : MessageComposerAction()
|
||||||
data class OnVoiceRecordingUiStateChanged(val uiState: VoiceMessageRecorderView.RecordingUiState) : MessageComposerAction()
|
data class OnVoiceRecordingUiStateChanged(val uiState: VoiceMessageRecorderView.RecordingUiState) : MessageComposerAction()
|
||||||
|
@ -43,6 +43,7 @@ import androidx.core.view.isVisible
|
|||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
import com.airbnb.mvrx.parentFragmentViewModel
|
import com.airbnb.mvrx.parentFragmentViewModel
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
@ -69,6 +70,7 @@ import im.vector.app.features.attachments.AttachmentTypeSelectorBottomSheet
|
|||||||
import im.vector.app.features.attachments.AttachmentTypeSelectorSharedAction
|
import im.vector.app.features.attachments.AttachmentTypeSelectorSharedAction
|
||||||
import im.vector.app.features.attachments.AttachmentTypeSelectorSharedActionViewModel
|
import im.vector.app.features.attachments.AttachmentTypeSelectorSharedActionViewModel
|
||||||
import im.vector.app.features.attachments.AttachmentTypeSelectorView
|
import im.vector.app.features.attachments.AttachmentTypeSelectorView
|
||||||
|
import im.vector.app.features.attachments.AttachmentTypeSelectorViewModel
|
||||||
import im.vector.app.features.attachments.AttachmentsHelper
|
import im.vector.app.features.attachments.AttachmentsHelper
|
||||||
import im.vector.app.features.attachments.ContactAttachment
|
import im.vector.app.features.attachments.ContactAttachment
|
||||||
import im.vector.app.features.attachments.ShareIntentHandler
|
import im.vector.app.features.attachments.ShareIntentHandler
|
||||||
@ -97,6 +99,7 @@ import im.vector.app.features.settings.VectorPreferences
|
|||||||
import im.vector.app.features.share.SharedData
|
import im.vector.app.features.share.SharedData
|
||||||
import im.vector.app.features.voice.VoiceFailure
|
import im.vector.app.features.voice.VoiceFailure
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.filterIsInstance
|
import kotlinx.coroutines.flow.filterIsInstance
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
@ -167,7 +170,8 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||||||
private val timelineViewModel: TimelineViewModel by parentFragmentViewModel()
|
private val timelineViewModel: TimelineViewModel by parentFragmentViewModel()
|
||||||
private val messageComposerViewModel: MessageComposerViewModel by parentFragmentViewModel()
|
private val messageComposerViewModel: MessageComposerViewModel by parentFragmentViewModel()
|
||||||
private lateinit var sharedActionViewModel: MessageSharedActionViewModel
|
private lateinit var sharedActionViewModel: MessageSharedActionViewModel
|
||||||
private val attachmentViewModel: AttachmentTypeSelectorSharedActionViewModel by viewModels()
|
private val attachmentViewModel: AttachmentTypeSelectorViewModel by fragmentViewModel()
|
||||||
|
private val attachmentActionsViewModel: AttachmentTypeSelectorSharedActionViewModel by viewModels()
|
||||||
|
|
||||||
private val composer: MessageComposerView get() {
|
private val composer: MessageComposerView get() {
|
||||||
return if (vectorPreferences.isRichTextEditorEnabled()) {
|
return if (vectorPreferences.isRichTextEditorEnabled()) {
|
||||||
@ -213,6 +217,13 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
messageComposerViewModel.stateFlow.map { it.isFullScreen }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.onEach { isFullScreen ->
|
||||||
|
composer.toggleFullScreen(isFullScreen)
|
||||||
|
}
|
||||||
|
.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||||
|
|
||||||
messageComposerViewModel.onEach(MessageComposerViewState::sendMode, MessageComposerViewState::canSendMessage) { mode, canSend ->
|
messageComposerViewModel.onEach(MessageComposerViewState::sendMode, MessageComposerViewState::canSendMessage) { mode, canSend ->
|
||||||
if (!canSend.boolean()) {
|
if (!canSend.boolean()) {
|
||||||
return@onEach
|
return@onEach
|
||||||
@ -226,7 +237,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
attachmentViewModel.stream()
|
attachmentActionsViewModel.stream()
|
||||||
.filterIsInstance<AttachmentTypeSelectorSharedAction.SelectAttachmentTypeAction>()
|
.filterIsInstance<AttachmentTypeSelectorSharedAction.SelectAttachmentTypeAction>()
|
||||||
.onEach { onTypeSelected(it.attachmentType) }
|
.onEach { onTypeSelected(it.attachmentType) }
|
||||||
.launchIn(lifecycleScope)
|
.launchIn(lifecycleScope)
|
||||||
@ -246,7 +257,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||||||
}
|
}
|
||||||
// TODO remove this when there will be a recording indicator outside of the timeline
|
// TODO remove this when there will be a recording indicator outside of the timeline
|
||||||
// Pause voice broadcast if the timeline is not shown anymore
|
// Pause voice broadcast if the timeline is not shown anymore
|
||||||
it.isVoiceBroadcasting && !requireActivity().isChangingConfigurations -> timelineViewModel.handle(VoiceBroadcastAction.Recording.Pause)
|
it.isRecordingVoiceBroadcast && !requireActivity().isChangingConfigurations -> timelineViewModel.handle(VoiceBroadcastAction.Recording.Pause)
|
||||||
else -> {
|
else -> {
|
||||||
timelineViewModel.handle(VoiceBroadcastAction.Listening.Pause)
|
timelineViewModel.handle(VoiceBroadcastAction.Listening.Pause)
|
||||||
messageComposerViewModel.handle(MessageComposerAction.OnEntersBackground(composer.text.toString()))
|
messageComposerViewModel.handle(MessageComposerAction.OnEntersBackground(composer.text.toString()))
|
||||||
@ -264,11 +275,14 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||||||
messageComposerViewModel.endAllVoiceActions()
|
messageComposerViewModel.endAllVoiceActions()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun invalidate() = withState(timelineViewModel, messageComposerViewModel) { mainState, messageComposerState ->
|
override fun invalidate() = withState(
|
||||||
|
timelineViewModel, messageComposerViewModel, attachmentViewModel
|
||||||
|
) { mainState, messageComposerState, attachmentState ->
|
||||||
if (mainState.tombstoneEvent != null) return@withState
|
if (mainState.tombstoneEvent != null) return@withState
|
||||||
|
|
||||||
composer.setInvisible(!messageComposerState.isComposerVisible)
|
composer.setInvisible(!messageComposerState.isComposerVisible)
|
||||||
composer.sendButton.isInvisible = !messageComposerState.isSendButtonVisible
|
composer.sendButton.isInvisible = !messageComposerState.isSendButtonVisible
|
||||||
|
(composer as? RichTextComposerLayout)?.isTextFormattingEnabled = attachmentState.isTextFormattingEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupComposer() {
|
private fun setupComposer() {
|
||||||
@ -309,7 +323,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||||||
// Show keyboard when the user started a thread
|
// Show keyboard when the user started a thread
|
||||||
composerEditText.showKeyboard(andRequestFocus = true)
|
composerEditText.showKeyboard(andRequestFocus = true)
|
||||||
}
|
}
|
||||||
composer.callback = object : PlainTextComposerLayout.Callback {
|
composer.callback = object : Callback {
|
||||||
override fun onAddAttachment() {
|
override fun onAddAttachment() {
|
||||||
if (vectorPreferences.isRichTextEditorEnabled()) {
|
if (vectorPreferences.isRichTextEditorEnabled()) {
|
||||||
AttachmentTypeSelectorBottomSheet.show(childFragmentManager)
|
AttachmentTypeSelectorBottomSheet.show(childFragmentManager)
|
||||||
@ -336,8 +350,12 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||||||
composer.emojiButton?.isVisible = isEmojiKeyboardVisible
|
composer.emojiButton?.isVisible = isEmojiKeyboardVisible
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSendMessage(text: CharSequence) {
|
override fun onSendMessage(text: CharSequence) = withState(messageComposerViewModel) { state ->
|
||||||
sendTextMessage(text, composer.formattedText)
|
sendTextMessage(text, composer.formattedText)
|
||||||
|
|
||||||
|
if (state.isFullScreen) {
|
||||||
|
messageComposerViewModel.handle(MessageComposerAction.SetFullScreen(false))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCloseRelatedMessage() {
|
override fun onCloseRelatedMessage() {
|
||||||
@ -351,6 +369,10 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||||||
override fun onTextChanged(text: CharSequence) {
|
override fun onTextChanged(text: CharSequence) {
|
||||||
messageComposerViewModel.handle(MessageComposerAction.OnTextChanged(text))
|
messageComposerViewModel.handle(MessageComposerAction.OnTextChanged(text))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onFullScreenModeChanged() = withState(messageComposerViewModel) { state ->
|
||||||
|
messageComposerViewModel.handle(MessageComposerAction.SetFullScreen(!state.isFullScreen))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -477,7 +499,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||||||
composer.sendButton.alpha = 0f
|
composer.sendButton.alpha = 0f
|
||||||
composer.sendButton.isVisible = true
|
composer.sendButton.isVisible = true
|
||||||
composer.sendButton.animate().alpha(1f).setDuration(150).start()
|
composer.sendButton.animate().alpha(1f).setDuration(150).start()
|
||||||
} else {
|
} else if (!event.isVisible) {
|
||||||
composer.sendButton.isInvisible = true
|
composer.sendButton.isInvisible = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,13 +30,14 @@ interface MessageComposerView {
|
|||||||
val emojiButton: ImageButton?
|
val emojiButton: ImageButton?
|
||||||
val sendButton: ImageButton
|
val sendButton: ImageButton
|
||||||
val attachmentButton: ImageButton
|
val attachmentButton: ImageButton
|
||||||
|
val fullScreenButton: ImageButton?
|
||||||
val composerRelatedMessageTitle: TextView
|
val composerRelatedMessageTitle: TextView
|
||||||
val composerRelatedMessageContent: TextView
|
val composerRelatedMessageContent: TextView
|
||||||
val composerRelatedMessageImage: ImageView
|
val composerRelatedMessageImage: ImageView
|
||||||
val composerRelatedMessageActionIcon: ImageView
|
val composerRelatedMessageActionIcon: ImageView
|
||||||
val composerRelatedMessageAvatar: ImageView
|
val composerRelatedMessageAvatar: ImageView
|
||||||
|
|
||||||
var callback: PlainTextComposerLayout.Callback?
|
var callback: Callback?
|
||||||
|
|
||||||
var isVisible: Boolean
|
var isVisible: Boolean
|
||||||
|
|
||||||
@ -44,6 +45,15 @@ interface MessageComposerView {
|
|||||||
fun expand(animate: Boolean = true, transitionComplete: (() -> Unit)? = null)
|
fun expand(animate: Boolean = true, transitionComplete: (() -> Unit)? = null)
|
||||||
fun setTextIfDifferent(text: CharSequence?): Boolean
|
fun setTextIfDifferent(text: CharSequence?): Boolean
|
||||||
fun replaceFormattedContent(text: CharSequence)
|
fun replaceFormattedContent(text: CharSequence)
|
||||||
|
fun toggleFullScreen(newValue: Boolean)
|
||||||
|
|
||||||
fun setInvisible(isInvisible: Boolean)
|
fun setInvisible(isInvisible: Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Callback : ComposerEditText.Callback {
|
||||||
|
fun onCloseRelatedMessage()
|
||||||
|
fun onSendMessage(text: CharSequence)
|
||||||
|
fun onAddAttachment()
|
||||||
|
fun onExpandOrCompactChange()
|
||||||
|
fun onFullScreenModeChanged()
|
||||||
|
}
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package im.vector.app.features.home.room.detail.composer
|
package im.vector.app.features.home.room.detail.composer
|
||||||
|
|
||||||
|
import android.text.SpannableString
|
||||||
import androidx.lifecycle.asFlow
|
import androidx.lifecycle.asFlow
|
||||||
import com.airbnb.mvrx.MavericksViewModelFactory
|
import com.airbnb.mvrx.MavericksViewModelFactory
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
@ -122,6 +123,7 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||||||
is MessageComposerAction.AudioSeekBarMovedTo -> handleAudioSeekBarMovedTo(action)
|
is MessageComposerAction.AudioSeekBarMovedTo -> handleAudioSeekBarMovedTo(action)
|
||||||
is MessageComposerAction.SlashCommandConfirmed -> handleSlashCommandConfirmed(action)
|
is MessageComposerAction.SlashCommandConfirmed -> handleSlashCommandConfirmed(action)
|
||||||
is MessageComposerAction.InsertUserDisplayName -> handleInsertUserDisplayName(action)
|
is MessageComposerAction.InsertUserDisplayName -> handleInsertUserDisplayName(action)
|
||||||
|
is MessageComposerAction.SetFullScreen -> handleSetFullScreen(action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,12 +132,11 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleOnTextChanged(action: MessageComposerAction.OnTextChanged) {
|
private fun handleOnTextChanged(action: MessageComposerAction.OnTextChanged) {
|
||||||
setState {
|
val needsSendButtonVisibilityUpdate = currentComposerText.isEmpty() != action.text.isEmpty()
|
||||||
// Makes sure currentComposerText is upToDate when accessing further setState
|
currentComposerText = SpannableString(action.text)
|
||||||
currentComposerText = action.text
|
if (needsSendButtonVisibilityUpdate) {
|
||||||
this
|
updateIsSendButtonVisibility(true)
|
||||||
}
|
}
|
||||||
updateIsSendButtonVisibility(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun subscribeToStateInternal() {
|
private fun subscribeToStateInternal() {
|
||||||
@ -163,6 +164,10 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleSetFullScreen(action: MessageComposerAction.SetFullScreen) {
|
||||||
|
setState { copy(isFullScreen = action.isFullScreen) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun observePowerLevelAndEncryption() {
|
private fun observePowerLevelAndEncryption() {
|
||||||
combine(
|
combine(
|
||||||
PowerLevelsFlowFactory(room).createFlow(),
|
PowerLevelsFlowFactory(room).createFlow(),
|
||||||
|
@ -70,6 +70,7 @@ data class MessageComposerViewState(
|
|||||||
val voiceRecordingUiState: VoiceMessageRecorderView.RecordingUiState = VoiceMessageRecorderView.RecordingUiState.Idle,
|
val voiceRecordingUiState: VoiceMessageRecorderView.RecordingUiState = VoiceMessageRecorderView.RecordingUiState.Idle,
|
||||||
val voiceBroadcastState: VoiceBroadcastState? = null,
|
val voiceBroadcastState: VoiceBroadcastState? = null,
|
||||||
val text: CharSequence? = null,
|
val text: CharSequence? = null,
|
||||||
|
val isFullScreen: Boolean = false,
|
||||||
) : MavericksState {
|
) : MavericksState {
|
||||||
|
|
||||||
val isVoiceRecording = when (voiceRecordingUiState) {
|
val isVoiceRecording = when (voiceRecordingUiState) {
|
||||||
@ -79,9 +80,8 @@ data class MessageComposerViewState(
|
|||||||
is VoiceMessageRecorderView.RecordingUiState.Recording -> true
|
is VoiceMessageRecorderView.RecordingUiState.Recording -> true
|
||||||
}
|
}
|
||||||
|
|
||||||
val isVoiceBroadcasting = when (voiceBroadcastState) {
|
val isRecordingVoiceBroadcast = when (voiceBroadcastState) {
|
||||||
VoiceBroadcastState.STARTED,
|
VoiceBroadcastState.STARTED,
|
||||||
VoiceBroadcastState.PAUSED,
|
|
||||||
VoiceBroadcastState.RESUMED -> true
|
VoiceBroadcastState.RESUMED -> true
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
@ -49,13 +49,6 @@ class PlainTextComposerLayout @JvmOverloads constructor(
|
|||||||
defStyleAttr: Int = 0
|
defStyleAttr: Int = 0
|
||||||
) : ConstraintLayout(context, attrs, defStyleAttr), MessageComposerView {
|
) : ConstraintLayout(context, attrs, defStyleAttr), MessageComposerView {
|
||||||
|
|
||||||
interface Callback : ComposerEditText.Callback {
|
|
||||||
fun onCloseRelatedMessage()
|
|
||||||
fun onSendMessage(text: CharSequence)
|
|
||||||
fun onAddAttachment()
|
|
||||||
fun onExpandOrCompactChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val views: ComposerLayoutBinding
|
private val views: ComposerLayoutBinding
|
||||||
|
|
||||||
override var callback: Callback? = null
|
override var callback: Callback? = null
|
||||||
@ -83,6 +76,7 @@ class PlainTextComposerLayout @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
override val attachmentButton: ImageButton
|
override val attachmentButton: ImageButton
|
||||||
get() = views.attachmentButton
|
get() = views.attachmentButton
|
||||||
|
override val fullScreenButton: ImageButton? = null
|
||||||
override val composerRelatedMessageActionIcon: ImageView
|
override val composerRelatedMessageActionIcon: ImageView
|
||||||
get() = views.composerRelatedMessageActionIcon
|
get() = views.composerRelatedMessageActionIcon
|
||||||
override val composerRelatedMessageAvatar: ImageView
|
override val composerRelatedMessageAvatar: ImageView
|
||||||
@ -155,6 +149,10 @@ class PlainTextComposerLayout @JvmOverloads constructor(
|
|||||||
return views.composerEditText.setTextIfDifferent(text)
|
return views.composerEditText.setTextIfDifferent(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun toggleFullScreen(newValue: Boolean) {
|
||||||
|
// Plain text composer has no full screen
|
||||||
|
}
|
||||||
|
|
||||||
private fun applyNewConstraintSet(animate: Boolean, transitionComplete: (() -> Unit)?) {
|
private fun applyNewConstraintSet(animate: Boolean, transitionComplete: (() -> Unit)?) {
|
||||||
// val wasSendButtonInvisible = views.sendButton.isInvisible
|
// val wasSendButtonInvisible = views.sendButton.isInvisible
|
||||||
if (animate) {
|
if (animate) {
|
||||||
|
@ -21,7 +21,6 @@ import android.text.Editable
|
|||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
@ -33,18 +32,13 @@ import androidx.constraintlayout.widget.ConstraintSet
|
|||||||
import androidx.core.text.toSpannable
|
import androidx.core.text.toSpannable
|
||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.transition.ChangeBounds
|
|
||||||
import androidx.transition.Fade
|
|
||||||
import androidx.transition.Transition
|
|
||||||
import androidx.transition.TransitionManager
|
|
||||||
import androidx.transition.TransitionSet
|
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.animations.SimpleTransitionListener
|
import im.vector.app.core.extensions.animateLayoutChange
|
||||||
import im.vector.app.core.extensions.setTextIfDifferent
|
import im.vector.app.core.extensions.setTextIfDifferent
|
||||||
import im.vector.app.databinding.ComposerRichTextLayoutBinding
|
import im.vector.app.databinding.ComposerRichTextLayoutBinding
|
||||||
import im.vector.app.databinding.ViewRichTextMenuButtonBinding
|
import im.vector.app.databinding.ViewRichTextMenuButtonBinding
|
||||||
import io.element.android.wysiwyg.EditorEditText
|
import io.element.android.wysiwyg.EditorEditText
|
||||||
import io.element.android.wysiwyg.InlineFormat
|
import io.element.android.wysiwyg.inputhandlers.models.InlineFormat
|
||||||
import uniffi.wysiwyg_composer.ComposerAction
|
import uniffi.wysiwyg_composer.ComposerAction
|
||||||
import uniffi.wysiwyg_composer.MenuState
|
import uniffi.wysiwyg_composer.MenuState
|
||||||
|
|
||||||
@ -56,24 +50,40 @@ class RichTextComposerLayout @JvmOverloads constructor(
|
|||||||
|
|
||||||
private val views: ComposerRichTextLayoutBinding
|
private val views: ComposerRichTextLayoutBinding
|
||||||
|
|
||||||
override var callback: PlainTextComposerLayout.Callback? = null
|
override var callback: Callback? = null
|
||||||
|
|
||||||
private var currentConstraintSetId: Int = -1
|
private var currentConstraintSetId: Int = -1
|
||||||
|
|
||||||
private val animationDuration = 100L
|
private val animationDuration = 100L
|
||||||
|
private val maxEditTextLinesWhenCollapsed = 12
|
||||||
|
|
||||||
|
private val isFullScreen: Boolean get() = currentConstraintSetId == R.layout.composer_rich_text_layout_constraint_set_fullscreen
|
||||||
|
|
||||||
|
var isTextFormattingEnabled = true
|
||||||
|
set(value) {
|
||||||
|
if (field == value) return
|
||||||
|
syncEditTexts()
|
||||||
|
field = value
|
||||||
|
updateEditTextVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
override val text: Editable?
|
override val text: Editable?
|
||||||
get() = views.composerEditText.text
|
get() = editText.text
|
||||||
override val formattedText: String?
|
override val formattedText: String?
|
||||||
get() = views.composerEditText.getHtmlOutput()
|
get() = (editText as? EditorEditText)?.getHtmlOutput()
|
||||||
override val editText: EditText
|
override val editText: EditText
|
||||||
get() = views.composerEditText
|
get() = if (isTextFormattingEnabled) {
|
||||||
|
views.richTextComposerEditText
|
||||||
|
} else {
|
||||||
|
views.plainTextComposerEditText
|
||||||
|
}
|
||||||
override val emojiButton: ImageButton?
|
override val emojiButton: ImageButton?
|
||||||
get() = null
|
get() = null
|
||||||
override val sendButton: ImageButton
|
override val sendButton: ImageButton
|
||||||
get() = views.sendButton
|
get() = views.sendButton
|
||||||
override val attachmentButton: ImageButton
|
override val attachmentButton: ImageButton
|
||||||
get() = views.attachmentButton
|
get() = views.attachmentButton
|
||||||
|
override val fullScreenButton: ImageButton?
|
||||||
|
get() = views.composerFullScreenButton
|
||||||
override val composerRelatedMessageActionIcon: ImageView
|
override val composerRelatedMessageActionIcon: ImageView
|
||||||
get() = views.composerRelatedMessageActionIcon
|
get() = views.composerRelatedMessageActionIcon
|
||||||
override val composerRelatedMessageAvatar: ImageView
|
override val composerRelatedMessageAvatar: ImageView
|
||||||
@ -94,21 +104,12 @@ class RichTextComposerLayout @JvmOverloads constructor(
|
|||||||
|
|
||||||
collapse(false)
|
collapse(false)
|
||||||
|
|
||||||
views.composerEditText.addTextChangedListener(object : TextWatcher {
|
views.richTextComposerEditText.addTextChangedListener(
|
||||||
private var previousTextWasExpanded = false
|
TextChangeListener({ callback?.onTextChanged(it) }, { updateTextFieldBorder() })
|
||||||
|
)
|
||||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
views.plainTextComposerEditText.addTextChangedListener(
|
||||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
TextChangeListener({ callback?.onTextChanged(it) }, { updateTextFieldBorder() })
|
||||||
override fun afterTextChanged(s: Editable) {
|
)
|
||||||
callback?.onTextChanged(s)
|
|
||||||
|
|
||||||
val isExpanded = s.lines().count() > 1
|
|
||||||
if (previousTextWasExpanded != isExpanded) {
|
|
||||||
updateTextFieldBorder(isExpanded)
|
|
||||||
}
|
|
||||||
previousTextWasExpanded = isExpanded
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
views.composerRelatedMessageCloseButton.setOnClickListener {
|
views.composerRelatedMessageCloseButton.setOnClickListener {
|
||||||
collapse()
|
collapse()
|
||||||
@ -124,24 +125,32 @@ class RichTextComposerLayout @JvmOverloads constructor(
|
|||||||
callback?.onAddAttachment()
|
callback?.onAddAttachment()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
views.composerFullScreenButton.setOnClickListener {
|
||||||
|
callback?.onFullScreenModeChanged()
|
||||||
|
}
|
||||||
|
|
||||||
setupRichTextMenu()
|
setupRichTextMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupRichTextMenu() {
|
private fun setupRichTextMenu() {
|
||||||
addRichTextMenuItem(R.drawable.ic_composer_bold, R.string.rich_text_editor_format_bold, ComposerAction.Bold) {
|
addRichTextMenuItem(R.drawable.ic_composer_bold, R.string.rich_text_editor_format_bold, ComposerAction.Bold) {
|
||||||
views.composerEditText.toggleInlineFormat(InlineFormat.Bold)
|
views.richTextComposerEditText.toggleInlineFormat(InlineFormat.Bold)
|
||||||
}
|
}
|
||||||
addRichTextMenuItem(R.drawable.ic_composer_italic, R.string.rich_text_editor_format_italic, ComposerAction.Italic) {
|
addRichTextMenuItem(R.drawable.ic_composer_italic, R.string.rich_text_editor_format_italic, ComposerAction.Italic) {
|
||||||
views.composerEditText.toggleInlineFormat(InlineFormat.Italic)
|
views.richTextComposerEditText.toggleInlineFormat(InlineFormat.Italic)
|
||||||
}
|
}
|
||||||
addRichTextMenuItem(R.drawable.ic_composer_underlined, R.string.rich_text_editor_format_underline, ComposerAction.Underline) {
|
addRichTextMenuItem(R.drawable.ic_composer_underlined, R.string.rich_text_editor_format_underline, ComposerAction.Underline) {
|
||||||
views.composerEditText.toggleInlineFormat(InlineFormat.Underline)
|
views.richTextComposerEditText.toggleInlineFormat(InlineFormat.Underline)
|
||||||
}
|
}
|
||||||
addRichTextMenuItem(R.drawable.ic_composer_strikethrough, R.string.rich_text_editor_format_strikethrough, ComposerAction.StrikeThrough) {
|
addRichTextMenuItem(R.drawable.ic_composer_strikethrough, R.string.rich_text_editor_format_strikethrough, ComposerAction.StrikeThrough) {
|
||||||
views.composerEditText.toggleInlineFormat(InlineFormat.StrikeThrough)
|
views.richTextComposerEditText.toggleInlineFormat(InlineFormat.StrikeThrough)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
views.composerEditText.menuStateChangedListener = EditorEditText.OnMenuStateChangedListener { state ->
|
override fun onAttachedToWindow() {
|
||||||
|
super.onAttachedToWindow()
|
||||||
|
|
||||||
|
views.richTextComposerEditText.menuStateChangedListener = EditorEditText.OnMenuStateChangedListener { state ->
|
||||||
if (state is MenuState.Update) {
|
if (state is MenuState.Update) {
|
||||||
updateMenuStateFor(ComposerAction.Bold, state)
|
updateMenuStateFor(ComposerAction.Bold, state)
|
||||||
updateMenuStateFor(ComposerAction.Italic, state)
|
updateMenuStateFor(ComposerAction.Italic, state)
|
||||||
@ -149,8 +158,26 @@ class RichTextComposerLayout @JvmOverloads constructor(
|
|||||||
updateMenuStateFor(ComposerAction.StrikeThrough, state)
|
updateMenuStateFor(ComposerAction.StrikeThrough, state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateEditTextVisibility()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateEditTextVisibility() {
|
||||||
|
views.richTextComposerEditText.isVisible = isTextFormattingEnabled
|
||||||
|
views.richTextMenu.isVisible = isTextFormattingEnabled
|
||||||
|
views.plainTextComposerEditText.isVisible = !isTextFormattingEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the non-active input with the contents of the active input.
|
||||||
|
*/
|
||||||
|
private fun syncEditTexts() =
|
||||||
|
if (isTextFormattingEnabled) {
|
||||||
|
views.plainTextComposerEditText.setText(views.richTextComposerEditText.getPlainText())
|
||||||
|
} else {
|
||||||
|
views.richTextComposerEditText.setText(views.plainTextComposerEditText.text.toString())
|
||||||
|
}
|
||||||
|
|
||||||
private fun addRichTextMenuItem(@DrawableRes iconId: Int, @StringRes description: Int, action: ComposerAction, onClick: () -> Unit) {
|
private fun addRichTextMenuItem(@DrawableRes iconId: Int, @StringRes description: Int, action: ComposerAction, onClick: () -> Unit) {
|
||||||
val inflater = LayoutInflater.from(context)
|
val inflater = LayoutInflater.from(context)
|
||||||
val button = ViewRichTextMenuButtonBinding.inflate(inflater, views.richTextMenu, true)
|
val button = ViewRichTextMenuButtonBinding.inflate(inflater, views.richTextMenu, true)
|
||||||
@ -170,8 +197,9 @@ class RichTextComposerLayout @JvmOverloads constructor(
|
|||||||
button.isSelected = menuState.reversedActions.contains(action)
|
button.isSelected = menuState.reversedActions.contains(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateTextFieldBorder(isExpanded: Boolean) {
|
private fun updateTextFieldBorder() {
|
||||||
val borderResource = if (isExpanded) {
|
val isExpanded = editText.editableText.lines().count() > 1
|
||||||
|
val borderResource = if (isExpanded || isFullScreen) {
|
||||||
R.drawable.bg_composer_rich_edit_text_expanded
|
R.drawable.bg_composer_rich_edit_text_expanded
|
||||||
} else {
|
} else {
|
||||||
R.drawable.bg_composer_rich_edit_text_single_line
|
R.drawable.bg_composer_rich_edit_text_single_line
|
||||||
@ -180,7 +208,7 @@ class RichTextComposerLayout @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun replaceFormattedContent(text: CharSequence) {
|
override fun replaceFormattedContent(text: CharSequence) {
|
||||||
views.composerEditText.setHtml(text.toString())
|
views.richTextComposerEditText.setHtml(text.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun collapse(animate: Boolean, transitionComplete: (() -> Unit)?) {
|
override fun collapse(animate: Boolean, transitionComplete: (() -> Unit)?) {
|
||||||
@ -190,6 +218,7 @@ class RichTextComposerLayout @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
currentConstraintSetId = R.layout.composer_rich_text_layout_constraint_set_compact
|
currentConstraintSetId = R.layout.composer_rich_text_layout_constraint_set_compact
|
||||||
applyNewConstraintSet(animate, transitionComplete)
|
applyNewConstraintSet(animate, transitionComplete)
|
||||||
|
updateEditTextVisibility()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun expand(animate: Boolean, transitionComplete: (() -> Unit)?) {
|
override fun expand(animate: Boolean, transitionComplete: (() -> Unit)?) {
|
||||||
@ -199,41 +228,71 @@ class RichTextComposerLayout @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
currentConstraintSetId = R.layout.composer_rich_text_layout_constraint_set_expanded
|
currentConstraintSetId = R.layout.composer_rich_text_layout_constraint_set_expanded
|
||||||
applyNewConstraintSet(animate, transitionComplete)
|
applyNewConstraintSet(animate, transitionComplete)
|
||||||
|
updateEditTextVisibility()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setTextIfDifferent(text: CharSequence?): Boolean {
|
override fun setTextIfDifferent(text: CharSequence?): Boolean {
|
||||||
return views.composerEditText.setTextIfDifferent(text)
|
return editText.setTextIfDifferent(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toggleFullScreen(newValue: Boolean) {
|
||||||
|
val constraintSetId = if (newValue) R.layout.composer_rich_text_layout_constraint_set_fullscreen else currentConstraintSetId
|
||||||
|
ConstraintSet().also {
|
||||||
|
it.clone(context, constraintSetId)
|
||||||
|
it.applyTo(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTextFieldBorder()
|
||||||
|
updateEditTextVisibility()
|
||||||
|
|
||||||
|
updateEditTextFullScreenState(views.richTextComposerEditText, newValue)
|
||||||
|
updateEditTextFullScreenState(views.plainTextComposerEditText, newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateEditTextFullScreenState(editText: EditText, isFullScreen: Boolean) {
|
||||||
|
if (isFullScreen) {
|
||||||
|
editText.maxLines = Int.MAX_VALUE
|
||||||
|
// This is a workaround to fix incorrect scroll position when maximised
|
||||||
|
post { editText.requestLayout() }
|
||||||
|
} else {
|
||||||
|
editText.maxLines = maxEditTextLinesWhenCollapsed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun applyNewConstraintSet(animate: Boolean, transitionComplete: (() -> Unit)?) {
|
private fun applyNewConstraintSet(animate: Boolean, transitionComplete: (() -> Unit)?) {
|
||||||
// val wasSendButtonInvisible = views.sendButton.isInvisible
|
// val wasSendButtonInvisible = views.sendButton.isInvisible
|
||||||
if (animate) {
|
if (animate) {
|
||||||
configureAndBeginTransition(transitionComplete)
|
animateLayoutChange(animationDuration, transitionComplete)
|
||||||
}
|
}
|
||||||
ConstraintSet().also {
|
ConstraintSet().also {
|
||||||
it.clone(context, currentConstraintSetId)
|
it.clone(context, currentConstraintSetId)
|
||||||
it.applyTo(this)
|
it.applyTo(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Might be updated by view state just after, but avoid blinks
|
// Might be updated by view state just after, but avoid blinks
|
||||||
// views.sendButton.isInvisible = wasSendButtonInvisible
|
// views.sendButton.isInvisible = wasSendButtonInvisible
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun configureAndBeginTransition(transitionComplete: (() -> Unit)? = null) {
|
|
||||||
val transition = TransitionSet().apply {
|
|
||||||
ordering = TransitionSet.ORDERING_SEQUENTIAL
|
|
||||||
addTransition(ChangeBounds())
|
|
||||||
addTransition(Fade(Fade.IN))
|
|
||||||
duration = animationDuration
|
|
||||||
addListener(object : SimpleTransitionListener() {
|
|
||||||
override fun onTransitionEnd(transition: Transition) {
|
|
||||||
transitionComplete?.invoke()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
TransitionManager.beginDelayedTransition((parent as? ViewGroup ?: this), transition)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setInvisible(isInvisible: Boolean) {
|
override fun setInvisible(isInvisible: Boolean) {
|
||||||
this.isInvisible = isInvisible
|
this.isInvisible = isInvisible
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class TextChangeListener(
|
||||||
|
private val onTextChanged: (s: Editable) -> Unit,
|
||||||
|
private val onExpandedChanged: (isExpanded: Boolean) -> Unit,
|
||||||
|
) : TextWatcher {
|
||||||
|
private var previousTextWasExpanded = false
|
||||||
|
|
||||||
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||||
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||||
|
override fun afterTextChanged(s: Editable) {
|
||||||
|
onTextChanged.invoke(s)
|
||||||
|
|
||||||
|
val isExpanded = s.lines().count() > 1
|
||||||
|
if (previousTextWasExpanded != isExpanded) {
|
||||||
|
onExpandedChanged(isExpanded)
|
||||||
|
}
|
||||||
|
previousTextWasExpanded = isExpanded
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -15,26 +15,25 @@
|
|||||||
*/
|
*/
|
||||||
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
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastRecordingItem_
|
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastRecordingItem_
|
||||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastPlayer
|
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
|
||||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
|
|
||||||
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
|
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
|
||||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
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 im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
|
||||||
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,61 @@ 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 &&
|
||||||
|
messageContent.deviceId == session.sessionParams.deviceId
|
||||||
|
|
||||||
|
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 }
|
||||||
|
@ -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.listening.VoiceBroadcastPlayer
|
||||||
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
||||||
|
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
@ -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.listening.VoiceBroadcastPlayer
|
||||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastPlayer
|
import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
|
||||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
|
||||||
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 {
|
||||||
|
@ -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.model.VoiceBroadcastState
|
||||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
|
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
|
||||||
import org.matrix.android.sdk.api.util.MatrixItem
|
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) {
|
||||||
recorderListener = object : VoiceBroadcastRecorder.Listener {
|
if (recorder != null && recorder?.state != VoiceBroadcastRecorder.State.Idle) {
|
||||||
override fun onStateUpdated(state: VoiceBroadcastRecorder.State) {
|
recorderListener = object : VoiceBroadcastRecorder.Listener {
|
||||||
renderState(holder, state)
|
override fun onStateUpdated(state: VoiceBroadcastRecorder.State) {
|
||||||
}
|
renderRecordingState(holder, state)
|
||||||
}
|
}
|
||||||
voiceBroadcastRecorder?.addListener(recorderListener)
|
}.also { recorder?.addListener(it) }
|
||||||
renderHeader(holder)
|
} else {
|
||||||
}
|
renderVoiceBroadcastState(holder)
|
||||||
|
|
||||||
private fun renderHeader(holder: Holder) {
|
|
||||||
with(holder) {
|
|
||||||
roomItem?.let {
|
|
||||||
attributes.avatarRenderer.render(it, roomAvatarImageView)
|
|
||||||
titleText.text = it.displayName
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderState(holder: Holder, state: VoiceBroadcastRecorder.State) {
|
override fun renderMetadata(holder: Holder) {
|
||||||
with(holder) {
|
with(holder) {
|
||||||
when (state) {
|
listenersCountMetadata.isVisible = false
|
||||||
VoiceBroadcastRecorder.State.Recording -> {
|
remainingTimeMetadata.isVisible = false
|
||||||
stopRecordButton.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 drawable = drawableProvider.getDrawable(R.drawable.ic_play_pause_pause, drawableColor)
|
|
||||||
recordButton.setImageDrawable(drawable)
|
|
||||||
recordButton.contentDescription = holder.view.resources.getString(R.string.a11y_pause_voice_broadcast_record)
|
|
||||||
recordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Pause) }
|
|
||||||
stopRecordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) }
|
|
||||||
}
|
|
||||||
VoiceBroadcastRecorder.State.Paused -> {
|
|
||||||
stopRecordButton.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.contentDescription = holder.view.resources.getString(R.string.a11y_resume_voice_broadcast_record)
|
|
||||||
recordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Resume) }
|
|
||||||
stopRecordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) }
|
|
||||||
}
|
|
||||||
VoiceBroadcastRecorder.State.Idle -> {
|
|
||||||
recordButton.isEnabled = false
|
|
||||||
stopRecordButton.isEnabled = false
|
|
||||||
liveIndicator.isVisible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun renderRecordingState(holder: Holder, state: VoiceBroadcastRecorder.State) {
|
||||||
|
when (state) {
|
||||||
|
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
|
||||||
|
recordButton.isEnabled = true
|
||||||
|
|
||||||
|
val drawableColor = colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary)
|
||||||
|
val drawable = drawableProvider.getDrawable(R.drawable.ic_play_pause_pause, drawableColor)
|
||||||
|
recordButton.setImageDrawable(drawable)
|
||||||
|
recordButton.contentDescription = holder.view.resources.getString(R.string.a11y_pause_voice_broadcast_record)
|
||||||
|
recordButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Pause) }
|
||||||
|
stopRecordButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderPausedState(holder: Holder) = with(holder) {
|
||||||
|
stopRecordButton.isEnabled = true
|
||||||
|
recordButton.isEnabled = true
|
||||||
|
|
||||||
|
recordButton.setImageResource(R.drawable.ic_recording_dot)
|
||||||
|
recordButton.contentDescription = holder.view.resources.getString(R.string.a11y_resume_voice_broadcast_record)
|
||||||
|
recordButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Resume) }
|
||||||
|
stopRecordButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderStoppedState(holder: Holder) = with(holder) {
|
||||||
|
recordButton.isEnabled = false
|
||||||
|
stopRecordButton.isEnabled = 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)
|
||||||
}
|
}
|
||||||
|
@ -109,6 +109,7 @@ class VectorPreferences @Inject constructor(
|
|||||||
const val SETTINGS_SHOW_URL_PREVIEW_KEY = "SETTINGS_SHOW_URL_PREVIEW_KEY"
|
const val SETTINGS_SHOW_URL_PREVIEW_KEY = "SETTINGS_SHOW_URL_PREVIEW_KEY"
|
||||||
private const val SETTINGS_SEND_TYPING_NOTIF_KEY = "SETTINGS_SEND_TYPING_NOTIF_KEY"
|
private const val SETTINGS_SEND_TYPING_NOTIF_KEY = "SETTINGS_SEND_TYPING_NOTIF_KEY"
|
||||||
private const val SETTINGS_ENABLE_MARKDOWN_KEY = "SETTINGS_ENABLE_MARKDOWN_KEY"
|
private const val SETTINGS_ENABLE_MARKDOWN_KEY = "SETTINGS_ENABLE_MARKDOWN_KEY"
|
||||||
|
private const val SETTINGS_ENABLE_RICH_TEXT_FORMATTING_KEY = "SETTINGS_ENABLE_RICH_TEXT_FORMATTING_KEY"
|
||||||
private const val SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY = "SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY"
|
private const val SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY = "SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY"
|
||||||
private const val SETTINGS_12_24_TIMESTAMPS_KEY = "SETTINGS_12_24_TIMESTAMPS_KEY"
|
private const val SETTINGS_12_24_TIMESTAMPS_KEY = "SETTINGS_12_24_TIMESTAMPS_KEY"
|
||||||
private const val SETTINGS_SHOW_READ_RECEIPTS_KEY = "SETTINGS_SHOW_READ_RECEIPTS_KEY"
|
private const val SETTINGS_SHOW_READ_RECEIPTS_KEY = "SETTINGS_SHOW_READ_RECEIPTS_KEY"
|
||||||
@ -759,6 +760,24 @@ class VectorPreferences @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells if text formatting is enabled within the rich text editor.
|
||||||
|
*
|
||||||
|
* @return true if the text formatting is enabled
|
||||||
|
*/
|
||||||
|
fun isTextFormattingEnabled(): Boolean =
|
||||||
|
defaultPrefs.getBoolean(SETTINGS_ENABLE_RICH_TEXT_FORMATTING_KEY, true)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update whether text formatting is enabled within the rich text editor.
|
||||||
|
*
|
||||||
|
* @param isEnabled true to enable the text formatting
|
||||||
|
*/
|
||||||
|
fun setTextFormattingEnabled(isEnabled: Boolean) =
|
||||||
|
defaultPrefs.edit {
|
||||||
|
putBoolean(SETTINGS_ENABLE_RICH_TEXT_FORMATTING_KEY, isEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tells if a confirmation dialog should be displayed before staring a call.
|
* Tells if a confirmation dialog should be displayed before staring a call.
|
||||||
*/
|
*/
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
sealed class VoiceBroadcastFailure : Throwable() {
|
||||||
|
sealed class RecordingError : VoiceBroadcastFailure() {
|
||||||
|
object NoPermission : RecordingError()
|
||||||
|
object BlockedBySomeoneElse : RecordingError()
|
||||||
|
object UserAlreadyBroadcasting : RecordingError()
|
||||||
|
}
|
||||||
|
}
|
@ -16,10 +16,11 @@
|
|||||||
|
|
||||||
package im.vector.app.features.voicebroadcast
|
package im.vector.app.features.voicebroadcast
|
||||||
|
|
||||||
import im.vector.app.features.voicebroadcast.usecase.PauseVoiceBroadcastUseCase
|
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
|
||||||
import im.vector.app.features.voicebroadcast.usecase.ResumeVoiceBroadcastUseCase
|
import im.vector.app.features.voicebroadcast.recording.usecase.PauseVoiceBroadcastUseCase
|
||||||
import im.vector.app.features.voicebroadcast.usecase.StartVoiceBroadcastUseCase
|
import im.vector.app.features.voicebroadcast.recording.usecase.ResumeVoiceBroadcastUseCase
|
||||||
import im.vector.app.features.voicebroadcast.usecase.StopVoiceBroadcastUseCase
|
import im.vector.app.features.voicebroadcast.recording.usecase.StartVoiceBroadcastUseCase
|
||||||
|
import im.vector.app.features.voicebroadcast.recording.usecase.StopVoiceBroadcastUseCase
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
* 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.listening
|
||||||
|
|
||||||
|
interface VoiceBroadcastPlayer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current playing voice broadcast identifier, if any.
|
||||||
|
*/
|
||||||
|
val currentVoiceBroadcastId: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current playing [State], [State.IDLE] by default.
|
||||||
|
*/
|
||||||
|
val playingState: State
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start playback of the given voice broadcast.
|
||||||
|
*/
|
||||||
|
fun playOrResume(roomId: String, voiceBroadcastId: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pause playback of the current voice broadcast, if any.
|
||||||
|
*/
|
||||||
|
fun pause()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop playback of the current voice broadcast, if any, and reset the player state.
|
||||||
|
*/
|
||||||
|
fun stop()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a [Listener] to the given voice broadcast id.
|
||||||
|
*/
|
||||||
|
fun addListener(voiceBroadcastId: String, listener: Listener)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a [Listener] from the given voice broadcast id.
|
||||||
|
*/
|
||||||
|
fun removeListener(voiceBroadcastId: String, listener: Listener)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Player states.
|
||||||
|
*/
|
||||||
|
enum class State {
|
||||||
|
PLAYING,
|
||||||
|
PAUSED,
|
||||||
|
BUFFERING,
|
||||||
|
IDLE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener related to [VoiceBroadcastPlayer].
|
||||||
|
*/
|
||||||
|
fun interface Listener {
|
||||||
|
/**
|
||||||
|
* Notify about [VoiceBroadcastPlayer.playingState] changes.
|
||||||
|
*/
|
||||||
|
fun onStateChanged(state: State)
|
||||||
|
}
|
||||||
|
}
|
@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.app.features.voicebroadcast
|
package im.vector.app.features.voicebroadcast.listening
|
||||||
|
|
||||||
import android.media.AudioAttributes
|
import android.media.AudioAttributes
|
||||||
import android.media.MediaPlayer
|
import android.media.MediaPlayer
|
||||||
@ -22,49 +22,43 @@ import androidx.annotation.MainThread
|
|||||||
import im.vector.app.core.di.ActiveSessionHolder
|
import im.vector.app.core.di.ActiveSessionHolder
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
|
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
|
||||||
import im.vector.app.features.voice.VoiceFailure
|
import im.vector.app.features.voice.VoiceFailure
|
||||||
|
import im.vector.app.features.voicebroadcast.getVoiceBroadcastChunk
|
||||||
|
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.Listener
|
||||||
|
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.State
|
||||||
|
import im.vector.app.features.voicebroadcast.listening.usecase.GetLiveVoiceBroadcastChunksUseCase
|
||||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
||||||
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
import im.vector.app.features.voicebroadcast.sequence
|
||||||
import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastUseCase
|
import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastUseCase
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
|
||||||
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.message.MessageAudioContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
|
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class VoiceBroadcastPlayer @Inject constructor(
|
class VoiceBroadcastPlayerImpl @Inject constructor(
|
||||||
private val sessionHolder: ActiveSessionHolder,
|
private val sessionHolder: ActiveSessionHolder,
|
||||||
private val playbackTracker: AudioMessagePlaybackTracker,
|
private val playbackTracker: AudioMessagePlaybackTracker,
|
||||||
private val getVoiceBroadcastUseCase: GetVoiceBroadcastUseCase,
|
private val getVoiceBroadcastUseCase: GetVoiceBroadcastUseCase,
|
||||||
) {
|
private val getLiveVoiceBroadcastChunksUseCase: GetLiveVoiceBroadcastChunksUseCase
|
||||||
|
) : VoiceBroadcastPlayer {
|
||||||
|
|
||||||
private val session
|
private val session
|
||||||
get() = sessionHolder.getActiveSession()
|
get() = sessionHolder.getActiveSession()
|
||||||
|
|
||||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
private var voiceBroadcastStateJob: Job? = null
|
private var voiceBroadcastStateJob: Job? = null
|
||||||
private var currentTimeline: Timeline? = null
|
|
||||||
set(value) {
|
|
||||||
field?.removeAllListeners()
|
|
||||||
field?.dispose()
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
|
|
||||||
private val mediaPlayerListener = MediaPlayerListener()
|
private val mediaPlayerListener = MediaPlayerListener()
|
||||||
private var timelineListener: TimelineListener? = null
|
|
||||||
|
|
||||||
private var currentMediaPlayer: MediaPlayer? = null
|
private var currentMediaPlayer: MediaPlayer? = null
|
||||||
private var nextMediaPlayer: MediaPlayer? = null
|
private var nextMediaPlayer: MediaPlayer? = null
|
||||||
@ -74,38 +68,49 @@ class VoiceBroadcastPlayer @Inject constructor(
|
|||||||
}
|
}
|
||||||
private var currentSequence: Int? = null
|
private var currentSequence: Int? = null
|
||||||
|
|
||||||
|
private var fetchPlaylistJob: Job? = null
|
||||||
private var playlist = emptyList<MessageAudioEvent>()
|
private var playlist = emptyList<MessageAudioEvent>()
|
||||||
var currentVoiceBroadcastId: String? = null
|
private var isLive: Boolean = false
|
||||||
|
|
||||||
private var state: State = State.IDLE
|
override var currentVoiceBroadcastId: String? = null
|
||||||
|
|
||||||
|
override var playingState = State.IDLE
|
||||||
@MainThread
|
@MainThread
|
||||||
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>()
|
|
||||||
|
|
||||||
fun playOrResume(roomId: String, eventId: String) {
|
/**
|
||||||
val hasChanged = currentVoiceBroadcastId != eventId
|
* Map voiceBroadcastId to listeners.
|
||||||
|
*/
|
||||||
|
private val listeners: MutableMap<String, CopyOnWriteArrayList<Listener>> = mutableMapOf()
|
||||||
|
|
||||||
|
override fun playOrResume(roomId: String, voiceBroadcastId: String) {
|
||||||
|
val hasChanged = currentVoiceBroadcastId != voiceBroadcastId
|
||||||
when {
|
when {
|
||||||
hasChanged -> startPlayback(roomId, eventId)
|
hasChanged -> startPlayback(roomId, voiceBroadcastId)
|
||||||
state == State.PAUSED -> resumePlayback()
|
playingState == State.PAUSED -> resumePlayback()
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pause() {
|
override fun pause() {
|
||||||
currentMediaPlayer?.pause()
|
currentMediaPlayer?.pause()
|
||||||
currentVoiceBroadcastId?.let { playbackTracker.pausePlayback(it) }
|
currentVoiceBroadcastId?.let { playbackTracker.pausePlayback(it) }
|
||||||
state = State.PAUSED
|
playingState = State.PAUSED
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stop() {
|
override fun stop() {
|
||||||
// Stop playback
|
// Stop playback
|
||||||
currentMediaPlayer?.stop()
|
currentMediaPlayer?.stop()
|
||||||
currentVoiceBroadcastId?.let { playbackTracker.stopPlayback(it) }
|
currentVoiceBroadcastId?.let { playbackTracker.stopPlayback(it) }
|
||||||
|
isLive = false
|
||||||
|
|
||||||
// Release current player
|
// Release current player
|
||||||
release(currentMediaPlayer)
|
release(currentMediaPlayer)
|
||||||
@ -119,50 +124,78 @@ class VoiceBroadcastPlayer @Inject constructor(
|
|||||||
voiceBroadcastStateJob?.cancel()
|
voiceBroadcastStateJob?.cancel()
|
||||||
voiceBroadcastStateJob = null
|
voiceBroadcastStateJob = null
|
||||||
|
|
||||||
// In case of live broadcast, stop observing new chunks
|
// Do not fetch the playlist anymore
|
||||||
currentTimeline = null
|
fetchPlaylistJob?.cancel()
|
||||||
timelineListener = null
|
fetchPlaylistJob = null
|
||||||
|
|
||||||
// Update state
|
// Update state
|
||||||
state = State.IDLE
|
playingState = State.IDLE
|
||||||
|
|
||||||
// Clear playlist
|
// Clear playlist
|
||||||
playlist = emptyList()
|
playlist = emptyList()
|
||||||
currentSequence = null
|
currentSequence = null
|
||||||
|
|
||||||
currentRoomId = null
|
currentRoomId = null
|
||||||
currentVoiceBroadcastId = null
|
currentVoiceBroadcastId = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addListener(listener: Listener) {
|
override fun addListener(voiceBroadcastId: String, listener: Listener) {
|
||||||
listeners.add(listener)
|
listeners[voiceBroadcastId]?.add(listener) ?: run {
|
||||||
listener.onStateChanged(state)
|
listeners[voiceBroadcastId] = CopyOnWriteArrayList<Listener>().apply { add(listener) }
|
||||||
|
}
|
||||||
|
if (voiceBroadcastId == currentVoiceBroadcastId) listener.onStateChanged(playingState) else listener.onStateChanged(State.IDLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeListener(listener: Listener) {
|
override fun removeListener(voiceBroadcastId: String, listener: Listener) {
|
||||||
listeners.remove(listener)
|
listeners[voiceBroadcastId]?.remove(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startPlayback(roomId: String, eventId: String) {
|
private fun startPlayback(roomId: String, eventId: String) {
|
||||||
val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId")
|
|
||||||
// Stop listening previous voice broadcast if any
|
// Stop listening previous voice broadcast if any
|
||||||
if (state != State.IDLE) stop()
|
if (playingState != State.IDLE) stop()
|
||||||
|
|
||||||
currentRoomId = roomId
|
currentRoomId = roomId
|
||||||
currentVoiceBroadcastId = eventId
|
currentVoiceBroadcastId = eventId
|
||||||
|
|
||||||
state = State.BUFFERING
|
playingState = State.BUFFERING
|
||||||
|
|
||||||
val voiceBroadcastState = getVoiceBroadcastUseCase.execute(roomId, eventId)?.content?.voiceBroadcastState
|
val voiceBroadcastState = getVoiceBroadcastUseCase.execute(roomId, eventId)?.content?.voiceBroadcastState
|
||||||
if (voiceBroadcastState == VoiceBroadcastState.STOPPED) {
|
isLive = voiceBroadcastState != null && voiceBroadcastState != VoiceBroadcastState.STOPPED
|
||||||
// Get static playlist
|
fetchPlaylistAndStartPlayback(roomId, eventId)
|
||||||
updatePlaylist(getExistingChunks(room, eventId))
|
}
|
||||||
startPlayback(false)
|
|
||||||
} else {
|
private fun fetchPlaylistAndStartPlayback(roomId: String, voiceBroadcastId: String) {
|
||||||
playLiveVoiceBroadcast(room, eventId)
|
fetchPlaylistJob = getLiveVoiceBroadcastChunksUseCase.execute(roomId, voiceBroadcastId)
|
||||||
|
.onEach(this::updatePlaylist)
|
||||||
|
.launchIn(coroutineScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updatePlaylist(playlist: List<MessageAudioEvent>) {
|
||||||
|
this.playlist = playlist.sortedBy { it.getVoiceBroadcastChunk()?.sequence?.toLong() ?: it.root.originServerTs }
|
||||||
|
onPlaylistUpdated()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onPlaylistUpdated() {
|
||||||
|
when (playingState) {
|
||||||
|
State.PLAYING -> {
|
||||||
|
if (nextMediaPlayer == null) {
|
||||||
|
coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
State.PAUSED -> {
|
||||||
|
if (nextMediaPlayer == null) {
|
||||||
|
coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
State.BUFFERING -> {
|
||||||
|
val newMediaContent = getNextAudioContent()
|
||||||
|
if (newMediaContent != null) startPlayback()
|
||||||
|
}
|
||||||
|
State.IDLE -> startPlayback()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startPlayback(isLive: Boolean) {
|
private fun startPlayback() {
|
||||||
val event = if (isLive) playlist.lastOrNull() else playlist.firstOrNull()
|
val event = if (isLive) playlist.lastOrNull() else playlist.firstOrNull()
|
||||||
val content = event?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return }
|
val content = event?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return }
|
||||||
val sequence = event.getVoiceBroadcastChunk()?.sequence
|
val sequence = event.getVoiceBroadcastChunk()?.sequence
|
||||||
@ -172,7 +205,7 @@ class VoiceBroadcastPlayer @Inject constructor(
|
|||||||
currentMediaPlayer?.start()
|
currentMediaPlayer?.start()
|
||||||
currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) }
|
currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) }
|
||||||
currentSequence = sequence
|
currentSequence = sequence
|
||||||
withContext(Dispatchers.Main) { state = State.PLAYING }
|
withContext(Dispatchers.Main) { playingState = State.PLAYING }
|
||||||
nextMediaPlayer = prepareNextMediaPlayer()
|
nextMediaPlayer = prepareNextMediaPlayer()
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
Timber.e(failure, "Unable to start playback")
|
Timber.e(failure, "Unable to start playback")
|
||||||
@ -181,39 +214,15 @@ class VoiceBroadcastPlayer @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun playLiveVoiceBroadcast(room: Room, eventId: String) {
|
|
||||||
room.timelineService().getTimelineEvent(eventId)?.root?.asVoiceBroadcastEvent() ?: error("Cannot retrieve voice broadcast $eventId")
|
|
||||||
updatePlaylist(getExistingChunks(room, eventId))
|
|
||||||
startPlayback(true)
|
|
||||||
observeIncomingEvents(room, eventId)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getExistingChunks(room: Room, eventId: String): List<MessageAudioEvent> {
|
|
||||||
return room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, eventId)
|
|
||||||
.mapNotNull { it.root.asMessageAudioEvent() }
|
|
||||||
.filter { it.isVoiceBroadcast() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun observeIncomingEvents(room: Room, eventId: String) {
|
|
||||||
currentTimeline = room.timelineService().createTimeline(null, TimelineSettings(5)).also { timeline ->
|
|
||||||
timelineListener = TimelineListener(eventId).also { timeline.addListener(it) }
|
|
||||||
timeline.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun resumePlayback() {
|
private fun resumePlayback() {
|
||||||
currentMediaPlayer?.start()
|
currentMediaPlayer?.start()
|
||||||
currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) }
|
currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) }
|
||||||
state = State.PLAYING
|
playingState = State.PLAYING
|
||||||
}
|
|
||||||
|
|
||||||
private fun updatePlaylist(playlist: List<MessageAudioEvent>) {
|
|
||||||
this.playlist = playlist.sortedBy { it.getVoiceBroadcastChunk()?.sequence?.toLong() ?: it.root.originServerTs }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getNextAudioContent(): MessageAudioContent? {
|
private fun getNextAudioContent(): MessageAudioContent? {
|
||||||
val nextSequence = currentSequence?.plus(1)
|
val nextSequence = currentSequence?.plus(1)
|
||||||
?: timelineListener?.let { playlist.lastOrNull()?.sequence }
|
?: playlist.lastOrNull()?.sequence
|
||||||
?: 1
|
?: 1
|
||||||
return playlist.find { it.getVoiceBroadcastChunk()?.sequence == nextSequence }?.content
|
return playlist.find { it.getVoiceBroadcastChunk()?.sequence == nextSequence }?.content
|
||||||
}
|
}
|
||||||
@ -259,37 +268,6 @@ class VoiceBroadcastPlayer @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class TimelineListener(private val voiceBroadcastId: String) : Timeline.Listener {
|
|
||||||
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
|
||||||
val currentSequences = playlist.map { it.sequence }
|
|
||||||
val newChunks = snapshot
|
|
||||||
.mapNotNull { timelineEvent ->
|
|
||||||
timelineEvent.root.asMessageAudioEvent()
|
|
||||||
?.takeIf { it.isVoiceBroadcast() && it.getVoiceBroadcastEventId() == voiceBroadcastId && it.sequence !in currentSequences }
|
|
||||||
}
|
|
||||||
if (newChunks.isEmpty()) return
|
|
||||||
updatePlaylist(playlist + newChunks)
|
|
||||||
|
|
||||||
when (state) {
|
|
||||||
State.PLAYING -> {
|
|
||||||
if (nextMediaPlayer == null) {
|
|
||||||
coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
State.PAUSED -> {
|
|
||||||
if (nextMediaPlayer == null) {
|
|
||||||
coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
State.BUFFERING -> {
|
|
||||||
val newMediaContent = getNextAudioContent()
|
|
||||||
if (newMediaContent != null) startPlayback(true)
|
|
||||||
}
|
|
||||||
State.IDLE -> startPlayback(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class MediaPlayerListener : MediaPlayer.OnInfoListener, MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener {
|
private inner class MediaPlayerListener : MediaPlayer.OnInfoListener, MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener {
|
||||||
|
|
||||||
override fun onInfo(mp: MediaPlayer, what: Int, extra: Int): Boolean {
|
override fun onInfo(mp: MediaPlayer, what: Int, extra: Int): Boolean {
|
||||||
@ -309,13 +287,13 @@ class VoiceBroadcastPlayer @Inject constructor(
|
|||||||
val roomId = currentRoomId ?: return
|
val roomId = currentRoomId ?: return
|
||||||
val voiceBroadcastId = currentVoiceBroadcastId ?: return
|
val voiceBroadcastId = currentVoiceBroadcastId ?: return
|
||||||
val voiceBroadcastEventContent = getVoiceBroadcastUseCase.execute(roomId, voiceBroadcastId)?.content ?: return
|
val voiceBroadcastEventContent = getVoiceBroadcastUseCase.execute(roomId, voiceBroadcastId)?.content ?: return
|
||||||
val isLive = voiceBroadcastEventContent.voiceBroadcastState != null && voiceBroadcastEventContent.voiceBroadcastState != VoiceBroadcastState.STOPPED
|
isLive = voiceBroadcastEventContent.voiceBroadcastState != null && voiceBroadcastEventContent.voiceBroadcastState != VoiceBroadcastState.STOPPED
|
||||||
|
|
||||||
if (!isLive && voiceBroadcastEventContent.lastChunkSequence == currentSequence) {
|
if (!isLive && voiceBroadcastEventContent.lastChunkSequence == currentSequence) {
|
||||||
// We'll not receive new chunks anymore so we can stop the live listening
|
// We'll not receive new chunks anymore so we can stop the live listening
|
||||||
stop()
|
stop()
|
||||||
} else {
|
} else {
|
||||||
state = State.BUFFERING
|
playingState = State.BUFFERING
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,15 +302,4 @@ class VoiceBroadcastPlayer @Inject constructor(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class State {
|
|
||||||
PLAYING,
|
|
||||||
PAUSED,
|
|
||||||
BUFFERING,
|
|
||||||
IDLE
|
|
||||||
}
|
|
||||||
|
|
||||||
fun interface Listener {
|
|
||||||
fun onStateChanged(state: State)
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -0,0 +1,132 @@
|
|||||||
|
/*
|
||||||
|
* 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.listening.usecase
|
||||||
|
|
||||||
|
import im.vector.app.core.di.ActiveSessionHolder
|
||||||
|
import im.vector.app.features.voicebroadcast.getVoiceBroadcastEventId
|
||||||
|
import im.vector.app.features.voicebroadcast.isVoiceBroadcast
|
||||||
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
|
||||||
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
||||||
|
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
||||||
|
import im.vector.app.features.voicebroadcast.sequence
|
||||||
|
import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastUseCase
|
||||||
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.runningReduce
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
|
||||||
|
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||||
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||||
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a [Flow] of [MessageAudioEvent]s related to the given voice broadcast.
|
||||||
|
*/
|
||||||
|
class GetLiveVoiceBroadcastChunksUseCase @Inject constructor(
|
||||||
|
private val activeSessionHolder: ActiveSessionHolder,
|
||||||
|
private val getVoiceBroadcastUseCase: GetVoiceBroadcastUseCase,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun execute(roomId: String, voiceBroadcastId: String): Flow<List<MessageAudioEvent>> {
|
||||||
|
val session = activeSessionHolder.getSafeActiveSession() ?: return emptyFlow()
|
||||||
|
val room = session.roomService().getRoom(roomId) ?: return emptyFlow()
|
||||||
|
val timeline = room.timelineService().createTimeline(null, TimelineSettings(5))
|
||||||
|
|
||||||
|
// Get initial chunks
|
||||||
|
val existingChunks = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcastId)
|
||||||
|
.mapNotNull { timelineEvent -> timelineEvent.root.asMessageAudioEvent().takeIf { it.isVoiceBroadcast() } }
|
||||||
|
|
||||||
|
val voiceBroadcastEvent = getVoiceBroadcastUseCase.execute(roomId, voiceBroadcastId)
|
||||||
|
val voiceBroadcastState = voiceBroadcastEvent?.content?.voiceBroadcastState
|
||||||
|
|
||||||
|
return if (voiceBroadcastState == null || voiceBroadcastState == VoiceBroadcastState.STOPPED) {
|
||||||
|
// Just send the existing chunks if voice broadcast is stopped
|
||||||
|
flowOf(existingChunks)
|
||||||
|
} else {
|
||||||
|
// Observe new timeline events if voice broadcast is ongoing
|
||||||
|
callbackFlow {
|
||||||
|
// Init with existing chunks
|
||||||
|
send(existingChunks)
|
||||||
|
|
||||||
|
// Observe new timeline events
|
||||||
|
val listener = object : Timeline.Listener {
|
||||||
|
private var lastEventId: String? = null
|
||||||
|
private var lastSequence: Int? = null
|
||||||
|
|
||||||
|
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
||||||
|
val newEvents = lastEventId?.let { eventId -> snapshot.subList(0, snapshot.indexOfFirst { it.eventId == eventId }) } ?: snapshot
|
||||||
|
|
||||||
|
// Detect a potential stopped voice broadcast state event
|
||||||
|
val stopEvent = newEvents.findStopEvent()
|
||||||
|
if (stopEvent != null) {
|
||||||
|
lastSequence = stopEvent.content?.lastChunkSequence
|
||||||
|
}
|
||||||
|
|
||||||
|
val newChunks = newEvents.mapToChunkEvents(voiceBroadcastId, voiceBroadcastEvent.root.senderId)
|
||||||
|
|
||||||
|
// Notify about new chunks
|
||||||
|
if (newChunks.isNotEmpty()) {
|
||||||
|
trySend(newChunks)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Automatically stop observing the timeline if the last chunk has been received
|
||||||
|
if (lastSequence != null && newChunks.any { it.sequence == lastSequence }) {
|
||||||
|
timeline.removeListener(this)
|
||||||
|
timeline.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
lastEventId = snapshot.firstOrNull()?.eventId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
timeline.addListener(listener)
|
||||||
|
timeline.start()
|
||||||
|
awaitClose {
|
||||||
|
timeline.removeListener(listener)
|
||||||
|
timeline.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.runningReduce { accumulator: List<MessageAudioEvent>, value: List<MessageAudioEvent> -> accumulator.plus(value) }
|
||||||
|
.map { events -> events.distinctBy { it.sequence } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a [VoiceBroadcastEvent] with a [VoiceBroadcastState.STOPPED] state.
|
||||||
|
*/
|
||||||
|
private fun List<TimelineEvent>.findStopEvent(): VoiceBroadcastEvent? =
|
||||||
|
this.mapNotNull { it.root.asVoiceBroadcastEvent() }
|
||||||
|
.find { it.content?.voiceBroadcastState == VoiceBroadcastState.STOPPED }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform the list of [TimelineEvent] to a mapped list of [MessageAudioEvent] related to a given voice broadcast.
|
||||||
|
*/
|
||||||
|
private fun List<TimelineEvent>.mapToChunkEvents(voiceBroadcastId: String, senderId: String?): List<MessageAudioEvent> =
|
||||||
|
this.mapNotNull { timelineEvent ->
|
||||||
|
timelineEvent.root.asMessageAudioEvent()
|
||||||
|
?.takeIf {
|
||||||
|
it.isVoiceBroadcast() && it.getVoiceBroadcastEventId() == voiceBroadcastId &&
|
||||||
|
it.root.senderId == senderId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.app.features.voicebroadcast
|
package im.vector.app.features.voicebroadcast.recording
|
||||||
|
|
||||||
import androidx.annotation.IntRange
|
import androidx.annotation.IntRange
|
||||||
import im.vector.app.features.voice.VoiceRecorder
|
import im.vector.app.features.voice.VoiceRecorder
|
@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.app.features.voicebroadcast
|
package im.vector.app.features.voicebroadcast.recording
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.media.MediaRecorder
|
import android.media.MediaRecorder
|
@ -14,13 +14,13 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.app.features.voicebroadcast.usecase
|
package im.vector.app.features.voicebroadcast.recording.usecase
|
||||||
|
|
||||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
|
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
|
||||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
|
|
||||||
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
|
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
|
||||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
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 im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
|
||||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||||
import org.matrix.android.sdk.api.session.Session
|
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.events.model.toContent
|
@ -14,13 +14,13 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.app.features.voicebroadcast.usecase
|
package im.vector.app.features.voicebroadcast.recording.usecase
|
||||||
|
|
||||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
|
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
|
||||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
|
|
||||||
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
|
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
|
||||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
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 im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
|
||||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||||
import org.matrix.android.sdk.api.session.Session
|
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.events.model.toContent
|
@ -14,26 +14,33 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.app.features.voicebroadcast.usecase
|
package im.vector.app.features.voicebroadcast.recording.usecase
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import im.vector.app.core.resources.BuildMeta
|
import im.vector.app.core.resources.BuildMeta
|
||||||
import im.vector.app.features.attachments.toContentAttachmentData
|
import im.vector.app.features.attachments.toContentAttachmentData
|
||||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
|
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
|
||||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
|
import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure
|
||||||
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
|
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
|
||||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastChunk
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastChunk
|
||||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
||||||
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
|
||||||
|
import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase
|
||||||
import im.vector.lib.multipicker.utils.toMultiPickerAudioType
|
import im.vector.lib.multipicker.utils.toMultiPickerAudioType
|
||||||
|
import org.jetbrains.annotations.VisibleForTesting
|
||||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
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.RelationType
|
||||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
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.getRoom
|
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.Room
|
||||||
|
import org.matrix.android.sdk.api.session.room.getStateEvent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -43,6 +50,7 @@ class StartVoiceBroadcastUseCase @Inject constructor(
|
|||||||
private val voiceBroadcastRecorder: VoiceBroadcastRecorder?,
|
private val voiceBroadcastRecorder: VoiceBroadcastRecorder?,
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
|
private val getOngoingVoiceBroadcastsUseCase: GetOngoingVoiceBroadcastsUseCase,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun execute(roomId: String): Result<Unit> = runCatching {
|
suspend fun execute(roomId: String): Result<Unit> = runCatching {
|
||||||
@ -50,18 +58,8 @@ class StartVoiceBroadcastUseCase @Inject constructor(
|
|||||||
|
|
||||||
Timber.d("## StartVoiceBroadcastUseCase: Start voice broadcast requested")
|
Timber.d("## StartVoiceBroadcastUseCase: Start voice broadcast requested")
|
||||||
|
|
||||||
val onGoingVoiceBroadcastEvents = room.stateService().getStateEvents(
|
assertCanStartVoiceBroadcast(room)
|
||||||
setOf(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO),
|
startVoiceBroadcast(room)
|
||||||
QueryStringValue.IsNotEmpty
|
|
||||||
)
|
|
||||||
.mapNotNull { it.asVoiceBroadcastEvent() }
|
|
||||||
.filter { it.content?.voiceBroadcastState != null && 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) {
|
private suspend fun startVoiceBroadcast(room: Room) {
|
||||||
@ -107,4 +105,36 @@ class StartVoiceBroadcastUseCase @Inject constructor(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun assertCanStartVoiceBroadcast(room: Room) {
|
||||||
|
assertHasEnoughPowerLevels(room)
|
||||||
|
assertNoOngoingVoiceBroadcast(room)
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
fun assertHasEnoughPowerLevels(room: Room) {
|
||||||
|
val powerLevelsHelper = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
|
||||||
|
?.content
|
||||||
|
?.toModel<PowerLevelsContent>()
|
||||||
|
?.let { PowerLevelsHelper(it) }
|
||||||
|
|
||||||
|
if (powerLevelsHelper?.isUserAllowedToSend(session.myUserId, true, VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO) != true) {
|
||||||
|
Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: no permission")
|
||||||
|
throw VoiceBroadcastFailure.RecordingError.NoPermission
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
fun assertNoOngoingVoiceBroadcast(room: Room) {
|
||||||
|
when {
|
||||||
|
voiceBroadcastRecorder?.state == VoiceBroadcastRecorder.State.Recording || voiceBroadcastRecorder?.state == VoiceBroadcastRecorder.State.Paused -> {
|
||||||
|
Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: another voice broadcast")
|
||||||
|
throw VoiceBroadcastFailure.RecordingError.UserAlreadyBroadcasting
|
||||||
|
}
|
||||||
|
getOngoingVoiceBroadcastsUseCase.execute(room.roomId).isNotEmpty() -> {
|
||||||
|
Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: user already broadcasting")
|
||||||
|
throw VoiceBroadcastFailure.RecordingError.BlockedBySomeoneElse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -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.recording.usecase
|
||||||
|
|
||||||
|
import im.vector.app.core.di.ActiveSessionHolder
|
||||||
|
import im.vector.app.features.voicebroadcast.VoiceBroadcastHelper
|
||||||
|
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
||||||
|
import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase
|
||||||
|
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||||
|
import org.matrix.android.sdk.api.session.getRoom
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||||
|
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop ongoing voice broadcast if any.
|
||||||
|
*/
|
||||||
|
class StopOngoingVoiceBroadcastUseCase @Inject constructor(
|
||||||
|
private val activeSessionHolder: ActiveSessionHolder,
|
||||||
|
private val getOngoingVoiceBroadcastsUseCase: GetOngoingVoiceBroadcastsUseCase,
|
||||||
|
private val voiceBroadcastHelper: VoiceBroadcastHelper,
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun execute() {
|
||||||
|
Timber.d("## StopOngoingVoiceBroadcastUseCase: Stop ongoing voice broadcast requested")
|
||||||
|
|
||||||
|
val session = activeSessionHolder.getSafeActiveSession() ?: run {
|
||||||
|
Timber.w("## StopOngoingVoiceBroadcastUseCase: no active session")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// FIXME Iterate only on recent rooms for the moment, improve this
|
||||||
|
val recentRooms = session.roomService()
|
||||||
|
.getBreadcrumbs(roomSummaryQueryParams {
|
||||||
|
displayName = QueryStringValue.NoCondition
|
||||||
|
memberships = listOf(Membership.JOIN)
|
||||||
|
})
|
||||||
|
.mapNotNull { session.getRoom(it.roomId) }
|
||||||
|
|
||||||
|
recentRooms
|
||||||
|
.forEach { room ->
|
||||||
|
val ongoingVoiceBroadcasts = getOngoingVoiceBroadcastsUseCase.execute(room.roomId)
|
||||||
|
val myOngoingVoiceBroadcastId = ongoingVoiceBroadcasts.find { it.root.stateKey == session.myUserId }?.reference?.eventId
|
||||||
|
val initialEvent = myOngoingVoiceBroadcastId?.let { room.timelineService().getTimelineEvent(it)?.root?.asVoiceBroadcastEvent() }
|
||||||
|
if (myOngoingVoiceBroadcastId != null && initialEvent?.content?.deviceId == session.sessionParams.deviceId) {
|
||||||
|
voiceBroadcastHelper.stopVoiceBroadcast(room.roomId)
|
||||||
|
return // No need to iterate more as we should not have more than one recording VB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -14,13 +14,13 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.app.features.voicebroadcast.usecase
|
package im.vector.app.features.voicebroadcast.recording.usecase
|
||||||
|
|
||||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
|
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
|
||||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
|
|
||||||
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
|
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
|
||||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
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 im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
|
||||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||||
import org.matrix.android.sdk.api.session.Session
|
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.events.model.toContent
|
@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* 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.core.di.ActiveSessionHolder
|
||||||
|
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
|
||||||
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
|
||||||
|
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.getRoom
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class GetOngoingVoiceBroadcastsUseCase @Inject constructor(
|
||||||
|
private val activeSessionHolder: ActiveSessionHolder,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun execute(roomId: String): List<VoiceBroadcastEvent> {
|
||||||
|
val session = activeSessionHolder.getSafeActiveSession() ?: run {
|
||||||
|
Timber.d("## GetOngoingVoiceBroadcastsUseCase: no active session")
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId")
|
||||||
|
|
||||||
|
Timber.d("## GetLastVoiceBroadcastUseCase: get last voice broadcast in $roomId")
|
||||||
|
|
||||||
|
return room.stateService().getStateEvents(
|
||||||
|
setOf(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO),
|
||||||
|
QueryStringValue.IsNotEmpty
|
||||||
|
)
|
||||||
|
.mapNotNull { it.asVoiceBroadcastEvent() }
|
||||||
|
.filter { it.content?.voiceBroadcastState != null && it.content?.voiceBroadcastState != VoiceBroadcastState.STOPPED }
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
9
vector/src/main/res/drawable/ic_composer_full_screen.xml
Normal file
9
vector/src/main/res/drawable/ic_composer_full_screen.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="48dp"
|
||||||
|
android:height="48dp"
|
||||||
|
android:viewportWidth="48"
|
||||||
|
android:viewportHeight="48">
|
||||||
|
<path
|
||||||
|
android:pathData="M17.125,31.5C16.944,31.5 16.795,31.441 16.677,31.323C16.559,31.205 16.5,31.056 16.5,30.875V25.875C16.5,25.694 16.559,25.545 16.677,25.427C16.795,25.309 16.944,25.25 17.125,25.25C17.306,25.25 17.455,25.309 17.573,25.427C17.691,25.545 17.75,25.694 17.75,25.875V29.375L29.375,17.75H25.875C25.694,17.75 25.545,17.691 25.427,17.573C25.309,17.455 25.25,17.306 25.25,17.125C25.25,16.944 25.309,16.795 25.427,16.677C25.545,16.559 25.694,16.5 25.875,16.5H30.875C31.056,16.5 31.205,16.559 31.323,16.677C31.441,16.795 31.5,16.944 31.5,17.125V22.125C31.5,22.306 31.441,22.455 31.323,22.573C31.205,22.691 31.056,22.75 30.875,22.75C30.694,22.75 30.545,22.691 30.427,22.573C30.309,22.455 30.25,22.306 30.25,22.125V18.625L18.625,30.25H22.125C22.306,30.25 22.455,30.309 22.573,30.427C22.691,30.545 22.75,30.694 22.75,30.875C22.75,31.056 22.691,31.205 22.573,31.323C22.455,31.441 22.306,31.5 22.125,31.5H17.125Z"
|
||||||
|
android:fillColor="#C1C6CD"/>
|
||||||
|
</vector>
|
13
vector/src/main/res/drawable/ic_text_formatting.xml
Normal file
13
vector/src/main/res/drawable/ic_text_formatting.xml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<group>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M0,0h24v24h-24z"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M3,20.667C3,21.4 3.6,22 4.333,22H20.333C21.067,22 21.667,21.4 21.667,20.667C21.667,19.933 21.067,19.333 20.333,19.333H4.333C3.6,19.333 3,19.933 3,20.667ZM9,13.733H15.667L16.547,15.867C16.747,16.347 17.213,16.667 17.733,16.667C18.653,16.667 19.267,15.72 18.907,14.88L13.733,2.92C13.493,2.36 12.947,2 12.333,2C11.72,2 11.173,2.36 10.933,2.92L5.76,14.88C5.4,15.72 6.027,16.667 6.947,16.667C7.467,16.667 7.933,16.347 8.133,15.867L9,13.733ZM12.333,4.64L14.827,11.333H9.84L12.333,4.64Z"
|
||||||
|
android:fillColor="#0DBD8B"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
18
vector/src/main/res/drawable/ic_text_formatting_disabled.xml
Normal file
18
vector/src/main/res/drawable/ic_text_formatting_disabled.xml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<group>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M0,0h24v24h-24z"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M9,15.733H15.667L16.547,17.867C16.747,18.347 17.213,18.667 17.733,18.667C18.653,18.667 19.267,17.72 18.907,16.88L13.733,4.92C13.493,4.36 12.947,4 12.333,4C11.72,4 11.173,4.36 10.933,4.92L5.76,16.88C5.4,17.72 6.027,18.667 6.947,18.667C7.467,18.667 7.933,18.347 8.133,17.867L9,15.733ZM12.333,6.64L14.827,13.333H9.84L12.333,6.64Z"
|
||||||
|
android:fillColor="#0DBD8B"/>
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M2.5,11.667C2.5,12.676 3.324,13.5 4.333,13.5H20.333C21.343,13.5 22.167,12.676 22.167,11.667C22.167,10.657 21.343,9.833 20.333,9.833H4.333C3.324,9.833 2.5,10.657 2.5,11.667Z"
|
||||||
|
android:fillColor="#0DBD8B"
|
||||||
|
android:strokeColor="#ffffff"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
9
vector/src/main/res/drawable/ic_timer.xml
Normal file
9
vector/src/main/res/drawable/ic_timer.xml
Normal 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>
|
12
vector/src/main/res/drawable/ic_voice_broadcast_mic.xml
Normal file
12
vector/src/main/res/drawable/ic_voice_broadcast_mic.xml
Normal 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>
|
@ -1,6 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="?colorSurface">
|
android:background="?colorSurface">
|
||||||
@ -82,5 +83,24 @@
|
|||||||
app:tint="?colorPrimary"
|
app:tint="?colorPrimary"
|
||||||
app:titleTextColor="?vctr_content_primary" />
|
app:titleTextColor="?vctr_content_primary" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="?vctr_list_separator" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.SwitchCompat
|
||||||
|
android:id="@+id/textFormatting"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:drawableStart="@drawable/ic_text_formatting"
|
||||||
|
android:drawablePadding="20dp"
|
||||||
|
android:padding="20dp"
|
||||||
|
android:paddingStart="28dp"
|
||||||
|
android:text="@string/attachment_type_selector_text_formatting"
|
||||||
|
android:textAppearance="@style/TextAppearance.Vector.Subtitle"
|
||||||
|
android:textColor="?vctr_content_primary"
|
||||||
|
app:drawableTint="?colorPrimary"
|
||||||
|
tools:ignore="RtlSymmetry" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</androidx.core.widget.NestedScrollView>
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
tools:constraintSet="@layout/composer_rich_text_layout_constraint_set_compact"
|
tools:constraintSet="@layout/composer_rich_text_layout_constraint_set_compact"
|
||||||
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
|
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
|
||||||
|
|
||||||
@ -104,16 +104,41 @@
|
|||||||
android:background="@drawable/bg_composer_rich_edit_text_single_line" />
|
android:background="@drawable/bg_composer_rich_edit_text_single_line" />
|
||||||
|
|
||||||
<io.element.android.wysiwyg.EditorEditText
|
<io.element.android.wysiwyg.EditorEditText
|
||||||
android:id="@+id/composerEditText"
|
android:id="@+id/richTextComposerEditText"
|
||||||
style="@style/Widget.Vector.EditText.RichTextComposer"
|
style="@style/Widget.Vector.EditText.RichTextComposer"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:nextFocusLeft="@id/composerEditText"
|
android:gravity="top"
|
||||||
android:nextFocusUp="@id/composerEditText"
|
android:nextFocusLeft="@id/richTextComposerEditText"
|
||||||
|
android:nextFocusUp="@id/richTextComposerEditText"
|
||||||
tools:hint="@string/room_message_placeholder"
|
tools:hint="@string/room_message_placeholder"
|
||||||
tools:text="@tools:sample/lorem/random"
|
tools:text="@tools:sample/lorem/random"
|
||||||
tools:ignore="MissingConstraints" />
|
tools:ignore="MissingConstraints" />
|
||||||
|
|
||||||
|
<!-- Use a separate EditText for plain text editing while the rich text editor doesn't support this mode -->
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/plainTextComposerEditText"
|
||||||
|
style="@style/Widget.Vector.EditText.RichTextComposer"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="top"
|
||||||
|
android:nextFocusLeft="@id/plainTextComposerEditText"
|
||||||
|
android:nextFocusUp="@id/plainTextComposerEditText"
|
||||||
|
tools:hint="@string/room_message_placeholder"
|
||||||
|
tools:text="@tools:sample/lorem/random"
|
||||||
|
tools:ignore="MissingConstraints" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/composerFullScreenButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/composerEditTextOuterBorder"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
|
||||||
|
android:src="@drawable/ic_composer_full_screen"
|
||||||
|
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="@string/rich_text_editor_full_screen_toggle" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/sendButton"
|
android:id="@+id/sendButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/composerLayout"
|
android:id="@+id/composerLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent">
|
app:layout_constraintStart_toStartOf="parent">
|
||||||
@ -114,6 +114,7 @@
|
|||||||
android:background="?android:attr/selectableItemBackground"
|
android:background="?android:attr/selectableItemBackground"
|
||||||
android:contentDescription="@string/option_send_files"
|
android:contentDescription="@string/option_send_files"
|
||||||
android:src="@drawable/ic_attachment"
|
android:src="@drawable/ic_attachment"
|
||||||
|
app:layout_constraintVertical_bias="1"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/sendButton"
|
app:layout_constraintBottom_toBottomOf="@id/sendButton"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="@id/sendButton"
|
app:layout_constraintTop_toTopOf="@id/sendButton"
|
||||||
@ -135,13 +136,13 @@
|
|||||||
app:layout_constraintEnd_toEndOf="parent" />
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
<io.element.android.wysiwyg.EditorEditText
|
<io.element.android.wysiwyg.EditorEditText
|
||||||
android:id="@+id/composerEditText"
|
android:id="@+id/richTextComposerEditText"
|
||||||
style="@style/Widget.Vector.EditText.RichTextComposer"
|
style="@style/Widget.Vector.EditText.RichTextComposer"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:hint="@string/room_message_placeholder"
|
android:hint="@string/room_message_placeholder"
|
||||||
android:nextFocusLeft="@id/composerEditText"
|
android:nextFocusLeft="@id/richTextComposerEditText"
|
||||||
android:nextFocusUp="@id/composerEditText"
|
android:nextFocusUp="@id/richTextComposerEditText"
|
||||||
android:layout_marginHorizontal="12dp"
|
android:layout_marginHorizontal="12dp"
|
||||||
android:layout_marginVertical="10dp"
|
android:layout_marginVertical="10dp"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
|
app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
|
||||||
@ -150,6 +151,34 @@
|
|||||||
app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder"
|
app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder"
|
||||||
tools:text="@tools:sample/lorem/random" />
|
tools:text="@tools:sample/lorem/random" />
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/plainTextComposerEditText"
|
||||||
|
style="@style/Widget.Vector.EditText.RichTextComposer"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/room_message_placeholder"
|
||||||
|
android:nextFocusLeft="@id/plainTextComposerEditText"
|
||||||
|
android:nextFocusUp="@id/plainTextComposerEditText"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginVertical="10dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/composerFullScreenButton"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/composerEditTextOuterBorder"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder"
|
||||||
|
tools:text="@tools:sample/lorem/random" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/composerFullScreenButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/composerEditTextOuterBorder"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
|
||||||
|
app:layout_constraintVertical_bias="0"
|
||||||
|
android:src="@drawable/ic_composer_full_screen"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:contentDescription="@string/rich_text_editor_full_screen_toggle" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/sendButton"
|
android:id="@+id/sendButton"
|
||||||
android:layout_width="56dp"
|
android:layout_width="56dp"
|
||||||
@ -163,6 +192,7 @@
|
|||||||
app:layout_constraintTop_toBottomOf="@id/composerEditTextOuterBorder"
|
app:layout_constraintTop_toBottomOf="@id/composerEditTextOuterBorder"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintVertical_bias="1"
|
||||||
tools:ignore="MissingPrefix"
|
tools:ignore="MissingPrefix"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
@ -173,6 +203,7 @@
|
|||||||
app:layout_constraintStart_toEndOf="@id/attachmentButton"
|
app:layout_constraintStart_toEndOf="@id/attachmentButton"
|
||||||
app:layout_constraintEnd_toStartOf="@id/sendButton"
|
app:layout_constraintEnd_toStartOf="@id/sendButton"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintVertical_bias="1"
|
||||||
android:fillViewport="true">
|
android:fillViewport="true">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/composerLayout"
|
android:id="@+id/composerLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent">
|
app:layout_constraintStart_toStartOf="parent">
|
||||||
@ -149,21 +149,49 @@
|
|||||||
app:layout_constraintEnd_toEndOf="parent" />
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
<io.element.android.wysiwyg.EditorEditText
|
<io.element.android.wysiwyg.EditorEditText
|
||||||
android:id="@+id/composerEditText"
|
android:id="@+id/richTextComposerEditText"
|
||||||
style="@style/Widget.Vector.EditText.RichTextComposer"
|
style="@style/Widget.Vector.EditText.RichTextComposer"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:hint="@string/room_message_placeholder"
|
android:hint="@string/room_message_placeholder"
|
||||||
android:nextFocusLeft="@id/composerEditText"
|
android:nextFocusLeft="@id/richTextComposerEditText"
|
||||||
android:nextFocusUp="@id/composerEditText"
|
android:nextFocusUp="@id/richTextComposerEditText"
|
||||||
android:layout_marginHorizontal="12dp"
|
android:layout_marginStart="12dp"
|
||||||
android:layout_marginVertical="10dp"
|
android:layout_marginVertical="10dp"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
|
app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
|
||||||
app:layout_constraintEnd_toEndOf="@id/composerEditTextOuterBorder"
|
app:layout_constraintEnd_toStartOf="@id/composerFullScreenButton"
|
||||||
app:layout_constraintStart_toStartOf="@id/composerEditTextOuterBorder"
|
app:layout_constraintStart_toStartOf="@id/composerEditTextOuterBorder"
|
||||||
app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder"
|
app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder"
|
||||||
tools:text="@tools:sample/lorem/random" />
|
tools:text="@tools:sample/lorem/random" />
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/plainTextComposerEditText"
|
||||||
|
style="@style/Widget.Vector.EditText.RichTextComposer"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/room_message_placeholder"
|
||||||
|
android:nextFocusLeft="@id/plainTextComposerEditText"
|
||||||
|
android:nextFocusUp="@id/plainTextComposerEditText"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginVertical="10dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/composerFullScreenButton"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/composerEditTextOuterBorder"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder"
|
||||||
|
tools:text="@tools:sample/lorem/random" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/composerFullScreenButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/composerEditTextOuterBorder"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
|
||||||
|
app:layout_constraintVertical_bias="0"
|
||||||
|
android:src="@drawable/ic_composer_full_screen"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:contentDescription="@string/rich_text_editor_full_screen_toggle" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/sendButton"
|
android:id="@+id/sendButton"
|
||||||
android:layout_width="56dp"
|
android:layout_width="56dp"
|
||||||
|
@ -0,0 +1,234 @@
|
|||||||
|
<?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/composerLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/related_message_background"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:background="?colorSurface"
|
||||||
|
app:layout_constraintBottom_toTopOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
tools:layout_height="40dp" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/related_message_background_top_separator"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:background="?vctr_list_separator"
|
||||||
|
app:layout_constraintBottom_toTopOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/composerRelatedMessageAvatar"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
android:visibility="invisible"
|
||||||
|
app:layout_constraintBottom_toTopOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/composerRelatedMessageTitle"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:visibility="invisible"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/composerRelatedMessageContent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
tools:text="@tools:sample/first_names" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/composerRelatedMessageContent"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="invisible"
|
||||||
|
app:layout_constraintBottom_toTopOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
tools:text="@tools:sample/lorem/random" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/composerRelatedMessageActionIcon"
|
||||||
|
android:layout_width="20dp"
|
||||||
|
android:layout_height="20dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginBottom="38dp"
|
||||||
|
android:alpha="0"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
app:layout_constraintEnd_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="parent"
|
||||||
|
app:tint="?vctr_content_primary"
|
||||||
|
tools:ignore="MissingConstraints,MissingPrefix"
|
||||||
|
tools:src="@drawable/ic_edit" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/composerRelatedMessageImage"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
app:layout_constraintBottom_toTopOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="parent"
|
||||||
|
tools:ignore="MissingPrefix"
|
||||||
|
tools:src="@tools:sample/backgrounds/scenic" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/composerRelatedMessageCloseButton"
|
||||||
|
android:layout_width="22dp"
|
||||||
|
android:layout_height="22dp"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:contentDescription="@string/action_cancel"
|
||||||
|
android:src="@drawable/ic_close_round"
|
||||||
|
android:visibility="invisible"
|
||||||
|
app:layout_constraintBottom_toTopOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="parent"
|
||||||
|
app:tint="?colorError"
|
||||||
|
tools:ignore="MissingPrefix"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Barrier
|
||||||
|
android:id="@+id/composer_preview_barrier"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:barrierDirection="bottom"
|
||||||
|
app:barrierMargin="8dp"
|
||||||
|
app:constraint_referenced_ids="composerRelatedMessageContent,composerRelatedMessageActionIcon"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/attachmentButton"
|
||||||
|
android:layout_width="@dimen/composer_attachment_size"
|
||||||
|
android:layout_height="@dimen/composer_attachment_size"
|
||||||
|
android:layout_margin="@dimen/composer_attachment_margin"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:contentDescription="@string/option_send_files"
|
||||||
|
android:src="@drawable/ic_attachment"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/sendButton"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/sendButton"
|
||||||
|
app:layout_goneMarginBottom="57dp"
|
||||||
|
app:layout_constraintVertical_bias="1"
|
||||||
|
tools:ignore="MissingPrefix" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/composerEditTextOuterBorder"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:minHeight="40dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginHorizontal="12dp"
|
||||||
|
android:background="@drawable/bg_composer_rich_edit_text_expanded"
|
||||||
|
app:layout_constraintVertical_bias="0"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/sendButton"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
|
<io.element.android.wysiwyg.EditorEditText
|
||||||
|
android:id="@+id/richTextComposerEditText"
|
||||||
|
style="@style/Widget.Vector.EditText.RichTextComposer"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:hint="@string/room_message_placeholder"
|
||||||
|
android:nextFocusLeft="@id/richTextComposerEditText"
|
||||||
|
android:nextFocusUp="@id/richTextComposerEditText"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginVertical="10dp"
|
||||||
|
android:gravity="top"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/composerFullScreenButton"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/composerEditTextOuterBorder"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder"
|
||||||
|
tools:text="@tools:sample/lorem/random" />
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/plainTextComposerEditText"
|
||||||
|
style="@style/Widget.Vector.EditText.RichTextComposer"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:hint="@string/room_message_placeholder"
|
||||||
|
android:nextFocusLeft="@id/plainTextComposerEditText"
|
||||||
|
android:nextFocusUp="@id/plainTextComposerEditText"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginVertical="10dp"
|
||||||
|
android:gravity="top"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/composerFullScreenButton"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/composerEditTextOuterBorder"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder"
|
||||||
|
tools:text="@tools:sample/lorem/random" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/composerFullScreenButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/composerEditTextOuterBorder"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
|
||||||
|
app:layout_constraintVertical_bias="0"
|
||||||
|
android:src="@drawable/ic_composer_full_screen"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:contentDescription="@string/rich_text_editor_full_screen_toggle" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/sendButton"
|
||||||
|
android:layout_width="56dp"
|
||||||
|
android:layout_height="@dimen/composer_min_height"
|
||||||
|
android:layout_marginEnd="2dp"
|
||||||
|
android:background="@drawable/bg_send"
|
||||||
|
android:contentDescription="@string/action_send"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:src="@drawable/ic_send"
|
||||||
|
android:visibility="invisible"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintVertical_bias="1"
|
||||||
|
tools:ignore="MissingPrefix"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<HorizontalScrollView android:id="@+id/richTextMenuScrollView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/sendButton"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/attachmentButton"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/sendButton"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintVertical_bias="1"
|
||||||
|
android:fillViewport="true">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/richTextMenu"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</HorizontalScrollView>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/voiceMessageMicButton"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:contentDescription="@string/a11y_start_voice_message"
|
||||||
|
android:src="@drawable/ic_voice_mic"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
-->
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -4,7 +4,7 @@
|
|||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<im.vector.app.features.home.room.detail.composer.PlainTextComposerLayout
|
<im.vector.app.features.home.room.detail.composer.PlainTextComposerLayout
|
||||||
android:id="@+id/composerLayout"
|
android:id="@+id/composerLayout"
|
||||||
@ -19,7 +19,7 @@
|
|||||||
<im.vector.app.features.home.room.detail.composer.RichTextComposerLayout
|
<im.vector.app.features.home.room.detail.composer.RichTextComposerLayout
|
||||||
android:id="@+id/richTextComposerLayout"
|
android:id="@+id/richTextComposerLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
android:background="?android:colorBackground"
|
android:background="?android:colorBackground"
|
||||||
android:minHeight="56dp"
|
android:minHeight="56dp"
|
||||||
android:transitionName="composer"
|
android:transitionName="composer"
|
||||||
|
@ -6,6 +6,21 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<!-- ========================
|
||||||
|
/!\ Constraints for this layout are defined in external layout files that are used as constraint set for animation.
|
||||||
|
/!\ These 2 files must be modified to stay coherent!
|
||||||
|
======================== -->
|
||||||
|
|
||||||
|
<View android:id="@+id/scrim"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:background="#44000000" />
|
||||||
|
|
||||||
<com.google.android.material.appbar.AppBarLayout
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
android:id="@+id/appBarLayout"
|
android:id="@+id/appBarLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -98,6 +113,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingStart="20dp"
|
android:paddingStart="20dp"
|
||||||
android:paddingEnd="20dp"
|
android:paddingEnd="20dp"
|
||||||
|
android:visibility="gone"
|
||||||
app:layout_constraintBottom_toTopOf="@id/bottomBarrier"
|
app:layout_constraintBottom_toTopOf="@id/bottomBarrier"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
@ -128,6 +144,7 @@
|
|||||||
android:id="@+id/composerContainer"
|
android:id="@+id/composerContainer"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?android:colorBackground"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintBottom_toBottomOf="parent" />
|
app:layout_constraintBottom_toBottomOf="parent" />
|
||||||
@ -165,6 +182,7 @@
|
|||||||
android:layout_margin="16dp"
|
android:layout_margin="16dp"
|
||||||
android:contentDescription="@string/a11y_jump_to_bottom"
|
android:contentDescription="@string/a11y_jump_to_bottom"
|
||||||
android:src="@drawable/ic_expand_more"
|
android:src="@drawable/ic_expand_more"
|
||||||
|
android:visibility="gone"
|
||||||
app:backgroundTint="#FFFFFF"
|
app:backgroundTint="#FFFFFF"
|
||||||
app:badgeBackgroundColor="?colorPrimary"
|
app:badgeBackgroundColor="?colorPrimary"
|
||||||
app:badgeTextColor="?colorOnPrimary"
|
app:badgeTextColor="?colorOnPrimary"
|
||||||
|
258
vector/src/main/res/layout/fragment_timeline_fullscreen.xml
Normal file
258
vector/src/main/res/layout/fragment_timeline_fullscreen.xml
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
<?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/rootConstraintLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<!-- ========================
|
||||||
|
/!\ Constraints for this layout are defined in external layout files that are used as constraint set for animation.
|
||||||
|
/!\ These 2 files must be modified to stay coherent!
|
||||||
|
======================== -->
|
||||||
|
|
||||||
|
<View android:id="@+id/scrim"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:translationZ="10dp"
|
||||||
|
android:visibility="visible"
|
||||||
|
android:background="#44000000" />
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/appBarLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<im.vector.app.core.ui.views.CurrentCallsView
|
||||||
|
android:id="@+id/currentCallsView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/roomToolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?actionBarSize"
|
||||||
|
android:transitionName="toolbar">
|
||||||
|
|
||||||
|
<include
|
||||||
|
android:id="@+id/includeThreadToolbar"
|
||||||
|
layout="@layout/view_room_detail_thread_toolbar" />
|
||||||
|
|
||||||
|
<include
|
||||||
|
android:id="@+id/includeRoomToolbar"
|
||||||
|
layout="@layout/view_room_detail_toolbar" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.MaterialToolbar>
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<im.vector.app.features.sync.widget.SyncStateView
|
||||||
|
android:id="@+id/syncStateView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/appBarLayout" />
|
||||||
|
|
||||||
|
<im.vector.app.features.location.live.LiveLocationStatusView
|
||||||
|
android:id="@+id/liveLocationStatusIndicator"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/syncStateView"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<im.vector.app.features.call.conference.RemoveJitsiWidgetView
|
||||||
|
android:id="@+id/removeJitsiWidgetView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?android:colorBackground"
|
||||||
|
android:minHeight="54dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/liveLocationStatusIndicator" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/timelineRecyclerView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:overScrollMode="always"
|
||||||
|
android:visibility="invisible"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/typingMessageView"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/removeJitsiWidgetView"
|
||||||
|
tools:listitem="@layout/item_timeline_event_base" />
|
||||||
|
|
||||||
|
<com.google.android.material.chip.Chip
|
||||||
|
android:id="@+id/jumpToReadMarkerView"
|
||||||
|
style="?vctr_jump_to_unread_style"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:text="@string/room_jump_to_first_unread"
|
||||||
|
android:visibility="invisible"
|
||||||
|
app:chipIcon="@drawable/ic_jump_to_unread"
|
||||||
|
app:chipIconTint="?colorPrimary"
|
||||||
|
app:closeIcon="@drawable/ic_close_24dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/removeJitsiWidgetView" />
|
||||||
|
|
||||||
|
<im.vector.app.core.ui.views.TypingMessageView
|
||||||
|
android:id="@+id/typingMessageView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingStart="20dp"
|
||||||
|
android:paddingEnd="20dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/bottomBarrier"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/timelineRecyclerView" />
|
||||||
|
|
||||||
|
<im.vector.app.core.ui.views.NotificationAreaView
|
||||||
|
android:id="@+id/notificationAreaView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<ViewStub
|
||||||
|
android:id="@+id/failedMessagesWarningStub"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inflatedId="@+id/failedMessagesWarningStub"
|
||||||
|
android:layout="@layout/view_stub_failed_message_warning_layout"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/composerContainer"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
tools:layout_height="300dp" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/composerContainer"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:background="?android:colorBackground"
|
||||||
|
android:translationZ="48dp"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/appBarLayout"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/voiceMessageRecorderContainer"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:translationZ="48dp"
|
||||||
|
android:background="?android:colorBackground"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent" />
|
||||||
|
|
||||||
|
<ViewStub
|
||||||
|
android:id="@+id/inviteViewStub"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:background="?android:colorBackground"
|
||||||
|
android:layout="@layout/view_stub_invite_layout"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/appBarLayout" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Barrier
|
||||||
|
android:id="@+id/bottomBarrier"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:barrierDirection="top"
|
||||||
|
app:constraint_referenced_ids="notificationAreaView,failedMessagesWarningStub" />
|
||||||
|
|
||||||
|
<im.vector.app.core.platform.BadgeFloatingActionButton
|
||||||
|
android:id="@+id/jumpToBottomView"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:contentDescription="@string/a11y_jump_to_bottom"
|
||||||
|
android:src="@drawable/ic_expand_more"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:backgroundTint="#FFFFFF"
|
||||||
|
app:badgeBackgroundColor="?colorPrimary"
|
||||||
|
app:badgeTextColor="?colorOnPrimary"
|
||||||
|
app:badgeTextPadding="2dp"
|
||||||
|
app:badgeTextSize="10sp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/bottomBarrier"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:tint="@android:color/black" />
|
||||||
|
|
||||||
|
<im.vector.app.core.ui.views.CompatKonfetti
|
||||||
|
android:id="@+id/viewKonfetti"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:visibility="invisible" />
|
||||||
|
|
||||||
|
<com.jetradarmobile.snowfall.SnowfallView
|
||||||
|
android:id="@+id/viewSnowFall"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="?vctr_chat_effect_snow_background"
|
||||||
|
android:visibility="invisible" />
|
||||||
|
|
||||||
|
<!-- Room not found layout -->
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/roomNotFound"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:background="?android:colorBackground"
|
||||||
|
android:elevation="10dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:visibility="gone">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/roomNotFoundIcon"
|
||||||
|
android:layout_width="60dp"
|
||||||
|
android:layout_height="60dp"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
android:src="@drawable/ic_alert_triangle"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/roomNotFoundText"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_chainStyle="packed" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/roomNotFoundText"
|
||||||
|
style="@style/Widget.Vector.TextView.Subtitle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/layout_vertical_margin"
|
||||||
|
android:gravity="center"
|
||||||
|
android:paddingHorizontal="16dp"
|
||||||
|
android:text="@string/timeline_error_room_not_found"
|
||||||
|
android:textColor="?vctr_content_primary"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/roomNotFoundIcon" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -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_height="wrap_content"
|
|
||||||
tools:text="@sample/users.json/data/displayName" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/voiceBroadcastViewGroup"
|
|
||||||
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_mic"
|
||||||
android:gravity="center_vertical"
|
tools:metadataValue="@sample/users.json/data/displayName" />
|
||||||
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/voiceBroadcastMetadata"
|
||||||
android:layout_width="16dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="16dp"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginEnd="5dp"
|
app:metadataIcon="@drawable/ic_voice_broadcast"
|
||||||
android:contentDescription="@null"
|
app:metadataValue="@string/attachment_type_voice_broadcast" />
|
||||||
android:src="@drawable/ic_voice_broadcast_16"
|
|
||||||
app:tint="?vctr_content_secondary" />
|
|
||||||
|
|
||||||
<TextView
|
<im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
|
||||||
android:id="@+id/voiceBroadcastText"
|
android:id="@+id/listenersCountMetadata"
|
||||||
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"
|
app:metadataIcon="@drawable/ic_member_small"
|
||||||
android:text="@string/attachment_type_voice_broadcast" />
|
app:metadataValue="@string/no_value_placeholder"
|
||||||
</LinearLayout>
|
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>
|
||||||
|
@ -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>
|
||||||
|
27
vector/src/main/res/layout/view_voice_broadcast_metadata.xml
Normal file
27
vector/src/main/res/layout/view_voice_broadcast_metadata.xml
Normal 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>
|
@ -18,7 +18,9 @@ package im.vector.app.features.attachments
|
|||||||
|
|
||||||
import com.airbnb.mvrx.test.MavericksTestRule
|
import com.airbnb.mvrx.test.MavericksTestRule
|
||||||
import im.vector.app.test.fakes.FakeVectorFeatures
|
import im.vector.app.test.fakes.FakeVectorFeatures
|
||||||
|
import im.vector.app.test.fakes.FakeVectorPreferences
|
||||||
import im.vector.app.test.test
|
import im.vector.app.test.test
|
||||||
|
import io.mockk.verifyOrder
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@ -29,6 +31,7 @@ internal class AttachmentTypeSelectorViewModelTest {
|
|||||||
val mavericksTestRule = MavericksTestRule()
|
val mavericksTestRule = MavericksTestRule()
|
||||||
|
|
||||||
private val fakeVectorFeatures = FakeVectorFeatures()
|
private val fakeVectorFeatures = FakeVectorFeatures()
|
||||||
|
private val fakeVectorPreferences = FakeVectorPreferences()
|
||||||
private val initialState = AttachmentTypeSelectorViewState()
|
private val initialState = AttachmentTypeSelectorViewState()
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
@ -36,6 +39,7 @@ internal class AttachmentTypeSelectorViewModelTest {
|
|||||||
// Disable all features by default
|
// Disable all features by default
|
||||||
fakeVectorFeatures.givenLocationSharing(isEnabled = false)
|
fakeVectorFeatures.givenLocationSharing(isEnabled = false)
|
||||||
fakeVectorFeatures.givenVoiceBroadcast(isEnabled = false)
|
fakeVectorFeatures.givenVoiceBroadcast(isEnabled = false)
|
||||||
|
fakeVectorPreferences.givenTextFormatting(isEnabled = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -82,10 +86,57 @@ internal class AttachmentTypeSelectorViewModelTest {
|
|||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given text formatting is enabled, then text formatting option is checked`() {
|
||||||
|
fakeVectorPreferences.givenTextFormatting(isEnabled = true)
|
||||||
|
|
||||||
|
createViewModel()
|
||||||
|
.test()
|
||||||
|
.assertStates(
|
||||||
|
listOf(
|
||||||
|
initialState.copy(
|
||||||
|
isTextFormattingEnabled = true
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when text formatting is changed, then it updates the UI`() {
|
||||||
|
createViewModel()
|
||||||
|
.apply {
|
||||||
|
handle(AttachmentTypeSelectorAction.ToggleTextFormatting(isEnabled = true))
|
||||||
|
}
|
||||||
|
.test()
|
||||||
|
.assertStates(
|
||||||
|
listOf(
|
||||||
|
initialState.copy(
|
||||||
|
isTextFormattingEnabled = true
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when text formatting is changed, then it persists the change`() {
|
||||||
|
createViewModel()
|
||||||
|
.apply {
|
||||||
|
handle(AttachmentTypeSelectorAction.ToggleTextFormatting(isEnabled = true))
|
||||||
|
handle(AttachmentTypeSelectorAction.ToggleTextFormatting(isEnabled = false))
|
||||||
|
}
|
||||||
|
verifyOrder {
|
||||||
|
fakeVectorPreferences.instance.setTextFormattingEnabled(true)
|
||||||
|
fakeVectorPreferences.instance.setTextFormattingEnabled(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun createViewModel(): AttachmentTypeSelectorViewModel {
|
private fun createViewModel(): AttachmentTypeSelectorViewModel {
|
||||||
return AttachmentTypeSelectorViewModel(
|
return AttachmentTypeSelectorViewModel(
|
||||||
initialState,
|
initialState,
|
||||||
vectorFeatures = fakeVectorFeatures,
|
vectorFeatures = fakeVectorFeatures,
|
||||||
|
vectorPreferences = fakeVectorPreferences.instance,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,9 +17,10 @@
|
|||||||
package im.vector.app.features.voicebroadcast.usecase
|
package im.vector.app.features.voicebroadcast.usecase
|
||||||
|
|
||||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
|
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
|
||||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
|
|
||||||
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
|
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
|
||||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
||||||
|
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
|
||||||
|
import im.vector.app.features.voicebroadcast.recording.usecase.PauseVoiceBroadcastUseCase
|
||||||
import im.vector.app.test.fakes.FakeRoom
|
import im.vector.app.test.fakes.FakeRoom
|
||||||
import im.vector.app.test.fakes.FakeRoomService
|
import im.vector.app.test.fakes.FakeRoomService
|
||||||
import im.vector.app.test.fakes.FakeSession
|
import im.vector.app.test.fakes.FakeSession
|
||||||
|
@ -17,9 +17,10 @@
|
|||||||
package im.vector.app.features.voicebroadcast.usecase
|
package im.vector.app.features.voicebroadcast.usecase
|
||||||
|
|
||||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
|
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
|
||||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
|
|
||||||
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
|
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
|
||||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
||||||
|
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
|
||||||
|
import im.vector.app.features.voicebroadcast.recording.usecase.ResumeVoiceBroadcastUseCase
|
||||||
import im.vector.app.test.fakes.FakeRoom
|
import im.vector.app.test.fakes.FakeRoom
|
||||||
import im.vector.app.test.fakes.FakeRoomService
|
import im.vector.app.test.fakes.FakeRoomService
|
||||||
import im.vector.app.test.fakes.FakeSession
|
import im.vector.app.test.fakes.FakeSession
|
||||||
|
@ -17,23 +17,27 @@
|
|||||||
package im.vector.app.features.voicebroadcast.usecase
|
package im.vector.app.features.voicebroadcast.usecase
|
||||||
|
|
||||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
|
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
|
||||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
|
|
||||||
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
|
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
|
||||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
||||||
|
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
||||||
|
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
|
||||||
|
import im.vector.app.features.voicebroadcast.recording.usecase.StartVoiceBroadcastUseCase
|
||||||
import im.vector.app.test.fakes.FakeContext
|
import im.vector.app.test.fakes.FakeContext
|
||||||
import im.vector.app.test.fakes.FakeRoom
|
import im.vector.app.test.fakes.FakeRoom
|
||||||
import im.vector.app.test.fakes.FakeRoomService
|
import im.vector.app.test.fakes.FakeRoomService
|
||||||
import im.vector.app.test.fakes.FakeSession
|
import im.vector.app.test.fakes.FakeSession
|
||||||
import io.mockk.clearAllMocks
|
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
import io.mockk.coVerify
|
import io.mockk.coVerify
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.justRun
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.slot
|
import io.mockk.slot
|
||||||
|
import io.mockk.spyk
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.amshove.kluent.shouldBe
|
import org.amshove.kluent.shouldBe
|
||||||
import org.amshove.kluent.shouldBeNull
|
import org.amshove.kluent.shouldBeNull
|
||||||
|
import org.junit.Before
|
||||||
import org.junit.Test
|
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.Content
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
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.toContent
|
||||||
@ -48,13 +52,24 @@ class StartVoiceBroadcastUseCaseTest {
|
|||||||
private val fakeRoom = FakeRoom()
|
private val fakeRoom = FakeRoom()
|
||||||
private val fakeSession = FakeSession(fakeRoomService = FakeRoomService(fakeRoom))
|
private val fakeSession = FakeSession(fakeRoomService = FakeRoomService(fakeRoom))
|
||||||
private val fakeVoiceBroadcastRecorder = mockk<VoiceBroadcastRecorder>(relaxed = true)
|
private val fakeVoiceBroadcastRecorder = mockk<VoiceBroadcastRecorder>(relaxed = true)
|
||||||
private val startVoiceBroadcastUseCase = StartVoiceBroadcastUseCase(
|
private val fakeGetOngoingVoiceBroadcastsUseCase = mockk<GetOngoingVoiceBroadcastsUseCase>()
|
||||||
fakeSession,
|
private val startVoiceBroadcastUseCase = spyk(
|
||||||
fakeVoiceBroadcastRecorder,
|
StartVoiceBroadcastUseCase(
|
||||||
FakeContext().instance,
|
session = fakeSession,
|
||||||
mockk()
|
voiceBroadcastRecorder = fakeVoiceBroadcastRecorder,
|
||||||
|
context = FakeContext().instance,
|
||||||
|
buildMeta = mockk(),
|
||||||
|
getOngoingVoiceBroadcastsUseCase = fakeGetOngoingVoiceBroadcastsUseCase,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
every { fakeRoom.roomId } returns A_ROOM_ID
|
||||||
|
justRun { startVoiceBroadcastUseCase.assertHasEnoughPowerLevels(fakeRoom) }
|
||||||
|
every { fakeVoiceBroadcastRecorder.state } returns VoiceBroadcastRecorder.State.Idle
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@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 {
|
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()
|
val cases = VoiceBroadcastState.values()
|
||||||
@ -79,8 +94,8 @@ class StartVoiceBroadcastUseCaseTest {
|
|||||||
|
|
||||||
private suspend fun testVoiceBroadcastStarted(voiceBroadcasts: List<VoiceBroadcast>) {
|
private suspend fun testVoiceBroadcastStarted(voiceBroadcasts: List<VoiceBroadcast>) {
|
||||||
// Given
|
// Given
|
||||||
clearAllMocks()
|
setup()
|
||||||
givenAVoiceBroadcasts(voiceBroadcasts)
|
givenVoiceBroadcasts(voiceBroadcasts)
|
||||||
val voiceBroadcastInfoContentInterceptor = slot<Content>()
|
val voiceBroadcastInfoContentInterceptor = slot<Content>()
|
||||||
coEvery { fakeRoom.stateService().sendStateEvent(any(), any(), capture(voiceBroadcastInfoContentInterceptor)) } coAnswers { AN_EVENT_ID }
|
coEvery { fakeRoom.stateService().sendStateEvent(any(), any(), capture(voiceBroadcastInfoContentInterceptor)) } coAnswers { AN_EVENT_ID }
|
||||||
|
|
||||||
@ -102,8 +117,8 @@ class StartVoiceBroadcastUseCaseTest {
|
|||||||
|
|
||||||
private suspend fun testVoiceBroadcastNotStarted(voiceBroadcasts: List<VoiceBroadcast>) {
|
private suspend fun testVoiceBroadcastNotStarted(voiceBroadcasts: List<VoiceBroadcast>) {
|
||||||
// Given
|
// Given
|
||||||
clearAllMocks()
|
setup()
|
||||||
givenAVoiceBroadcasts(voiceBroadcasts)
|
givenVoiceBroadcasts(voiceBroadcasts)
|
||||||
|
|
||||||
// When
|
// When
|
||||||
startVoiceBroadcastUseCase.execute(A_ROOM_ID)
|
startVoiceBroadcastUseCase.execute(A_ROOM_ID)
|
||||||
@ -112,7 +127,7 @@ class StartVoiceBroadcastUseCaseTest {
|
|||||||
coVerify(exactly = 0) { fakeRoom.stateService().sendStateEvent(any(), any(), any()) }
|
coVerify(exactly = 0) { fakeRoom.stateService().sendStateEvent(any(), any(), any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun givenAVoiceBroadcasts(voiceBroadcasts: List<VoiceBroadcast>) {
|
private fun givenVoiceBroadcasts(voiceBroadcasts: List<VoiceBroadcast>) {
|
||||||
val events = voiceBroadcasts.map {
|
val events = voiceBroadcasts.map {
|
||||||
Event(
|
Event(
|
||||||
type = VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO,
|
type = VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO,
|
||||||
@ -122,7 +137,9 @@ class StartVoiceBroadcastUseCaseTest {
|
|||||||
).toContent()
|
).toContent()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
fakeRoom.stateService().givenGetStateEvents(QueryStringValue.IsNotEmpty, events)
|
.mapNotNull { it.asVoiceBroadcastEvent() }
|
||||||
|
.filter { it.content?.voiceBroadcastState != VoiceBroadcastState.STOPPED }
|
||||||
|
every { fakeGetOngoingVoiceBroadcastsUseCase.execute(any()) } returns events
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class VoiceBroadcast(val userId: String, val state: VoiceBroadcastState)
|
private data class VoiceBroadcast(val userId: String, val state: VoiceBroadcastState)
|
||||||
|
@ -17,9 +17,10 @@
|
|||||||
package im.vector.app.features.voicebroadcast.usecase
|
package im.vector.app.features.voicebroadcast.usecase
|
||||||
|
|
||||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
|
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
|
||||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
|
|
||||||
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
|
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
|
||||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
||||||
|
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
|
||||||
|
import im.vector.app.features.voicebroadcast.recording.usecase.StopVoiceBroadcastUseCase
|
||||||
import im.vector.app.test.fakes.FakeRoom
|
import im.vector.app.test.fakes.FakeRoom
|
||||||
import im.vector.app.test.fakes.FakeRoomService
|
import im.vector.app.test.fakes.FakeRoomService
|
||||||
import im.vector.app.test.fakes.FakeSession
|
import im.vector.app.test.fakes.FakeSession
|
||||||
|
@ -40,4 +40,7 @@ class FakeVectorPreferences {
|
|||||||
fun givenIsClientInfoRecordingEnabled(isEnabled: Boolean) {
|
fun givenIsClientInfoRecordingEnabled(isEnabled: Boolean) {
|
||||||
every { instance.isClientInfoRecordingEnabled() } returns isEnabled
|
every { instance.isClientInfoRecordingEnabled() } returns isEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun givenTextFormatting(isEnabled: Boolean) =
|
||||||
|
every { instance.isTextFormattingEnabled() } returns isEnabled
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user