Voice message UI initial implementation.

This commit is contained in:
Onuray Sahin 2021-06-17 16:17:38 +03:00
parent 0d6994dd43
commit 69350ef514
20 changed files with 428 additions and 3 deletions

View file

@ -48,6 +48,9 @@ allprojects {
// Chat effects
includeGroupByRegex 'com\\.github\\.jetradarmobile'
includeGroupByRegex 'nl\\.dionsegijn'
// Voice RecordView
includeGroupByRegex 'com\\.github\\.3llomi'
}
}
maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }

View file

@ -144,6 +144,8 @@ android {
buildConfigField "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy", "outboundSessionKeySharingStrategy", "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy.WhenTyping"
buildConfigField "Long", "VOICE_MESSAGE_DURATION_LIMIT_MS", "120000L"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// Keep abiFilter for the universalApk
@ -391,6 +393,7 @@ dependencies {
implementation "androidx.autofill:autofill:$autofill_version"
implementation 'jp.wasabeef:glide-transformations:4.3.0'
implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12'
implementation 'com.github.3llomi:RecordView:3.0.1'
// Custom Tab
implementation 'androidx.browser:browser:1.3.0'

View file

@ -109,4 +109,8 @@ sealed class RoomDetailAction : VectorViewModelAction {
// Failed messages
object RemoveAllFailedMessages : RoomDetailAction()
// Voice Message
object StartRecordingVoiceMessage : RoomDetailAction()
data class EndRecordingVoiceMessage(val recordTime: Long) : RoomDetailAction()
}

View file

@ -100,6 +100,7 @@ import im.vector.app.core.ui.views.NotificationAreaView
import im.vector.app.core.utils.Debouncer
import im.vector.app.core.utils.DimensionConverter
import im.vector.app.core.utils.KeyboardStateUtils
import im.vector.app.core.utils.PERMISSIONS_FOR_AUDIO_IP_CALL
import im.vector.app.core.utils.PERMISSIONS_FOR_WRITING_FILES
import im.vector.app.core.utils.checkPermissions
import im.vector.app.core.utils.colorizeMatchingText
@ -1192,6 +1193,18 @@ class RoomDetailFragment @Inject constructor(
override fun onTextEmptyStateChanged(isEmpty: Boolean) {
// No op
}
override fun onVoiceRecordingStarted() {
roomDetailViewModel.handle(RoomDetailAction.StartRecordingVoiceMessage)
}
override fun onVoiceRecordingEnded(recordTime: Long) {
roomDetailViewModel.handle(RoomDetailAction.EndRecordingVoiceMessage(recordTime))
}
override fun checkVoiceRecordingPermission(): Boolean {
return checkPermissions(PERMISSIONS_FOR_AUDIO_IP_CALL, requireActivity(), 0)
}
}
}

View file

@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail
import android.net.Uri
import androidx.annotation.IdRes
import androidx.core.net.toUri
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
@ -38,6 +39,7 @@ import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.mvrx.runCatchingToAsync
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.attachments.toContentAttachmentData
import im.vector.app.features.call.conference.JitsiService
import im.vector.app.features.call.dialpad.DialPadLookup
import im.vector.app.features.call.lookup.CallProtocolsChecker
@ -47,6 +49,7 @@ import im.vector.app.features.command.ParsedCommand
import im.vector.app.features.createdirect.DirectRoomHelper
import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy
import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider
import im.vector.app.features.home.room.detail.composer.VoiceMessageRecordingHelper
import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator
import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder
@ -56,6 +59,7 @@ import im.vector.app.features.home.room.typing.TypingHelper
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
import im.vector.app.features.session.coroutineScope
import im.vector.app.features.settings.VectorPreferences
import im.vector.lib.multipicker.utils.toMultiPickerAudioType
import io.reactivex.Observable
import io.reactivex.rxkotlin.subscribeBy
import io.reactivex.schedulers.Schedulers
@ -119,6 +123,7 @@ class RoomDetailViewModel @AssistedInject constructor(
private val chatEffectManager: ChatEffectManager,
private val directRoomHelper: DirectRoomHelper,
private val jitsiService: JitsiService,
private val voiceMessageRecordingHelper: VoiceMessageRecordingHelper,
timelineSettingsFactory: TimelineSettingsFactory
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener {
@ -324,6 +329,8 @@ class RoomDetailViewModel @AssistedInject constructor(
is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action)
RoomDetailAction.RemoveAllFailedMessages -> handleRemoveAllFailedMessages()
RoomDetailAction.ResendAll -> handleResendAll()
RoomDetailAction.StartRecordingVoiceMessage -> handleStartRecordingVoiceMessage()
is RoomDetailAction.EndRecordingVoiceMessage -> handleEndRecordingVoiceMessage(action.recordTime)
}.exhaustive
}
@ -611,6 +618,22 @@ class RoomDetailViewModel @AssistedInject constructor(
}
}
private fun handleStartRecordingVoiceMessage() {
voiceMessageRecordingHelper.startRecording()
}
private fun handleEndRecordingVoiceMessage(recordTime: Long) {
if (recordTime == 0L) {
voiceMessageRecordingHelper.deleteRecording()
return
}
voiceMessageRecordingHelper.stopRecording(recordTime)?.let { audioType ->
room.sendMedia(audioType.toContentAttachmentData(), false, emptySet())
room
//voiceMessageRecordingHelper.deleteRecording()
}
}
private fun isIntegrationEnabled() = session.integrationManagerService().isIntegrationEnabled()
fun isMenuItemVisible(@IdRes itemId: Int): Boolean = com.airbnb.mvrx.withState(this) { state ->

View file

@ -24,6 +24,7 @@ import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
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
@ -32,6 +33,7 @@ import androidx.transition.TransitionManager
import androidx.transition.TransitionSet
import im.vector.app.R
import im.vector.app.databinding.ComposerLayoutBinding
import org.matrix.android.sdk.api.extensions.orFalse
/**
* Encapsulate the timeline composer UX.
@ -46,6 +48,9 @@ class TextComposerView @JvmOverloads constructor(
fun onCloseRelatedMessage()
fun onSendMessage(text: CharSequence)
fun onAddAttachment()
fun onVoiceRecordingStarted()
fun onVoiceRecordingEnded(recordTime: Long)
fun checkVoiceRecordingPermission(): Boolean
}
val views: ComposerLayoutBinding
@ -71,7 +76,9 @@ class TextComposerView @JvmOverloads constructor(
}
override fun onTextEmptyStateChanged(isEmpty: Boolean) {
views.sendButton.isVisible = currentConstraintSetId == R.layout.composer_layout_constraint_set_expanded || !isEmpty
val shouldShowSendButton = currentConstraintSetId == R.layout.composer_layout_constraint_set_expanded || !isEmpty
views.sendButton.isInvisible = !shouldShowSendButton
views.voiceMessageRecorderView.isVisible = !shouldShowSendButton
}
}
views.composerRelatedMessageCloseButton.setOnClickListener {
@ -87,6 +94,28 @@ class TextComposerView @JvmOverloads constructor(
views.attachmentButton.setOnClickListener {
callback?.onAddAttachment()
}
views.voiceMessageRecorderView.callback = object : VoiceMessageRecorderView.Callback {
override fun onVoiceRecordingStarted() {
views.attachmentButton.isVisible = false
views.composerEditText.isVisible = false
views.composerEmojiButton.isVisible = false
views.composerEditTextOuterBorder.isVisible = false
callback?.onVoiceRecordingStarted()
}
override fun onVoiceRecordingEnded(recordTime: Long) {
views.attachmentButton.isVisible = true
views.composerEditText.isVisible = true
views.composerEmojiButton.isVisible = true
views.composerEditTextOuterBorder.isVisible = true
callback?.onVoiceRecordingEnded(recordTime)
}
override fun checkVoiceRecordingPermission(): Boolean {
return callback?.checkVoiceRecordingPermission().orFalse()
}
}
}
fun collapse(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) {
@ -96,7 +125,10 @@ class TextComposerView @JvmOverloads constructor(
}
currentConstraintSetId = R.layout.composer_layout_constraint_set_compact
applyNewConstraintSet(animate, transitionComplete)
views.sendButton.isVisible = !views.composerEditText.text.isNullOrEmpty()
val shouldShowSendButton = !views.composerEditText.text.isNullOrEmpty()
views.sendButton.isInvisible = !shouldShowSendButton
views.voiceMessageRecorderView.isVisible = !shouldShowSendButton
}
fun expand(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) {
@ -106,7 +138,8 @@ class TextComposerView @JvmOverloads constructor(
}
currentConstraintSetId = R.layout.composer_layout_constraint_set_expanded
applyNewConstraintSet(animate, transitionComplete)
views.sendButton.isVisible = true
views.sendButton.isInvisible = false
views.voiceMessageRecorderView.isVisible = false
}
private fun applyNewConstraintSet(animate: Boolean, transitionComplete: (() -> Unit)?) {

View file

@ -0,0 +1,92 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home.room.detail.composer
import android.content.Context
import android.util.AttributeSet
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import com.devlomi.record_view.OnRecordListener
import im.vector.app.BuildConfig
import im.vector.app.R
import im.vector.app.databinding.ViewVoiceMessageRecorderBinding
import org.matrix.android.sdk.api.extensions.orFalse
/**
* Encapsulates the voice message recording view and animations.
*/
class VoiceMessageRecorderView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
interface Callback {
fun onVoiceRecordingStarted()
fun onVoiceRecordingEnded(recordTime: Long)
fun checkVoiceRecordingPermission(): Boolean
}
private val views: ViewVoiceMessageRecorderBinding
var callback: Callback? = null
init {
inflate(context, R.layout.view_voice_message_recorder, this)
views = ViewVoiceMessageRecorderBinding.bind(this)
views.voiceMessageButton.setRecordView(views.voiceMessageRecordView)
views.voiceMessageRecordView.timeLimit = BuildConfig.VOICE_MESSAGE_DURATION_LIMIT_MS
views.voiceMessageRecordView.setRecordPermissionHandler { callback?.checkVoiceRecordingPermission().orFalse() }
views.voiceMessageRecordView.setOnRecordListener(object : OnRecordListener {
override fun onStart() {
onVoiceRecordingStarted()
}
override fun onCancel() {
onVoiceRecordingEnded(0)
}
override fun onFinish(recordTime: Long, limitReached: Boolean) {
onVoiceRecordingEnded(recordTime)
}
override fun onLessThanSecond() {
onVoiceRecordingEnded(0)
}
})
}
private fun onVoiceRecordingStarted() {
views.voiceMessageLockBackground.isVisible = true
views.voiceMessageLockArrow.isVisible = true
views.voiceMessageLockImage.isVisible = true
views.voiceMessageButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_voice_mic_recording))
callback?.onVoiceRecordingStarted()
}
private fun onVoiceRecordingEnded(recordTime: Long) {
views.voiceMessageLockBackground.isVisible = false
views.voiceMessageLockArrow.isVisible = false
views.voiceMessageLockImage.isVisible = false
views.voiceMessageButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_voice_mic))
callback?.onVoiceRecordingEnded(recordTime)
}
}

View file

@ -0,0 +1,87 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home.room.detail.composer
import android.content.Context
import android.media.MediaRecorder
import androidx.core.content.FileProvider
import androidx.core.net.toUri
import im.vector.app.BuildConfig
import im.vector.lib.multipicker.entity.MultiPickerAudioType
import im.vector.lib.multipicker.utils.toMultiPickerAudioType
import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.lang.RuntimeException
import java.util.UUID
import javax.inject.Inject
/**
* Helper class to record audio for voice messages.
*/
class VoiceMessageRecordingHelper @Inject constructor(
private val context: Context
) {
private lateinit var mediaRecorder: MediaRecorder
private val outputDirectory = File(context.cacheDir, "downloads")
private var outputFile: File? = null
init {
if (!outputDirectory.exists()) {
outputDirectory.mkdirs()
}
}
private fun refreshMediaRecorder() {
mediaRecorder = MediaRecorder()
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT)
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.OGG)
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.OPUS)
mediaRecorder.setAudioEncodingBitRate(24000)
mediaRecorder.setAudioSamplingRate(48000)
}
fun startRecording() {
outputFile = File(outputDirectory, UUID.randomUUID().toString() + ".ogg")
FileOutputStream(outputFile).use { fos ->
refreshMediaRecorder()
mediaRecorder.setOutputFile(fos.fd)
mediaRecorder.prepare()
mediaRecorder.start()
}
}
fun stopRecording(recordTime: Long): MultiPickerAudioType? {
try {
mediaRecorder.stop()
mediaRecorder.reset()
mediaRecorder.release()
outputFile?.let {
val outputFileUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileProvider", it)
return outputFileUri?.toMultiPickerAudioType(context)
} ?: return null
} catch (e: RuntimeException) { // Usually thrown when the record is less than 1 second.
Timber.e(e, "Voice message is not valid. Record time: %s", recordTime)
return null
}
}
fun deleteRecording() {
outputFile?.delete()
}
}

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<size
android:width="56dp"
android:height="160dp" />
<solid android:color="?vctr_voice_message_lock_background" />
<corners
android:bottomLeftRadius="28dp"
android:bottomRightRadius="28dp"
android:topLeftRadius="28dp"
android:topRightRadius="28dp" />
</shape>

View 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">
<path
android:pathData="M6,15L12,9L18,15"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#8E99A4"
android:strokeLineCap="round"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#0DBD8B" android:fillType="evenOdd" android:pathData="M11.3333,2C8.3878,2 6,4.3878 6,7.3333V10C4.8954,10 4,10.8954 4,12V20C4,21.1046 4.8954,22 6,22H18C19.1046,22 20,21.1046 20,20V12C20,10.8954 19.1046,10 18,10V7.3333C18,4.3878 15.6122,2 12.6667,2H11.3333ZM15.3333,10V7.3333C15.3333,5.8606 14.1394,4.6667 12.6667,4.6667H11.3333C9.8606,4.6667 8.6667,5.8606 8.6667,7.3333V10H15.3333Z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="16dp"
android:viewportHeight="16" android:viewportWidth="16"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#8E99A4" android:fillType="evenOdd" android:pathData="M7.4444,0C4.9899,0 3,1.9334 3,4.3183V6.1369C2.4115,6.3926 2,6.979 2,7.6615V14.3385C2,15.2561 2.7439,16 3.6615,16H12.3385C13.2561,16 14,15.2561 14,14.3385V7.6615C14,6.7439 13.2561,6 12.3385,6H5.2222V4.3183C5.2222,3.1259 6.2171,2.1592 7.4444,2.1592H8.5556C9.7829,2.1592 10.7778,3.1259 10.7778,4.3183H13C13,1.9334 11.0102,0 8.5556,0H7.4444Z"/>
</vector>

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M10.8,8.2C10.8,5.3281 13.1282,3 16,3C18.8719,3 21.2,5.3281 21.2,8.2V15.9767C21.2,18.8486 18.8719,21.1767 16,21.1767C13.1282,21.1767 10.8,18.8486 10.8,15.9767V8.2Z"
android:fillColor="#8D99A5"/>
<path
android:pathData="M6.8998,14.3167C7.8203,14.3167 8.5665,15.0629 8.5665,15.9834C8.5665,20.0737 11.8818,23.3944 15.98,23.4051C15.9867,23.405 15.9934,23.4049 16.0001,23.4049C16.0068,23.4049 16.0134,23.405 16.0201,23.4051C20.1181,23.3941 23.4332,20.0735 23.4332,15.9834C23.4332,15.0629 24.1793,14.3167 25.0998,14.3167C26.0203,14.3167 26.7665,15.0629 26.7665,15.9834C26.7665,21.3586 22.8201,25.8101 17.6667,26.6103V27.6683C17.6667,28.5888 16.9206,29.335 16.0001,29.335C15.0796,29.335 14.3334,28.5888 14.3334,27.6683V26.6104C9.1798,25.8104 5.2332,21.3587 5.2332,15.9834C5.2332,15.0629 5.9794,14.3167 6.8998,14.3167Z"
android:fillColor="#8D99A5"/>
</vector>

View file

@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="52dp"
android:height="52dp"
android:viewportWidth="52"
android:viewportHeight="52">
<path
android:pathData="M26.173,26.1729m-22.7631,0a22.7631,22.7631 0,1 1,45.5262 0a22.7631,22.7631 0,1 1,-45.5262 0"
android:fillColor="#0DBD8B"/>
<path
android:pathData="M26,26m-26,0a26,26 0,1 1,52 0a26,26 0,1 1,-52 0"
android:strokeAlpha="0.2"
android:fillColor="#0DBD8B"
android:fillAlpha="0.2"/>
<path
android:pathData="M21.2414,18.7749C21.2414,16.051 23.4496,13.8429 26.1734,13.8429C28.8973,13.8429 31.1054,16.051 31.1054,18.7749V26.1509C31.1054,28.8747 28.8973,31.0829 26.1734,31.0829C23.4496,31.0829 21.2414,28.8747 21.2414,26.1509V18.7749ZM17.542,24.2475C18.5968,24.2475 19.4518,25.1025 19.4518,26.1572C19.4518,29.8561 22.4509,32.8596 26.1586,32.8675C26.1637,32.8674 26.1689,32.8674 26.174,32.8674C26.179,32.8674 26.184,32.8674 26.189,32.8675C29.896,32.8589 32.8944,29.8556 32.8944,26.1572C32.8944,25.1025 33.7494,24.2475 34.8041,24.2475C35.8588,24.2475 36.7138,25.1025 36.7138,26.1572C36.7138,31.3227 32.9916,35.6165 28.0837,36.5143V37.24C28.0837,38.2947 27.2287,39.1497 26.174,39.1497C25.1193,39.1497 24.2643,38.2947 24.2643,37.24V36.5147C19.3555,35.6176 15.6323,31.3233 15.6323,26.1572C15.6323,25.1025 16.4873,24.2475 17.542,24.2475Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,8 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000"
android:pathData="M15,18L9,12L15,6"
android:strokeColor="#8E99A4" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="2"/>
</vector>

View file

@ -131,4 +131,10 @@
android:src="@drawable/ic_send"
tools:ignore="MissingConstraints" />
<im.vector.app.features.home.room.detail.composer.VoiceMessageRecorderView
android:id="@+id/voiceMessageRecorderView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:ignore="MissingConstraints" />
</merge>

View file

@ -178,4 +178,12 @@
tools:ignore="MissingPrefix"
tools:visibility="visible" />
<im.vector.app.features.home.room.detail.composer.VoiceMessageRecorderView
android:id="@+id/voiceMessageRecorderView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,69 @@
<?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:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginEnd="2dp">
<View
android:id="@+id/voiceMessageLockBackground"
android:layout_width="56dp"
android:layout_height="160dp"
android:background="@drawable/bg_voice_message_lock"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<com.devlomi.record_view.RecordView
android:id="@+id/voiceMessageRecordView"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:counter_time_color="@color/palette_gray_200"
app:layout_constraintBottom_toBottomOf="@+id/voiceMessageButton"
app:layout_constraintEnd_toStartOf="@+id/voiceMessageButton"
app:layout_constraintStart_toStartOf="parent"
app:slide_to_cancel_arrow="@drawable/ic_voice_slide_to_cancel_arrow"
app:slide_to_cancel_arrow_color="@color/palette_gray_300"
app:slide_to_cancel_text="@string/voice_message_slide_to_cancel" />
<com.devlomi.record_view.RecordButton
android:id="@+id/voiceMessageButton"
android:layout_width="56dp"
android:layout_height="56dp"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/a11y_start_voice_message"
android:scaleType="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:mic_icon="@drawable/ic_voice_mic"
tools:ignore="MissingPrefix" />
<ImageView
android:id="@+id/voiceMessageLockArrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_voice_lock_arrow"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/voiceMessageButton"
app:layout_constraintEnd_toEndOf="@id/voiceMessageLockBackground"
app:layout_constraintStart_toStartOf="@id/voiceMessageLockBackground"
android:layout_marginBottom="24dp"
tools:ignore="ContentDescription"
tools:visibility="visible" />
<ImageView
android:id="@+id/voiceMessageLockImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="18dp"
android:contentDescription="@string/a11y_lock_voice_message"
android:src="@drawable/ic_voice_message_unlocked"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="@id/voiceMessageLockBackground"
app:layout_constraintStart_toStartOf="@id/voiceMessageLockBackground"
app:layout_constraintTop_toTopOf="@id/voiceMessageLockBackground"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -132,4 +132,9 @@
<color name="vctr_chat_effect_snow_background_light">@color/black_alpha</color>
<color name="vctr_chat_effect_snow_background_dark">@android:color/transparent</color>
<attr name="vctr_voice_message_lock_background" format="color" />
<color name="vctr_voice_message_lock_background_light">#FFF3F8FD</color>
<color name="vctr_voice_message_lock_background_dark">#22252B</color>
<color name="vctr_voice_message_lock_background_black">#22252B</color>
</resources>

View file

@ -3399,4 +3399,8 @@
<string name="this_space_has_no_rooms_admin">Some rooms may be hidden because theyre private and you need an invite.</string>
<string name="unnamed_room">Unnamed Room</string>
<string name="a11y_start_voice_message">Start Voice Message</string>
<string name="voice_message_slide_to_cancel">Slide to cancel</string>
<string name="a11y_lock_voice_message">Voice Message Lock</string>
</resources>