mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-22 09:25:49 +03:00
Merge pull request #7455 from vector-im/resilience-rc
Merge branch resilience-rc into develop
This commit is contained in:
commit
98e0397afd
75 changed files with 2074 additions and 618 deletions
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 : [
|
||||
'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 : [
|
||||
'moshi' : "com.squareup.moshi:moshi:$moshi",
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
<resources>
|
||||
|
||||
<string name="ellipsis" translatable="false">…</string>
|
||||
<string name="no_value_placeholder" translatable="false">–</string>
|
||||
|
||||
<!-- Temporary string -->
|
||||
<string name="not_implemented" translatable="false">Not implemented yet in ${app_name}</string>
|
||||
|
|
|
@ -3094,6 +3094,10 @@
|
|||
<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_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_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_camera">Camera</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>
|
||||
<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_strikethrough">Apply strikethrough 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>
|
||||
|
|
|
@ -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
|
||||
android:name=".features.home.room.detail.RoomDetailActivity"
|
||||
android:parentActivityName=".features.home.HomeActivity">
|
||||
android:parentActivityName=".features.home.HomeActivity"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".features.home.HomeActivity" />
|
||||
|
|
|
@ -18,24 +18,33 @@ package im.vector.app.core.di
|
|||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
|
||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorderQ
|
||||
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
|
||||
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
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object VoiceModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesVoiceBroadcastRecorder(context: Context): VoiceBroadcastRecorder? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
VoiceBroadcastRecorderQ(context)
|
||||
} else {
|
||||
null
|
||||
@Module
|
||||
abstract class VoiceModule {
|
||||
|
||||
companion object {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesVoiceBroadcastRecorder(context: Context): VoiceBroadcastRecorder? {
|
||||
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.features.call.dialpad.DialPadLookup
|
||||
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.MatrixError
|
||||
import org.matrix.android.sdk.api.failure.MatrixIdFailure
|
||||
|
@ -135,6 +137,7 @@ class DefaultErrorFormatter @Inject constructor(
|
|||
is MatrixIdFailure.InvalidMatrixId ->
|
||||
stringProvider.getString(R.string.login_signin_matrix_id_error_invalid_matrix_id)
|
||||
is VoiceFailure -> voiceMessageError(throwable)
|
||||
is VoiceBroadcastFailure -> voiceBroadcastMessageError(throwable)
|
||||
is ActivityNotFoundException ->
|
||||
stringProvider.getString(R.string.error_no_external_application_found)
|
||||
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 {
|
||||
val delay = error.retryAfterMillis
|
||||
|
||||
|
|
|
@ -29,7 +29,13 @@ import androidx.appcompat.widget.SearchView
|
|||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
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.core.animations.SimpleTransitionListener
|
||||
import im.vector.app.features.themes.ThemeUtils
|
||||
|
||||
/**
|
||||
|
@ -90,3 +96,18 @@ fun View.setAttributeBackground(@AttrRes attributeId: Int) {
|
|||
val attribute = ThemeUtils.getAttribute(context, attributeId)!!
|
||||
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.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.viewModels
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.parentFragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
|
||||
import im.vector.app.databinding.BottomSheetAttachmentTypeSelectorBinding
|
||||
import im.vector.app.features.home.room.detail.TimelineViewModel
|
||||
|
@ -34,7 +34,7 @@ import im.vector.app.features.home.room.detail.TimelineViewModel
|
|||
@AndroidEntryPoint
|
||||
class AttachmentTypeSelectorBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetAttachmentTypeSelectorBinding>() {
|
||||
|
||||
private val viewModel: AttachmentTypeSelectorViewModel by fragmentViewModel()
|
||||
private val viewModel: AttachmentTypeSelectorViewModel by parentFragmentViewModel()
|
||||
private val timelineViewModel: TimelineViewModel by parentFragmentViewModel()
|
||||
private val sharedActionViewModel: AttachmentTypeSelectorSharedActionViewModel by viewModels(
|
||||
ownerProducer = { requireParentFragment() }
|
||||
|
@ -51,6 +51,14 @@ class AttachmentTypeSelectorBottomSheet : VectorBaseBottomSheetDialogFragment<Bo
|
|||
views.location.isVisible = viewState.isLocationVisible
|
||||
views.voiceBroadcast.isVisible = viewState.isVoiceBroadcastVisible
|
||||
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?) {
|
||||
|
@ -63,6 +71,7 @@ class AttachmentTypeSelectorBottomSheet : VectorBaseBottomSheetDialogFragment<Bo
|
|||
views.location.debouncedClicks { onAttachmentSelected(AttachmentType.LOCATION) }
|
||||
views.camera.debouncedClicks { onAttachmentSelected(AttachmentType.CAMERA) }
|
||||
views.contact.debouncedClicks { onAttachmentSelected(AttachmentType.CONTACT) }
|
||||
views.textFormatting.setOnCheckedChangeListener { _, isChecked -> onTextFormattingToggled(isChecked) }
|
||||
}
|
||||
|
||||
private fun onAttachmentSelected(attachmentType: AttachmentType) {
|
||||
|
@ -71,6 +80,9 @@ class AttachmentTypeSelectorBottomSheet : VectorBaseBottomSheetDialogFragment<Bo
|
|||
dismiss()
|
||||
}
|
||||
|
||||
private fun onTextFormattingToggled(isEnabled: Boolean) =
|
||||
viewModel.handle(AttachmentTypeSelectorAction.ToggleTextFormatting(isEnabled))
|
||||
|
||||
companion object {
|
||||
fun show(fragmentManager: FragmentManager) {
|
||||
val bottomSheet = AttachmentTypeSelectorBottomSheet()
|
||||
|
|
|
@ -23,15 +23,17 @@ import dagger.assisted.AssistedFactory
|
|||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||
import im.vector.app.core.platform.EmptyAction
|
||||
import im.vector.app.core.platform.EmptyViewEvents
|
||||
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.settings.VectorPreferences
|
||||
|
||||
class AttachmentTypeSelectorViewModel @AssistedInject constructor(
|
||||
@Assisted initialState: AttachmentTypeSelectorViewState,
|
||||
private val vectorFeatures: VectorFeatures,
|
||||
) : VectorViewModel<AttachmentTypeSelectorViewState, EmptyAction, EmptyViewEvents>(initialState) {
|
||||
private val vectorPreferences: VectorPreferences,
|
||||
) : VectorViewModel<AttachmentTypeSelectorViewState, AttachmentTypeSelectorAction, EmptyViewEvents>(initialState) {
|
||||
@AssistedFactory
|
||||
interface Factory : MavericksAssistedViewModelFactory<AttachmentTypeSelectorViewModel, AttachmentTypeSelectorViewState> {
|
||||
override fun create(initialState: AttachmentTypeSelectorViewState): AttachmentTypeSelectorViewModel
|
||||
|
@ -39,8 +41,8 @@ class AttachmentTypeSelectorViewModel @AssistedInject constructor(
|
|||
|
||||
companion object : MavericksViewModelFactory<AttachmentTypeSelectorViewModel, AttachmentTypeSelectorViewState> by hiltMavericksViewModelFactory()
|
||||
|
||||
override fun handle(action: EmptyAction) {
|
||||
// do nothing
|
||||
override fun handle(action: AttachmentTypeSelectorAction) = when (action) {
|
||||
is AttachmentTypeSelectorAction.ToggleTextFormatting -> setTextFormattingEnabled(action.isEnabled)
|
||||
}
|
||||
|
||||
init {
|
||||
|
@ -48,6 +50,16 @@ class AttachmentTypeSelectorViewModel @AssistedInject constructor(
|
|||
copy(
|
||||
isLocationVisible = vectorFeatures.isLocationSharingEnabled(),
|
||||
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(
|
||||
val isLocationVisible: Boolean = false,
|
||||
val isVoiceBroadcastVisible: Boolean = false,
|
||||
val isTextFormattingEnabled: Boolean = false,
|
||||
) : 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.session.coroutineScope
|
||||
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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
|
@ -92,6 +93,7 @@ class HomeActivityViewModel @AssistedInject constructor(
|
|||
private val analyticsConfig: AnalyticsConfig,
|
||||
private val releaseNotesPreferencesStore: ReleaseNotesPreferencesStore,
|
||||
private val vectorFeatures: VectorFeatures,
|
||||
private val stopOngoingVoiceBroadcastUseCase: StopOngoingVoiceBroadcastUseCase,
|
||||
) : VectorViewModel<HomeActivityViewState, HomeActivityViewActions, HomeActivityViewEvents>(initialState) {
|
||||
|
||||
@AssistedFactory
|
||||
|
@ -123,6 +125,7 @@ class HomeActivityViewModel @AssistedInject constructor(
|
|||
observeReleaseNotes()
|
||||
observeLocalNotificationsSilenced()
|
||||
initThreadsMigration()
|
||||
viewModelScope.launch { stopOngoingVoiceBroadcastUseCase.execute() }
|
||||
}
|
||||
|
||||
private fun observeReleaseNotes() = withState { state ->
|
||||
|
|
|
@ -34,6 +34,8 @@ class JumpToBottomViewVisibilityManager(
|
|||
private val layoutManager: LinearLayoutManager
|
||||
) {
|
||||
|
||||
private var canShowButtonOnScroll = true
|
||||
|
||||
init {
|
||||
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
|
@ -43,7 +45,7 @@ class JumpToBottomViewVisibilityManager(
|
|||
|
||||
if (scrollingToPast) {
|
||||
jumpToBottomView.hide()
|
||||
} else {
|
||||
} else if (canShowButtonOnScroll) {
|
||||
maybeShowJumpToBottomViewVisibility()
|
||||
}
|
||||
}
|
||||
|
@ -66,7 +68,13 @@ class JumpToBottomViewVisibilityManager(
|
|||
}
|
||||
}
|
||||
|
||||
fun hideAndPreventVisibilityChangesWithScrolling() {
|
||||
jumpToBottomView.hide()
|
||||
canShowButtonOnScroll = false
|
||||
}
|
||||
|
||||
private fun maybeShowJumpToBottomViewVisibility() {
|
||||
canShowButtonOnScroll = true
|
||||
if (layoutManager.findFirstVisibleItemPosition() > 1) {
|
||||
jumpToBottomView.show()
|
||||
} else {
|
||||
|
|
|
@ -32,7 +32,10 @@ import android.view.ViewGroup
|
|||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.activity.addCallback
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.view.menu.MenuBuilder
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
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.GalleryOrCameraDialogHelperFactory
|
||||
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.commitTransaction
|
||||
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.permissions.RoomWidgetPermissionBottomSheet
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -337,6 +343,7 @@ class TimelineFragment :
|
|||
setupJumpToBottomView()
|
||||
setupRemoveJitsiWidgetView()
|
||||
setupLiveLocationIndicator()
|
||||
setupBackPressHandling()
|
||||
|
||||
views.includeRoomToolbar.roomToolbarContentView.debouncedClicks {
|
||||
navigator.openRoomProfile(requireActivity(), timelineArgs.roomId)
|
||||
|
@ -414,6 +421,31 @@ class TimelineFragment :
|
|||
if (savedInstanceState == null) {
|
||||
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() {
|
||||
|
@ -1016,7 +1048,13 @@ class TimelineFragment :
|
|||
override fun onLayoutCompleted(state: RecyclerView.State) {
|
||||
super.onLayoutCompleted(state)
|
||||
updateJumpToReadMarkerViewVisibility()
|
||||
jumpToBottomViewVisibilityManager.maybeShowJumpToBottomViewVisibilityWithDelay()
|
||||
withState(messageComposerViewModel) { composerState ->
|
||||
if (!composerState.isFullScreen) {
|
||||
jumpToBottomViewVisibilityManager.maybeShowJumpToBottomViewVisibilityWithDelay()
|
||||
} else {
|
||||
jumpToBottomViewVisibilityManager.hideAndPreventVisibilityChangesWithScrolling()
|
||||
}
|
||||
}
|
||||
}
|
||||
}.apply {
|
||||
// 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) {
|
||||
@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())
|
||||
.setTitle(R.string.dialog_title_error)
|
||||
.setTitle(titleResId)
|
||||
.setMessage(errorFormatter.toHumanReadable(result.throwable))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.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.
|
||||
*/
|
||||
|
|
|
@ -624,7 +624,12 @@ class TimelineViewModel @AssistedInject constructor(
|
|||
if (room == null) return
|
||||
viewModelScope.launch {
|
||||
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.Resume -> voiceBroadcastHelper.resumeVoiceBroadcast(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 InsertUserDisplayName(val userId: String) : MessageComposerAction()
|
||||
|
||||
data class SetFullScreen(val isFullScreen: Boolean) : MessageComposerAction()
|
||||
|
||||
// Voice Message
|
||||
data class InitializeVoiceRecorder(val attachmentData: ContentAttachmentData) : 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.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.parentFragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
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.AttachmentTypeSelectorSharedActionViewModel
|
||||
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.ContactAttachment
|
||||
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.voice.VoiceFailure
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
@ -167,7 +170,8 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||
private val timelineViewModel: TimelineViewModel by parentFragmentViewModel()
|
||||
private val messageComposerViewModel: MessageComposerViewModel by parentFragmentViewModel()
|
||||
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() {
|
||||
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 ->
|
||||
if (!canSend.boolean()) {
|
||||
return@onEach
|
||||
|
@ -226,7 +237,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||
}
|
||||
}
|
||||
|
||||
attachmentViewModel.stream()
|
||||
attachmentActionsViewModel.stream()
|
||||
.filterIsInstance<AttachmentTypeSelectorSharedAction.SelectAttachmentTypeAction>()
|
||||
.onEach { onTypeSelected(it.attachmentType) }
|
||||
.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
|
||||
// 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 -> {
|
||||
timelineViewModel.handle(VoiceBroadcastAction.Listening.Pause)
|
||||
messageComposerViewModel.handle(MessageComposerAction.OnEntersBackground(composer.text.toString()))
|
||||
|
@ -264,11 +275,14 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||
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
|
||||
|
||||
composer.setInvisible(!messageComposerState.isComposerVisible)
|
||||
composer.sendButton.isInvisible = !messageComposerState.isSendButtonVisible
|
||||
(composer as? RichTextComposerLayout)?.isTextFormattingEnabled = attachmentState.isTextFormattingEnabled
|
||||
}
|
||||
|
||||
private fun setupComposer() {
|
||||
|
@ -309,7 +323,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||
// Show keyboard when the user started a thread
|
||||
composerEditText.showKeyboard(andRequestFocus = true)
|
||||
}
|
||||
composer.callback = object : PlainTextComposerLayout.Callback {
|
||||
composer.callback = object : Callback {
|
||||
override fun onAddAttachment() {
|
||||
if (vectorPreferences.isRichTextEditorEnabled()) {
|
||||
AttachmentTypeSelectorBottomSheet.show(childFragmentManager)
|
||||
|
@ -336,8 +350,12 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||
composer.emojiButton?.isVisible = isEmojiKeyboardVisible
|
||||
}
|
||||
|
||||
override fun onSendMessage(text: CharSequence) {
|
||||
override fun onSendMessage(text: CharSequence) = withState(messageComposerViewModel) { state ->
|
||||
sendTextMessage(text, composer.formattedText)
|
||||
|
||||
if (state.isFullScreen) {
|
||||
messageComposerViewModel.handle(MessageComposerAction.SetFullScreen(false))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCloseRelatedMessage() {
|
||||
|
@ -351,6 +369,10 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||
override fun onTextChanged(text: CharSequence) {
|
||||
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.isVisible = true
|
||||
composer.sendButton.animate().alpha(1f).setDuration(150).start()
|
||||
} else {
|
||||
} else if (!event.isVisible) {
|
||||
composer.sendButton.isInvisible = true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,13 +30,14 @@ interface MessageComposerView {
|
|||
val emojiButton: ImageButton?
|
||||
val sendButton: ImageButton
|
||||
val attachmentButton: ImageButton
|
||||
val fullScreenButton: ImageButton?
|
||||
val composerRelatedMessageTitle: TextView
|
||||
val composerRelatedMessageContent: TextView
|
||||
val composerRelatedMessageImage: ImageView
|
||||
val composerRelatedMessageActionIcon: ImageView
|
||||
val composerRelatedMessageAvatar: ImageView
|
||||
|
||||
var callback: PlainTextComposerLayout.Callback?
|
||||
var callback: Callback?
|
||||
|
||||
var isVisible: Boolean
|
||||
|
||||
|
@ -44,6 +45,15 @@ interface MessageComposerView {
|
|||
fun expand(animate: Boolean = true, transitionComplete: (() -> Unit)? = null)
|
||||
fun setTextIfDifferent(text: CharSequence?): Boolean
|
||||
fun replaceFormattedContent(text: CharSequence)
|
||||
fun toggleFullScreen(newValue: 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
|
||||
|
||||
import android.text.SpannableString
|
||||
import androidx.lifecycle.asFlow
|
||||
import com.airbnb.mvrx.MavericksViewModelFactory
|
||||
import dagger.assisted.Assisted
|
||||
|
@ -122,6 +123,7 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||
is MessageComposerAction.AudioSeekBarMovedTo -> handleAudioSeekBarMovedTo(action)
|
||||
is MessageComposerAction.SlashCommandConfirmed -> handleSlashCommandConfirmed(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) {
|
||||
setState {
|
||||
// Makes sure currentComposerText is upToDate when accessing further setState
|
||||
currentComposerText = action.text
|
||||
this
|
||||
val needsSendButtonVisibilityUpdate = currentComposerText.isEmpty() != action.text.isEmpty()
|
||||
currentComposerText = SpannableString(action.text)
|
||||
if (needsSendButtonVisibilityUpdate) {
|
||||
updateIsSendButtonVisibility(true)
|
||||
}
|
||||
updateIsSendButtonVisibility(true)
|
||||
}
|
||||
|
||||
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() {
|
||||
combine(
|
||||
PowerLevelsFlowFactory(room).createFlow(),
|
||||
|
|
|
@ -70,6 +70,7 @@ data class MessageComposerViewState(
|
|||
val voiceRecordingUiState: VoiceMessageRecorderView.RecordingUiState = VoiceMessageRecorderView.RecordingUiState.Idle,
|
||||
val voiceBroadcastState: VoiceBroadcastState? = null,
|
||||
val text: CharSequence? = null,
|
||||
val isFullScreen: Boolean = false,
|
||||
) : MavericksState {
|
||||
|
||||
val isVoiceRecording = when (voiceRecordingUiState) {
|
||||
|
@ -79,9 +80,8 @@ data class MessageComposerViewState(
|
|||
is VoiceMessageRecorderView.RecordingUiState.Recording -> true
|
||||
}
|
||||
|
||||
val isVoiceBroadcasting = when (voiceBroadcastState) {
|
||||
val isRecordingVoiceBroadcast = when (voiceBroadcastState) {
|
||||
VoiceBroadcastState.STARTED,
|
||||
VoiceBroadcastState.PAUSED,
|
||||
VoiceBroadcastState.RESUMED -> true
|
||||
else -> false
|
||||
}
|
||||
|
|
|
@ -49,13 +49,6 @@ class PlainTextComposerLayout @JvmOverloads constructor(
|
|||
defStyleAttr: Int = 0
|
||||
) : ConstraintLayout(context, attrs, defStyleAttr), MessageComposerView {
|
||||
|
||||
interface Callback : ComposerEditText.Callback {
|
||||
fun onCloseRelatedMessage()
|
||||
fun onSendMessage(text: CharSequence)
|
||||
fun onAddAttachment()
|
||||
fun onExpandOrCompactChange()
|
||||
}
|
||||
|
||||
private val views: ComposerLayoutBinding
|
||||
|
||||
override var callback: Callback? = null
|
||||
|
@ -83,6 +76,7 @@ class PlainTextComposerLayout @JvmOverloads constructor(
|
|||
}
|
||||
override val attachmentButton: ImageButton
|
||||
get() = views.attachmentButton
|
||||
override val fullScreenButton: ImageButton? = null
|
||||
override val composerRelatedMessageActionIcon: ImageView
|
||||
get() = views.composerRelatedMessageActionIcon
|
||||
override val composerRelatedMessageAvatar: ImageView
|
||||
|
@ -155,6 +149,10 @@ class PlainTextComposerLayout @JvmOverloads constructor(
|
|||
return views.composerEditText.setTextIfDifferent(text)
|
||||
}
|
||||
|
||||
override fun toggleFullScreen(newValue: Boolean) {
|
||||
// Plain text composer has no full screen
|
||||
}
|
||||
|
||||
private fun applyNewConstraintSet(animate: Boolean, transitionComplete: (() -> Unit)?) {
|
||||
// val wasSendButtonInvisible = views.sendButton.isInvisible
|
||||
if (animate) {
|
||||
|
|
|
@ -21,7 +21,6 @@ import android.text.Editable
|
|||
import android.text.TextWatcher
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
|
@ -33,18 +32,13 @@ import androidx.constraintlayout.widget.ConstraintSet
|
|||
import androidx.core.text.toSpannable
|
||||
import androidx.core.view.isInvisible
|
||||
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.core.animations.SimpleTransitionListener
|
||||
import im.vector.app.core.extensions.animateLayoutChange
|
||||
import im.vector.app.core.extensions.setTextIfDifferent
|
||||
import im.vector.app.databinding.ComposerRichTextLayoutBinding
|
||||
import im.vector.app.databinding.ViewRichTextMenuButtonBinding
|
||||
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.MenuState
|
||||
|
||||
|
@ -56,24 +50,40 @@ class RichTextComposerLayout @JvmOverloads constructor(
|
|||
|
||||
private val views: ComposerRichTextLayoutBinding
|
||||
|
||||
override var callback: PlainTextComposerLayout.Callback? = null
|
||||
override var callback: Callback? = null
|
||||
|
||||
private var currentConstraintSetId: Int = -1
|
||||
|
||||
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?
|
||||
get() = views.composerEditText.text
|
||||
get() = editText.text
|
||||
override val formattedText: String?
|
||||
get() = views.composerEditText.getHtmlOutput()
|
||||
get() = (editText as? EditorEditText)?.getHtmlOutput()
|
||||
override val editText: EditText
|
||||
get() = views.composerEditText
|
||||
get() = if (isTextFormattingEnabled) {
|
||||
views.richTextComposerEditText
|
||||
} else {
|
||||
views.plainTextComposerEditText
|
||||
}
|
||||
override val emojiButton: ImageButton?
|
||||
get() = null
|
||||
override val sendButton: ImageButton
|
||||
get() = views.sendButton
|
||||
override val attachmentButton: ImageButton
|
||||
get() = views.attachmentButton
|
||||
override val fullScreenButton: ImageButton?
|
||||
get() = views.composerFullScreenButton
|
||||
override val composerRelatedMessageActionIcon: ImageView
|
||||
get() = views.composerRelatedMessageActionIcon
|
||||
override val composerRelatedMessageAvatar: ImageView
|
||||
|
@ -94,21 +104,12 @@ class RichTextComposerLayout @JvmOverloads constructor(
|
|||
|
||||
collapse(false)
|
||||
|
||||
views.composerEditText.addTextChangedListener(object : 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) {
|
||||
callback?.onTextChanged(s)
|
||||
|
||||
val isExpanded = s.lines().count() > 1
|
||||
if (previousTextWasExpanded != isExpanded) {
|
||||
updateTextFieldBorder(isExpanded)
|
||||
}
|
||||
previousTextWasExpanded = isExpanded
|
||||
}
|
||||
})
|
||||
views.richTextComposerEditText.addTextChangedListener(
|
||||
TextChangeListener({ callback?.onTextChanged(it) }, { updateTextFieldBorder() })
|
||||
)
|
||||
views.plainTextComposerEditText.addTextChangedListener(
|
||||
TextChangeListener({ callback?.onTextChanged(it) }, { updateTextFieldBorder() })
|
||||
)
|
||||
|
||||
views.composerRelatedMessageCloseButton.setOnClickListener {
|
||||
collapse()
|
||||
|
@ -124,24 +125,32 @@ class RichTextComposerLayout @JvmOverloads constructor(
|
|||
callback?.onAddAttachment()
|
||||
}
|
||||
|
||||
views.composerFullScreenButton.setOnClickListener {
|
||||
callback?.onFullScreenModeChanged()
|
||||
}
|
||||
|
||||
setupRichTextMenu()
|
||||
}
|
||||
|
||||
private fun setupRichTextMenu() {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
updateMenuStateFor(ComposerAction.Bold, state)
|
||||
updateMenuStateFor(ComposerAction.Italic, state)
|
||||
|
@ -149,8 +158,26 @@ class RichTextComposerLayout @JvmOverloads constructor(
|
|||
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) {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
val button = ViewRichTextMenuButtonBinding.inflate(inflater, views.richTextMenu, true)
|
||||
|
@ -170,8 +197,9 @@ class RichTextComposerLayout @JvmOverloads constructor(
|
|||
button.isSelected = menuState.reversedActions.contains(action)
|
||||
}
|
||||
|
||||
private fun updateTextFieldBorder(isExpanded: Boolean) {
|
||||
val borderResource = if (isExpanded) {
|
||||
private fun updateTextFieldBorder() {
|
||||
val isExpanded = editText.editableText.lines().count() > 1
|
||||
val borderResource = if (isExpanded || isFullScreen) {
|
||||
R.drawable.bg_composer_rich_edit_text_expanded
|
||||
} else {
|
||||
R.drawable.bg_composer_rich_edit_text_single_line
|
||||
|
@ -180,7 +208,7 @@ class RichTextComposerLayout @JvmOverloads constructor(
|
|||
}
|
||||
|
||||
override fun replaceFormattedContent(text: CharSequence) {
|
||||
views.composerEditText.setHtml(text.toString())
|
||||
views.richTextComposerEditText.setHtml(text.toString())
|
||||
}
|
||||
|
||||
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
|
||||
applyNewConstraintSet(animate, transitionComplete)
|
||||
updateEditTextVisibility()
|
||||
}
|
||||
|
||||
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
|
||||
applyNewConstraintSet(animate, transitionComplete)
|
||||
updateEditTextVisibility()
|
||||
}
|
||||
|
||||
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)?) {
|
||||
// val wasSendButtonInvisible = views.sendButton.isInvisible
|
||||
if (animate) {
|
||||
configureAndBeginTransition(transitionComplete)
|
||||
animateLayoutChange(animationDuration, transitionComplete)
|
||||
}
|
||||
ConstraintSet().also {
|
||||
it.clone(context, currentConstraintSetId)
|
||||
it.applyTo(this)
|
||||
}
|
||||
|
||||
// Might be updated by view state just after, but avoid blinks
|
||||
// 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) {
|
||||
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 MessageLocationContent -> buildLocationItem(messageContent, informationData, highlight, attributes)
|
||||
is MessageBeaconInfoContent -> liveLocationShareMessageItemFactory.create(params.event, highlight, attributes)
|
||||
is MessageVoiceBroadcastInfoContent -> voiceBroadcastItemFactory.create(params, messageContent, highlight, callback, attributes)
|
||||
is MessageVoiceBroadcastInfoContent -> voiceBroadcastItemFactory.create(params, messageContent, highlight, attributes)
|
||||
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||
}
|
||||
return messageItem?.apply {
|
||||
|
|
|
@ -15,26 +15,25 @@
|
|||
*/
|
||||
package im.vector.app.features.home.room.detail.timeline.factory
|
||||
|
||||
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.app.core.resources.ColorProvider
|
||||
import im.vector.app.core.resources.DrawableProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.app.features.displayname.getBestName
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.VoiceBroadcastEventsGroup
|
||||
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageVoiceBroadcastItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastListeningItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastListeningItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastRecordingItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastRecordingItem_
|
||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastPlayer
|
||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
|
||||
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
|
||||
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
|
||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
||||
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
||||
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import org.matrix.android.sdk.api.session.getUser
|
||||
import org.matrix.android.sdk.api.session.getUserOrDefault
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -51,81 +50,61 @@ class VoiceBroadcastItemFactory @Inject constructor(
|
|||
params: TimelineItemFactoryParams,
|
||||
messageContent: MessageVoiceBroadcastInfoContent,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?,
|
||||
attributes: AbsMessageItem.Attributes,
|
||||
): VectorEpoxyModel<out VectorEpoxyHolder>? {
|
||||
): AbsMessageVoiceBroadcastItem<*>? {
|
||||
// Only display item of the initial event with updated data
|
||||
if (messageContent.voiceBroadcastState != VoiceBroadcastState.STARTED) return null
|
||||
val eventsGroup = params.eventsGroup ?: return null
|
||||
val voiceBroadcastEventsGroup = VoiceBroadcastEventsGroup(eventsGroup)
|
||||
val mostRecentTimelineEvent = voiceBroadcastEventsGroup.getLastDisplayableEvent()
|
||||
val mostRecentEvent = mostRecentTimelineEvent.root.asVoiceBroadcastEvent()
|
||||
val mostRecentMessageContent = mostRecentEvent?.content ?: return null
|
||||
val isRecording = mostRecentMessageContent.voiceBroadcastState != VoiceBroadcastState.STOPPED && mostRecentEvent.root.stateKey == session.myUserId
|
||||
val recorderName = mostRecentTimelineEvent.root.stateKey?.let { session.getUser(it) }?.displayName ?: mostRecentTimelineEvent.root.stateKey
|
||||
|
||||
val voiceBroadcastEventsGroup = params.eventsGroup?.let { VoiceBroadcastEventsGroup(it) } ?: return null
|
||||
val voiceBroadcastEvent = voiceBroadcastEventsGroup.getLastDisplayableEvent().root.asVoiceBroadcastEvent() ?: return null
|
||||
val voiceBroadcastContent = voiceBroadcastEvent.content ?: return null
|
||||
val voiceBroadcastId = voiceBroadcastEventsGroup.voiceBroadcastId
|
||||
|
||||
val isRecording = voiceBroadcastContent.voiceBroadcastState != VoiceBroadcastState.STOPPED &&
|
||||
voiceBroadcastEvent.root.stateKey == session.myUserId &&
|
||||
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) {
|
||||
createRecordingItem(
|
||||
params.event.roomId,
|
||||
eventsGroup.groupId,
|
||||
highlight,
|
||||
callback,
|
||||
attributes
|
||||
)
|
||||
createRecordingItem(highlight, attributes, voiceBroadcastAttributes)
|
||||
} else {
|
||||
createListeningItem(
|
||||
params.event.roomId,
|
||||
eventsGroup.groupId,
|
||||
mostRecentMessageContent.voiceBroadcastState,
|
||||
recorderName,
|
||||
highlight,
|
||||
callback,
|
||||
attributes
|
||||
)
|
||||
createListeningItem(highlight, attributes, voiceBroadcastAttributes)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createRecordingItem(
|
||||
roomId: String,
|
||||
voiceBroadcastId: String,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?,
|
||||
attributes: AbsMessageItem.Attributes,
|
||||
voiceBroadcastAttributes: AbsMessageVoiceBroadcastItem.Attributes,
|
||||
): MessageVoiceBroadcastRecordingItem {
|
||||
val roomSummary = session.getRoom(roomId)?.roomSummary()
|
||||
return MessageVoiceBroadcastRecordingItem_()
|
||||
.id("voice_broadcast_$voiceBroadcastId")
|
||||
.id("voice_broadcast_${voiceBroadcastAttributes.voiceBroadcastId}")
|
||||
.attributes(attributes)
|
||||
.voiceBroadcastAttributes(voiceBroadcastAttributes)
|
||||
.highlighted(highlight)
|
||||
.roomItem(roomSummary?.toMatrixItem())
|
||||
.colorProvider(colorProvider)
|
||||
.drawableProvider(drawableProvider)
|
||||
.voiceBroadcastRecorder(voiceBroadcastRecorder)
|
||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||
.callback(callback)
|
||||
}
|
||||
|
||||
private fun createListeningItem(
|
||||
roomId: String,
|
||||
voiceBroadcastId: String,
|
||||
voiceBroadcastState: VoiceBroadcastState?,
|
||||
broadcasterName: String?,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?,
|
||||
attributes: AbsMessageItem.Attributes,
|
||||
voiceBroadcastAttributes: AbsMessageVoiceBroadcastItem.Attributes,
|
||||
): MessageVoiceBroadcastListeningItem {
|
||||
val roomSummary = session.getRoom(roomId)?.roomSummary()
|
||||
return MessageVoiceBroadcastListeningItem_()
|
||||
.id("voice_broadcast_$voiceBroadcastId")
|
||||
.id("voice_broadcast_${voiceBroadcastAttributes.voiceBroadcastId}")
|
||||
.attributes(attributes)
|
||||
.voiceBroadcastAttributes(voiceBroadcastAttributes)
|
||||
.highlighted(highlight)
|
||||
.roomItem(roomSummary?.toMatrixItem())
|
||||
.colorProvider(colorProvider)
|
||||
.drawableProvider(drawableProvider)
|
||||
.voiceBroadcastPlayer(voiceBroadcastPlayer)
|
||||
.voiceBroadcastId(voiceBroadcastId)
|
||||
.voiceBroadcastState(voiceBroadcastState)
|
||||
.broadcasterName(broadcasterName)
|
||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||
.callback(callback)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -141,6 +141,9 @@ class CallSignalingEventsGroup(private val group: TimelineEventsGroup) {
|
|||
}
|
||||
|
||||
class VoiceBroadcastEventsGroup(private val group: TimelineEventsGroup) {
|
||||
|
||||
val voiceBroadcastId = group.groupId
|
||||
|
||||
fun getLastDisplayableEvent(): TimelineEvent {
|
||||
return group.events.find { it.root.asVoiceBroadcastEvent()?.content?.voiceBroadcastState == VoiceBroadcastState.STOPPED }
|
||||
?: group.events.filter { it.root.type == VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO }.maxBy { it.root.originServerTs ?: 0L }
|
||||
|
|
|
@ -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.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.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.timeline.TimelineEventController
|
||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastPlayer
|
||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
|
||||
import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
|
||||
|
||||
@EpoxyModelClass
|
||||
abstract class MessageVoiceBroadcastListeningItem : AbsMessageItem<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
|
||||
abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem<MessageVoiceBroadcastListeningItem.Holder>() {
|
||||
|
||||
private lateinit var playerListener: VoiceBroadcastPlayer.Listener
|
||||
|
||||
override fun isCacheable(): Boolean = false
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
bindVoiceBroadcastItem(holder)
|
||||
|
@ -75,51 +38,20 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageItem<MessageVoiceB
|
|||
|
||||
private fun bindVoiceBroadcastItem(holder: Holder) {
|
||||
playerListener = VoiceBroadcastPlayer.Listener { state ->
|
||||
renderState(holder, state)
|
||||
renderPlayingState(holder, state)
|
||||
}
|
||||
voiceBroadcastPlayer?.addListener(playerListener)
|
||||
renderHeader(holder)
|
||||
renderLiveIcon(holder)
|
||||
player.addListener(voiceBroadcastId, playerListener)
|
||||
}
|
||||
|
||||
private fun renderHeader(holder: Holder) {
|
||||
override fun renderMetadata(holder: Holder) {
|
||||
with(holder) {
|
||||
roomItem?.let {
|
||||
attributes.avatarRenderer.render(it, roomAvatarImageView)
|
||||
titleText.text = it.displayName
|
||||
}
|
||||
broadcasterNameText.text = broadcasterName
|
||||
broadcasterNameMetadata.value = recorderName
|
||||
voiceBroadcastMetadata.isVisible = true
|
||||
listenersCountMetadata.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderLiveIcon(holder: Holder) {
|
||||
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) {
|
||||
private fun renderPlayingState(holder: Holder, state: VoiceBroadcastPlayer.State) {
|
||||
with(holder) {
|
||||
bufferingView.isVisible = state == VoiceBroadcastPlayer.State.BUFFERING
|
||||
playPauseButton.isVisible = state != VoiceBroadcastPlayer.State.BUFFERING
|
||||
|
@ -127,15 +59,15 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageItem<MessageVoiceB
|
|||
when (state) {
|
||||
VoiceBroadcastPlayer.State.PLAYING -> {
|
||||
playPauseButton.setImageResource(R.drawable.ic_play_pause_pause)
|
||||
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast)
|
||||
playPauseButton.onClick { attributes.callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.Pause) }
|
||||
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast)
|
||||
playPauseButton.onClick { callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.Pause) }
|
||||
}
|
||||
VoiceBroadcastPlayer.State.IDLE,
|
||||
VoiceBroadcastPlayer.State.PAUSED -> {
|
||||
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 {
|
||||
attributes.callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastId))
|
||||
callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastId))
|
||||
}
|
||||
}
|
||||
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) {
|
||||
super.unbind(holder)
|
||||
voiceBroadcastPlayer?.removeListener(playerListener)
|
||||
player.removeListener(voiceBroadcastId, playerListener)
|
||||
}
|
||||
|
||||
override fun getViewStubId() = STUB_ID
|
||||
|
||||
class Holder : AbsMessageItem.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)
|
||||
class Holder : AbsMessageVoiceBroadcastItem.Holder(STUB_ID) {
|
||||
val playPauseButton by bind<ImageButton>(R.id.playPauseButton)
|
||||
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 {
|
||||
|
|
|
@ -17,45 +17,19 @@
|
|||
package im.vector.app.features.home.room.detail.timeline.item
|
||||
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.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.timeline.TimelineEventController
|
||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
||||
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
|
||||
import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
|
||||
|
||||
@EpoxyModelClass
|
||||
abstract class MessageVoiceBroadcastRecordingItem : AbsMessageItem<MessageVoiceBroadcastRecordingItem.Holder>() {
|
||||
abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem<MessageVoiceBroadcastRecordingItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute
|
||||
var callback: TimelineEventController.Callback? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var voiceBroadcastRecorder: VoiceBroadcastRecorder? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var colorProvider: ColorProvider
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var drawableProvider: DrawableProvider
|
||||
|
||||
@EpoxyAttribute
|
||||
var roomItem: MatrixItem? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var title: String? = null
|
||||
|
||||
private lateinit var recorderListener: VoiceBroadcastRecorder.Listener
|
||||
|
||||
override fun isCacheable(): Boolean = false
|
||||
private var recorderListener: VoiceBroadcastRecorder.Listener? = null
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
|
@ -63,73 +37,80 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageItem<MessageVoiceB
|
|||
}
|
||||
|
||||
private fun bindVoiceBroadcastItem(holder: Holder) {
|
||||
recorderListener = object : VoiceBroadcastRecorder.Listener {
|
||||
override fun onStateUpdated(state: VoiceBroadcastRecorder.State) {
|
||||
renderState(holder, state)
|
||||
}
|
||||
}
|
||||
voiceBroadcastRecorder?.addListener(recorderListener)
|
||||
renderHeader(holder)
|
||||
}
|
||||
|
||||
private fun renderHeader(holder: Holder) {
|
||||
with(holder) {
|
||||
roomItem?.let {
|
||||
attributes.avatarRenderer.render(it, roomAvatarImageView)
|
||||
titleText.text = it.displayName
|
||||
}
|
||||
if (recorder != null && recorder?.state != VoiceBroadcastRecorder.State.Idle) {
|
||||
recorderListener = object : VoiceBroadcastRecorder.Listener {
|
||||
override fun onStateUpdated(state: VoiceBroadcastRecorder.State) {
|
||||
renderRecordingState(holder, state)
|
||||
}
|
||||
}.also { recorder?.addListener(it) }
|
||||
} else {
|
||||
renderVoiceBroadcastState(holder)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderState(holder: Holder, state: VoiceBroadcastRecorder.State) {
|
||||
override fun renderMetadata(holder: Holder) {
|
||||
with(holder) {
|
||||
when (state) {
|
||||
VoiceBroadcastRecorder.State.Recording -> {
|
||||
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
|
||||
}
|
||||
}
|
||||
listenersCountMetadata.isVisible = false
|
||||
remainingTimeMetadata.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) {
|
||||
super.unbind(holder)
|
||||
voiceBroadcastRecorder?.removeListener(recorderListener)
|
||||
recorderListener?.let { recorder?.removeListener(it) }
|
||||
recorderListener = null
|
||||
}
|
||||
|
||||
override fun getViewStubId() = STUB_ID
|
||||
|
||||
class Holder : AbsMessageItem.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)
|
||||
class Holder : AbsMessageVoiceBroadcastItem.Holder(STUB_ID) {
|
||||
val listenersCountMetadata by bind<VoiceBroadcastMetadataView>(R.id.listenersCountMetadata)
|
||||
val remainingTimeMetadata by bind<VoiceBroadcastMetadataView>(R.id.remainingTimeMetadata)
|
||||
val recordButton by bind<ImageButton>(R.id.recordButton)
|
||||
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"
|
||||
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_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_12_24_TIMESTAMPS_KEY = "SETTINGS_12_24_TIMESTAMPS_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.
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
|
||||
import im.vector.app.features.voicebroadcast.usecase.PauseVoiceBroadcastUseCase
|
||||
import im.vector.app.features.voicebroadcast.usecase.ResumeVoiceBroadcastUseCase
|
||||
import im.vector.app.features.voicebroadcast.usecase.StartVoiceBroadcastUseCase
|
||||
import im.vector.app.features.voicebroadcast.usecase.StopVoiceBroadcastUseCase
|
||||
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
|
||||
import im.vector.app.features.voicebroadcast.recording.usecase.PauseVoiceBroadcastUseCase
|
||||
import im.vector.app.features.voicebroadcast.recording.usecase.ResumeVoiceBroadcastUseCase
|
||||
import im.vector.app.features.voicebroadcast.recording.usecase.StartVoiceBroadcastUseCase
|
||||
import im.vector.app.features.voicebroadcast.recording.usecase.StopVoiceBroadcastUseCase
|
||||
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.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.voicebroadcast
|
||||
package im.vector.app.features.voicebroadcast.listening
|
||||
|
||||
import android.media.AudioAttributes
|
||||
import android.media.MediaPlayer
|
||||
|
@ -22,49 +22,43 @@ import androidx.annotation.MainThread
|
|||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
|
||||
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.asVoiceBroadcastEvent
|
||||
import im.vector.app.features.voicebroadcast.sequence
|
||||
import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastUseCase
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
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.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 java.util.concurrent.CopyOnWriteArrayList
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class VoiceBroadcastPlayer @Inject constructor(
|
||||
class VoiceBroadcastPlayerImpl @Inject constructor(
|
||||
private val sessionHolder: ActiveSessionHolder,
|
||||
private val playbackTracker: AudioMessagePlaybackTracker,
|
||||
private val getVoiceBroadcastUseCase: GetVoiceBroadcastUseCase,
|
||||
) {
|
||||
private val getLiveVoiceBroadcastChunksUseCase: GetLiveVoiceBroadcastChunksUseCase
|
||||
) : VoiceBroadcastPlayer {
|
||||
|
||||
private val session
|
||||
get() = sessionHolder.getActiveSession()
|
||||
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
private var voiceBroadcastStateJob: Job? = null
|
||||
private var currentTimeline: Timeline? = null
|
||||
set(value) {
|
||||
field?.removeAllListeners()
|
||||
field?.dispose()
|
||||
field = value
|
||||
}
|
||||
|
||||
private val mediaPlayerListener = MediaPlayerListener()
|
||||
private var timelineListener: TimelineListener? = null
|
||||
|
||||
private var currentMediaPlayer: MediaPlayer? = null
|
||||
private var nextMediaPlayer: MediaPlayer? = null
|
||||
|
@ -74,38 +68,49 @@ class VoiceBroadcastPlayer @Inject constructor(
|
|||
}
|
||||
private var currentSequence: Int? = null
|
||||
|
||||
private var fetchPlaylistJob: Job? = null
|
||||
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
|
||||
set(value) {
|
||||
Timber.w("## VoiceBroadcastPlayer state: $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 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 {
|
||||
hasChanged -> startPlayback(roomId, eventId)
|
||||
state == State.PAUSED -> resumePlayback()
|
||||
hasChanged -> startPlayback(roomId, voiceBroadcastId)
|
||||
playingState == State.PAUSED -> resumePlayback()
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
fun pause() {
|
||||
override fun pause() {
|
||||
currentMediaPlayer?.pause()
|
||||
currentVoiceBroadcastId?.let { playbackTracker.pausePlayback(it) }
|
||||
state = State.PAUSED
|
||||
playingState = State.PAUSED
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
override fun stop() {
|
||||
// Stop playback
|
||||
currentMediaPlayer?.stop()
|
||||
currentVoiceBroadcastId?.let { playbackTracker.stopPlayback(it) }
|
||||
isLive = false
|
||||
|
||||
// Release current player
|
||||
release(currentMediaPlayer)
|
||||
|
@ -119,50 +124,78 @@ class VoiceBroadcastPlayer @Inject constructor(
|
|||
voiceBroadcastStateJob?.cancel()
|
||||
voiceBroadcastStateJob = null
|
||||
|
||||
// In case of live broadcast, stop observing new chunks
|
||||
currentTimeline = null
|
||||
timelineListener = null
|
||||
// Do not fetch the playlist anymore
|
||||
fetchPlaylistJob?.cancel()
|
||||
fetchPlaylistJob = null
|
||||
|
||||
// Update state
|
||||
state = State.IDLE
|
||||
playingState = State.IDLE
|
||||
|
||||
// Clear playlist
|
||||
playlist = emptyList()
|
||||
currentSequence = null
|
||||
|
||||
currentRoomId = null
|
||||
currentVoiceBroadcastId = null
|
||||
}
|
||||
|
||||
fun addListener(listener: Listener) {
|
||||
listeners.add(listener)
|
||||
listener.onStateChanged(state)
|
||||
override fun addListener(voiceBroadcastId: String, listener: Listener) {
|
||||
listeners[voiceBroadcastId]?.add(listener) ?: run {
|
||||
listeners[voiceBroadcastId] = CopyOnWriteArrayList<Listener>().apply { add(listener) }
|
||||
}
|
||||
if (voiceBroadcastId == currentVoiceBroadcastId) listener.onStateChanged(playingState) else listener.onStateChanged(State.IDLE)
|
||||
}
|
||||
|
||||
fun removeListener(listener: Listener) {
|
||||
listeners.remove(listener)
|
||||
override fun removeListener(voiceBroadcastId: String, listener: Listener) {
|
||||
listeners[voiceBroadcastId]?.remove(listener)
|
||||
}
|
||||
|
||||
private fun startPlayback(roomId: String, eventId: String) {
|
||||
val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId")
|
||||
// Stop listening previous voice broadcast if any
|
||||
if (state != State.IDLE) stop()
|
||||
if (playingState != State.IDLE) stop()
|
||||
|
||||
currentRoomId = roomId
|
||||
currentVoiceBroadcastId = eventId
|
||||
|
||||
state = State.BUFFERING
|
||||
playingState = State.BUFFERING
|
||||
|
||||
val voiceBroadcastState = getVoiceBroadcastUseCase.execute(roomId, eventId)?.content?.voiceBroadcastState
|
||||
if (voiceBroadcastState == VoiceBroadcastState.STOPPED) {
|
||||
// Get static playlist
|
||||
updatePlaylist(getExistingChunks(room, eventId))
|
||||
startPlayback(false)
|
||||
} else {
|
||||
playLiveVoiceBroadcast(room, eventId)
|
||||
isLive = voiceBroadcastState != null && voiceBroadcastState != VoiceBroadcastState.STOPPED
|
||||
fetchPlaylistAndStartPlayback(roomId, eventId)
|
||||
}
|
||||
|
||||
private fun fetchPlaylistAndStartPlayback(roomId: String, voiceBroadcastId: String) {
|
||||
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 content = event?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return }
|
||||
val sequence = event.getVoiceBroadcastChunk()?.sequence
|
||||
|
@ -172,7 +205,7 @@ class VoiceBroadcastPlayer @Inject constructor(
|
|||
currentMediaPlayer?.start()
|
||||
currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) }
|
||||
currentSequence = sequence
|
||||
withContext(Dispatchers.Main) { state = State.PLAYING }
|
||||
withContext(Dispatchers.Main) { playingState = State.PLAYING }
|
||||
nextMediaPlayer = prepareNextMediaPlayer()
|
||||
} catch (failure: Throwable) {
|
||||
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() {
|
||||
currentMediaPlayer?.start()
|
||||
currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) }
|
||||
state = State.PLAYING
|
||||
}
|
||||
|
||||
private fun updatePlaylist(playlist: List<MessageAudioEvent>) {
|
||||
this.playlist = playlist.sortedBy { it.getVoiceBroadcastChunk()?.sequence?.toLong() ?: it.root.originServerTs }
|
||||
playingState = State.PLAYING
|
||||
}
|
||||
|
||||
private fun getNextAudioContent(): MessageAudioContent? {
|
||||
val nextSequence = currentSequence?.plus(1)
|
||||
?: timelineListener?.let { playlist.lastOrNull()?.sequence }
|
||||
?: playlist.lastOrNull()?.sequence
|
||||
?: 1
|
||||
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 {
|
||||
|
||||
override fun onInfo(mp: MediaPlayer, what: Int, extra: Int): Boolean {
|
||||
|
@ -309,13 +287,13 @@ class VoiceBroadcastPlayer @Inject constructor(
|
|||
val roomId = currentRoomId ?: return
|
||||
val voiceBroadcastId = currentVoiceBroadcastId ?: 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) {
|
||||
// We'll not receive new chunks anymore so we can stop the live listening
|
||||
stop()
|
||||
} else {
|
||||
state = State.BUFFERING
|
||||
playingState = State.BUFFERING
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -324,15 +302,4 @@ class VoiceBroadcastPlayer @Inject constructor(
|
|||
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.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.voicebroadcast
|
||||
package im.vector.app.features.voicebroadcast.recording
|
||||
|
||||
import androidx.annotation.IntRange
|
||||
import im.vector.app.features.voice.VoiceRecorder
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.voicebroadcast
|
||||
package im.vector.app.features.voicebroadcast.recording
|
||||
|
||||
import android.content.Context
|
||||
import android.media.MediaRecorder
|
|
@ -14,13 +14,13 @@
|
|||
* 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.VoiceBroadcastRecorder
|
||||
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
|
||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
||||
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
||||
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
|
@ -14,13 +14,13 @@
|
|||
* 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.VoiceBroadcastRecorder
|
||||
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
|
||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
||||
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
||||
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
|
@ -14,26 +14,33 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.voicebroadcast.usecase
|
||||
package im.vector.app.features.voicebroadcast.recording.usecase
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.FileProvider
|
||||
import im.vector.app.core.resources.BuildMeta
|
||||
import im.vector.app.features.attachments.toContentAttachmentData
|
||||
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.VoiceBroadcastChunk
|
||||
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 org.jetbrains.annotations.VisibleForTesting
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
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.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.powerlevels.PowerLevelsHelper
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
@ -43,6 +50,7 @@ class StartVoiceBroadcastUseCase @Inject constructor(
|
|||
private val voiceBroadcastRecorder: VoiceBroadcastRecorder?,
|
||||
private val context: Context,
|
||||
private val buildMeta: BuildMeta,
|
||||
private val getOngoingVoiceBroadcastsUseCase: GetOngoingVoiceBroadcastsUseCase,
|
||||
) {
|
||||
|
||||
suspend fun execute(roomId: String): Result<Unit> = runCatching {
|
||||
|
@ -50,18 +58,8 @@ class StartVoiceBroadcastUseCase @Inject constructor(
|
|||
|
||||
Timber.d("## StartVoiceBroadcastUseCase: Start voice broadcast requested")
|
||||
|
||||
val onGoingVoiceBroadcastEvents = 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 }
|
||||
|
||||
if (onGoingVoiceBroadcastEvents.isEmpty()) {
|
||||
startVoiceBroadcast(room)
|
||||
} else {
|
||||
Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: currentVoiceBroadcastEvents=$onGoingVoiceBroadcastEvents")
|
||||
}
|
||||
assertCanStartVoiceBroadcast(room)
|
||||
startVoiceBroadcast(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.
|
||||
*/
|
||||
|
||||
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.VoiceBroadcastRecorder
|
||||
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
|
||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
||||
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
||||
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
|
@ -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"?>
|
||||
<androidx.core.widget.NestedScrollView 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="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?colorSurface">
|
||||
|
@ -82,5 +83,24 @@
|
|||
app:tint="?colorPrimary"
|
||||
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>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
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:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
|
||||
|
||||
|
@ -104,16 +104,41 @@
|
|||
android:background="@drawable/bg_composer_rich_edit_text_single_line" />
|
||||
|
||||
<io.element.android.wysiwyg.EditorEditText
|
||||
android:id="@+id/composerEditText"
|
||||
android:id="@+id/richTextComposerEditText"
|
||||
style="@style/Widget.Vector.EditText.RichTextComposer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:nextFocusLeft="@id/composerEditText"
|
||||
android:nextFocusUp="@id/composerEditText"
|
||||
android:gravity="top"
|
||||
android:nextFocusLeft="@id/richTextComposerEditText"
|
||||
android:nextFocusUp="@id/richTextComposerEditText"
|
||||
tools:hint="@string/room_message_placeholder"
|
||||
tools:text="@tools:sample/lorem/random"
|
||||
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
|
||||
android:id="@+id/sendButton"
|
||||
android:layout_width="0dp"
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/composerLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
@ -114,6 +114,7 @@
|
|||
android:background="?android:attr/selectableItemBackground"
|
||||
android:contentDescription="@string/option_send_files"
|
||||
android:src="@drawable/ic_attachment"
|
||||
app:layout_constraintVertical_bias="1"
|
||||
app:layout_constraintBottom_toBottomOf="@id/sendButton"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/sendButton"
|
||||
|
@ -135,13 +136,13 @@
|
|||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<io.element.android.wysiwyg.EditorEditText
|
||||
android:id="@+id/composerEditText"
|
||||
android:id="@+id/richTextComposerEditText"
|
||||
style="@style/Widget.Vector.EditText.RichTextComposer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/room_message_placeholder"
|
||||
android:nextFocusLeft="@id/composerEditText"
|
||||
android:nextFocusUp="@id/composerEditText"
|
||||
android:nextFocusLeft="@id/richTextComposerEditText"
|
||||
android:nextFocusUp="@id/richTextComposerEditText"
|
||||
android:layout_marginHorizontal="12dp"
|
||||
android:layout_marginVertical="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
|
||||
|
@ -150,6 +151,34 @@
|
|||
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="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
|
||||
android:id="@+id/sendButton"
|
||||
android:layout_width="56dp"
|
||||
|
@ -163,6 +192,7 @@
|
|||
app:layout_constraintTop_toBottomOf="@id/composerEditTextOuterBorder"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintVertical_bias="1"
|
||||
tools:ignore="MissingPrefix"
|
||||
tools:visibility="visible" />
|
||||
|
||||
|
@ -173,6 +203,7 @@
|
|||
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
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/composerLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
@ -149,21 +149,49 @@
|
|||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<io.element.android.wysiwyg.EditorEditText
|
||||
android:id="@+id/composerEditText"
|
||||
android:id="@+id/richTextComposerEditText"
|
||||
style="@style/Widget.Vector.EditText.RichTextComposer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/room_message_placeholder"
|
||||
android:nextFocusLeft="@id/composerEditText"
|
||||
android:nextFocusUp="@id/composerEditText"
|
||||
android:layout_marginHorizontal="12dp"
|
||||
android:nextFocusLeft="@id/richTextComposerEditText"
|
||||
android:nextFocusUp="@id/richTextComposerEditText"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginVertical="10dp"
|
||||
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_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="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
|
||||
android:id="@+id/sendButton"
|
||||
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:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<im.vector.app.features.home.room.detail.composer.PlainTextComposerLayout
|
||||
android:id="@+id/composerLayout"
|
||||
|
@ -19,7 +19,7 @@
|
|||
<im.vector.app.features.home.room.detail.composer.RichTextComposerLayout
|
||||
android:id="@+id/richTextComposerLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:colorBackground"
|
||||
android:minHeight="56dp"
|
||||
android:transitionName="composer"
|
||||
|
|
|
@ -6,6 +6,21 @@
|
|||
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:visibility="gone"
|
||||
android:background="#44000000" />
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appBarLayout"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -98,6 +113,7 @@
|
|||
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"
|
||||
|
@ -128,6 +144,7 @@
|
|||
android:id="@+id/composerContainer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:colorBackground"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
@ -165,6 +182,7 @@
|
|||
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"
|
||||
|
|
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:background="@drawable/rounded_rect_shape_8"
|
||||
android:backgroundTint="?vctr_content_quinary"
|
||||
android:padding="@dimen/layout_vertical_margin"
|
||||
tools:viewBindingIgnore="true">
|
||||
android:padding="@dimen/layout_vertical_margin">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/liveIndicator"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="20dp"
|
||||
style="@style/VoiceBroadcastLiveIndicator"
|
||||
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:textColor="?colorOnError"
|
||||
app:drawableStartCompat="@drawable/ic_voice_broadcast_16"
|
||||
app:drawableTint="?colorOnError"
|
||||
app:drawableStartCompat="@drawable/ic_voice_broadcast"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
@ -54,61 +43,41 @@
|
|||
android:contentDescription="@string/avatar"
|
||||
app:layout_constraintStart_toEndOf="@id/avatarRightBarrier"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@sample/rooms.json/data/name" />
|
||||
tools:text="@sample/rooms.json/data/name" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/broadcasterViewGroup"
|
||||
<androidx.constraintlayout.helper.widget.Flow
|
||||
android:id="@+id/metadataFlow"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:orientation="vertical"
|
||||
app:constraint_referenced_ids="broadcasterNameMetadata,voiceBroadcastMetadata,listenersCountMetadata"
|
||||
app:flow_horizontalAlign="start"
|
||||
app:flow_verticalGap="4dp"
|
||||
app:layout_constraintStart_toEndOf="@id/avatarRightBarrier"
|
||||
app:layout_constraintTop_toBottomOf="@id/titleText">
|
||||
app:layout_constraintTop_toBottomOf="@id/titleText" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/broadcasterIcon"
|
||||
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"
|
||||
<im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
|
||||
android:id="@+id/broadcasterNameMetadata"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintStart_toEndOf="@id/avatarRightBarrier"
|
||||
app:layout_constraintTop_toBottomOf="@id/broadcasterViewGroup">
|
||||
app:metadataIcon="@drawable/ic_voice_broadcast_mic"
|
||||
tools:metadataValue="@sample/users.json/data/displayName" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/voiceBroadcastIcon"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:contentDescription="@null"
|
||||
android:src="@drawable/ic_voice_broadcast_16"
|
||||
app:tint="?vctr_content_secondary" />
|
||||
<im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
|
||||
android:id="@+id/voiceBroadcastMetadata"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:metadataIcon="@drawable/ic_voice_broadcast"
|
||||
app:metadataValue="@string/attachment_type_voice_broadcast" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/voiceBroadcastText"
|
||||
style="@style/Widget.Vector.TextView.Caption"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/attachment_type_voice_broadcast" />
|
||||
</LinearLayout>
|
||||
<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 listeners" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/headerBottomBarrier"
|
||||
|
@ -116,7 +85,16 @@
|
|||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="bottom"
|
||||
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
|
||||
android:id="@+id/playPauseButton"
|
||||
|
@ -126,24 +104,14 @@
|
|||
android:backgroundTint="?vctr_system"
|
||||
android:contentDescription="@string/a11y_play_voice_broadcast"
|
||||
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" />
|
||||
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/bufferingView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/a11y_voice_broadcast_buffering"
|
||||
android:indeterminate="true"
|
||||
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" />
|
||||
|
||||
android:indeterminateTint="?vctr_content_secondary" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
@ -7,25 +7,14 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/rounded_rect_shape_8"
|
||||
android:backgroundTint="?vctr_content_quinary"
|
||||
android:padding="@dimen/layout_vertical_margin"
|
||||
tools:viewBindingIgnore="true">
|
||||
android:padding="@dimen/layout_vertical_margin">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/liveIndicator"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="20dp"
|
||||
style="@style/VoiceBroadcastLiveIndicator"
|
||||
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:textColor="?colorOnError"
|
||||
app:drawableStartCompat="@drawable/ic_voice_broadcast_16"
|
||||
app:drawableTint="?colorOnError"
|
||||
app:drawableStartCompat="@drawable/ic_voice_broadcast"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
@ -54,7 +43,34 @@
|
|||
android:contentDescription="@string/avatar"
|
||||
app:layout_constraintStart_toEndOf="@id/avatarRightBarrier"
|
||||
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
|
||||
android:id="@+id/headerBottomBarrier"
|
||||
|
@ -62,7 +78,16 @@
|
|||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="bottom"
|
||||
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
|
||||
android:id="@+id/recordButton"
|
||||
|
@ -71,11 +96,7 @@
|
|||
android:background="@drawable/bg_rounded_button"
|
||||
android:backgroundTint="?vctr_system"
|
||||
android:contentDescription="@string/a11y_resume_voice_broadcast_record"
|
||||
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" />
|
||||
android:src="@drawable/ic_recording_dot" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/stopRecordButton"
|
||||
|
@ -84,10 +105,6 @@
|
|||
android:background="@drawable/bg_rounded_button"
|
||||
android:backgroundTint="?vctr_system"
|
||||
android:contentDescription="@string/a11y_stop_voice_broadcast_record"
|
||||
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" />
|
||||
android:src="@drawable/ic_stop" />
|
||||
|
||||
</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 im.vector.app.test.fakes.FakeVectorFeatures
|
||||
import im.vector.app.test.fakes.FakeVectorPreferences
|
||||
import im.vector.app.test.test
|
||||
import io.mockk.verifyOrder
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
@ -29,6 +31,7 @@ internal class AttachmentTypeSelectorViewModelTest {
|
|||
val mavericksTestRule = MavericksTestRule()
|
||||
|
||||
private val fakeVectorFeatures = FakeVectorFeatures()
|
||||
private val fakeVectorPreferences = FakeVectorPreferences()
|
||||
private val initialState = AttachmentTypeSelectorViewState()
|
||||
|
||||
@Before
|
||||
|
@ -36,6 +39,7 @@ internal class AttachmentTypeSelectorViewModelTest {
|
|||
// Disable all features by default
|
||||
fakeVectorFeatures.givenLocationSharing(isEnabled = false)
|
||||
fakeVectorFeatures.givenVoiceBroadcast(isEnabled = false)
|
||||
fakeVectorPreferences.givenTextFormatting(isEnabled = false)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -82,10 +86,57 @@ internal class AttachmentTypeSelectorViewModelTest {
|
|||
.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 {
|
||||
return AttachmentTypeSelectorViewModel(
|
||||
initialState,
|
||||
vectorFeatures = fakeVectorFeatures,
|
||||
vectorPreferences = fakeVectorPreferences.instance,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,9 +17,10 @@
|
|||
package im.vector.app.features.voicebroadcast.usecase
|
||||
|
||||
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.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.FakeRoomService
|
||||
import im.vector.app.test.fakes.FakeSession
|
||||
|
|
|
@ -17,9 +17,10 @@
|
|||
package im.vector.app.features.voicebroadcast.usecase
|
||||
|
||||
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.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.FakeRoomService
|
||||
import im.vector.app.test.fakes.FakeSession
|
||||
|
|
|
@ -17,23 +17,27 @@
|
|||
package im.vector.app.features.voicebroadcast.usecase
|
||||
|
||||
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.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.FakeRoom
|
||||
import im.vector.app.test.fakes.FakeRoomService
|
||||
import im.vector.app.test.fakes.FakeSession
|
||||
import io.mockk.clearAllMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.justRun
|
||||
import io.mockk.mockk
|
||||
import io.mockk.slot
|
||||
import io.mockk.spyk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.amshove.kluent.shouldBe
|
||||
import org.amshove.kluent.shouldBeNull
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.session.events.model.Content
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
|
@ -48,13 +52,24 @@ class StartVoiceBroadcastUseCaseTest {
|
|||
private val fakeRoom = FakeRoom()
|
||||
private val fakeSession = FakeSession(fakeRoomService = FakeRoomService(fakeRoom))
|
||||
private val fakeVoiceBroadcastRecorder = mockk<VoiceBroadcastRecorder>(relaxed = true)
|
||||
private val startVoiceBroadcastUseCase = StartVoiceBroadcastUseCase(
|
||||
fakeSession,
|
||||
fakeVoiceBroadcastRecorder,
|
||||
FakeContext().instance,
|
||||
mockk()
|
||||
private val fakeGetOngoingVoiceBroadcastsUseCase = mockk<GetOngoingVoiceBroadcastsUseCase>()
|
||||
private val startVoiceBroadcastUseCase = spyk(
|
||||
StartVoiceBroadcastUseCase(
|
||||
session = fakeSession,
|
||||
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
|
||||
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()
|
||||
|
@ -79,8 +94,8 @@ class StartVoiceBroadcastUseCaseTest {
|
|||
|
||||
private suspend fun testVoiceBroadcastStarted(voiceBroadcasts: List<VoiceBroadcast>) {
|
||||
// Given
|
||||
clearAllMocks()
|
||||
givenAVoiceBroadcasts(voiceBroadcasts)
|
||||
setup()
|
||||
givenVoiceBroadcasts(voiceBroadcasts)
|
||||
val voiceBroadcastInfoContentInterceptor = slot<Content>()
|
||||
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>) {
|
||||
// Given
|
||||
clearAllMocks()
|
||||
givenAVoiceBroadcasts(voiceBroadcasts)
|
||||
setup()
|
||||
givenVoiceBroadcasts(voiceBroadcasts)
|
||||
|
||||
// When
|
||||
startVoiceBroadcastUseCase.execute(A_ROOM_ID)
|
||||
|
@ -112,7 +127,7 @@ class StartVoiceBroadcastUseCaseTest {
|
|||
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 {
|
||||
Event(
|
||||
type = VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO,
|
||||
|
@ -122,7 +137,9 @@ class StartVoiceBroadcastUseCaseTest {
|
|||
).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)
|
||||
|
|
|
@ -17,9 +17,10 @@
|
|||
package im.vector.app.features.voicebroadcast.usecase
|
||||
|
||||
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.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.FakeRoomService
|
||||
import im.vector.app.test.fakes.FakeSession
|
||||
|
|
|
@ -40,4 +40,7 @@ class FakeVectorPreferences {
|
|||
fun givenIsClientInfoRecordingEnabled(isEnabled: Boolean) {
|
||||
every { instance.isClientInfoRecordingEnabled() } returns isEnabled
|
||||
}
|
||||
|
||||
fun givenTextFormatting(isEnabled: Boolean) =
|
||||
every { instance.isTextFormattingEnabled() } returns isEnabled
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue