diff --git a/build.gradle b/build.gradle index a7acc1c124..df412d3fe8 100644 --- a/build.gradle +++ b/build.gradle @@ -50,7 +50,7 @@ allprojects { includeGroupByRegex 'nl\\.dionsegijn' // Voice RecordView - includeGroupByRegex 'com\\.github\\.3llomi' + includeGroupByRegex 'com\\.github\\.Armen101' } } maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } diff --git a/vector/build.gradle b/vector/build.gradle index 991e483e98..2d662ea242 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -331,7 +331,7 @@ dependencies { implementation "androidx.recyclerview:recyclerview:1.2.1" implementation 'androidx.appcompat:appcompat:1.3.0' implementation "androidx.fragment:fragment-ktx:$fragment_version" - implementation 'androidx.constraintlayout:constraintlayout:2.0.4' + implementation 'androidx.constraintlayout:constraintlayout:2.1.0-beta02' implementation "androidx.sharetarget:sharetarget:1.1.0" implementation 'androidx.core:core-ktx:1.5.0' implementation "androidx.media:media:1.3.1" @@ -393,7 +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' + implementation 'com.github.Armen101:AudioRecordView:1.0.5' // Custom Tab implementation 'androidx.browser:browser:1.3.0' diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt index 6672027133..7833864707 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt @@ -20,6 +20,7 @@ import android.content.Context import android.net.Uri import android.text.Editable import android.util.AttributeSet +import android.view.KeyEvent import android.view.ViewGroup import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet @@ -48,9 +49,7 @@ class TextComposerView @JvmOverloads constructor( fun onCloseRelatedMessage() fun onSendMessage(text: CharSequence) fun onAddAttachment() - fun onVoiceRecordingStarted() - fun onVoiceRecordingEnded(recordTime: Long) - fun checkVoiceRecordingPermission(): Boolean + fun onTouchVoiceRecording() } val views: ComposerLayoutBinding @@ -78,7 +77,7 @@ class TextComposerView @JvmOverloads constructor( override fun onTextEmptyStateChanged(isEmpty: Boolean) { val shouldShowSendButton = currentConstraintSetId == R.layout.composer_layout_constraint_set_expanded || !isEmpty views.sendButton.isInvisible = !shouldShowSendButton - views.voiceMessageRecorderView.isVisible = !shouldShowSendButton + callback?.onTextEmptyStateChanged(isEmpty) } } views.composerRelatedMessageCloseButton.setOnClickListener { @@ -94,28 +93,6 @@ 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) { @@ -128,7 +105,6 @@ class TextComposerView @JvmOverloads constructor( val shouldShowSendButton = !views.composerEditText.text.isNullOrEmpty() views.sendButton.isInvisible = !shouldShowSendButton - views.voiceMessageRecorderView.isVisible = !shouldShowSendButton } fun expand(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) { @@ -139,7 +115,6 @@ class TextComposerView @JvmOverloads constructor( currentConstraintSetId = R.layout.composer_layout_constraint_set_expanded applyNewConstraintSet(animate, transitionComplete) views.sendButton.isInvisible = false - views.voiceMessageRecorderView.isVisible = false } private fun applyNewConstraintSet(animate: Boolean, transitionComplete: (() -> Unit)?) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecordingHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecordingHelper.kt deleted file mode 100644 index 63cbbe6e79..0000000000 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecordingHelper.kt +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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() - } -} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt new file mode 100644 index 0000000000..cfca70840a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt @@ -0,0 +1,134 @@ +/* + * 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.timeline.item + +import android.text.format.DateUtils +import android.view.ViewGroup +import android.widget.ImageButton +import android.widget.TextView +import androidx.core.view.isVisible +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.visualizer.amplitude.AudioRecordView +import im.vector.app.R +import im.vector.app.core.epoxy.ClickListener +import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder +import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder +import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker + +@EpoxyModelClass(layout = R.layout.item_timeline_event_base) +abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() { + + @EpoxyAttribute + var mxcUrl: String = "" + + @EpoxyAttribute + var duration: Int = 0 + + @EpoxyAttribute + var waveform: List<Int> = emptyList() + + @EpoxyAttribute + var izLocalFile = false + + @EpoxyAttribute + var izDownloaded = false + + @EpoxyAttribute + lateinit var contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder + + @EpoxyAttribute + lateinit var contentDownloadStateTrackerBinder: ContentDownloadStateTrackerBinder + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + var playbackControlButtonClickListener: ClickListener? = null + + @EpoxyAttribute + lateinit var voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker + + + override fun bind(holder: Holder) { + super.bind(holder) + renderSendState(holder.voiceLayout, null) + if (!attributes.informationData.sendState.hasFailed()) { + contentUploadStateTrackerBinder.bind(attributes.informationData.eventId, izLocalFile, holder.progressLayout) + } else { + holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_cross) + holder.progressLayout.isVisible = false + } + + holder.voicePlaybackTime.text = formatPlaybackTime(duration) + + holder.voicePlaybackWaveform.post { + holder.voicePlaybackWaveform.recreate() + waveform.forEach { amplitude -> + holder.voicePlaybackWaveform.update(amplitude) + } + } + + holder.voicePlaybackControlButton.setOnClickListener { playbackControlButtonClickListener?.invoke(it) } + + voiceMessagePlaybackTracker.track(attributes.informationData.eventId, object : VoiceMessagePlaybackTracker.Listener { + override fun onUpdate(state: VoiceMessagePlaybackTracker.Listener.State) { + when (state) { + is VoiceMessagePlaybackTracker.Listener.State.Idle -> handleIdleState(holder, state) + is VoiceMessagePlaybackTracker.Listener.State.Playing -> handlePlayingState(holder, state) + } + } + }) + } + + private fun handleIdleState(holder: Holder, state: VoiceMessagePlaybackTracker.Listener.State.Idle) { + holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_voice_play) + if (state.playbackTime > 0) { + holder.voicePlaybackTime.text = formatPlaybackTime(state.playbackTime) + } else { + holder.voicePlaybackTime.text = formatPlaybackTime(duration) + } + } + + private fun handlePlayingState(holder: Holder, state: VoiceMessagePlaybackTracker.Listener.State.Playing) { + holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_voice_pause) + if (state.playbackTime > 0) { + holder.voicePlaybackTime.text = formatPlaybackTime(state.playbackTime) + } else { + holder.voicePlaybackTime.text = formatPlaybackTime(duration) + } + } + + private fun formatPlaybackTime(time: Int) = DateUtils.formatElapsedTime((time / 1000).toLong()) + + override fun unbind(holder: Holder) { + super.unbind(holder) + contentUploadStateTrackerBinder.unbind(attributes.informationData.eventId) + contentDownloadStateTrackerBinder.unbind(mxcUrl) + } + + override fun getViewType() = STUB_ID + + class Holder : AbsMessageItem.Holder(STUB_ID) { + val voiceLayout by bind<ViewGroup>(R.id.voiceLayout) + val voicePlaybackControlButton by bind<ImageButton>(R.id.voicePlaybackControlButton) + val voicePlaybackTime by bind<TextView>(R.id.voicePlaybackTime) + val voicePlaybackWaveform by bind<AudioRecordView>(R.id.voicePlaybackWaveform) + val progressLayout by bind<ViewGroup>(R.id.messageFileUploadProgressLayout) + } + + companion object { + private const val STUB_ID = R.id.messageContentVoiceStub + } +} diff --git a/vector/src/main/res/drawable/bg_voice_play_pause_button.xml b/vector/src/main/res/drawable/bg_voice_play_pause_button.xml new file mode 100644 index 0000000000..c0b14c77e9 --- /dev/null +++ b/vector/src/main/res/drawable/bg_voice_play_pause_button.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval"> + <size android:width="32dp" android:height="32dp" /> + <solid android:color="?vctr_voice_message_play_pause_button_background" /> +</shape> \ No newline at end of file diff --git a/vector/src/main/res/drawable/bg_voice_playback.xml b/vector/src/main/res/drawable/bg_voice_playback.xml new file mode 100644 index 0000000000..db31e29bc7 --- /dev/null +++ b/vector/src/main/res/drawable/bg_voice_playback.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="?vctr_voice_message_playback_background" /> + <size + android:width="240dp" + android:height="44dp" /> + <corners + android:bottomLeftRadius="12dp" + android:bottomRightRadius="12dp" + android:topLeftRadius="12dp" + android:topRightRadius="12dp" /> +</shape> \ No newline at end of file diff --git a/vector/src/main/res/drawable/ic_voice_locked.xml b/vector/src/main/res/drawable/ic_voice_message_locked.xml similarity index 100% rename from vector/src/main/res/drawable/ic_voice_locked.xml rename to vector/src/main/res/drawable/ic_voice_message_locked.xml diff --git a/vector/src/main/res/drawable/ic_voice_pause.xml b/vector/src/main/res/drawable/ic_voice_pause.xml new file mode 100644 index 0000000000..af74dec46c --- /dev/null +++ b/vector/src/main/res/drawable/ic_voice_pause.xml @@ -0,0 +1,12 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="10dp" + android:height="12dp" + android:viewportWidth="10" + android:viewportHeight="12"> + <path + android:pathData="M1,0L2,0A1,1 0,0 1,3 1L3,11A1,1 0,0 1,2 12L1,12A1,1 0,0 1,0 11L0,1A1,1 0,0 1,1 0z" + android:fillColor="#737D8C"/> + <path + android:pathData="M8,0L9,0A1,1 0,0 1,10 1L10,11A1,1 0,0 1,9 12L8,12A1,1 0,0 1,7 11L7,1A1,1 0,0 1,8 0z" + android:fillColor="#737D8C"/> +</vector> diff --git a/vector/src/main/res/drawable/ic_voice_play.xml b/vector/src/main/res/drawable/ic_voice_play.xml new file mode 100644 index 0000000000..ad90006799 --- /dev/null +++ b/vector/src/main/res/drawable/ic_voice_play.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="13dp" + android:height="16dp" + android:viewportWidth="13" + android:viewportHeight="16"> + <path + android:pathData="M0,14.2104V1.7896C0,1.0072 0.8578,0.5279 1.5241,0.9379L11.6161,7.1483C12.2506,7.5388 12.2506,8.4612 11.6161,8.8517L1.5241,15.0621C0.8578,15.4721 0,14.9928 0,14.2104Z" + android:fillColor="#737D8C"/> +</vector> diff --git a/vector/src/main/res/layout/composer_layout.xml b/vector/src/main/res/layout/composer_layout.xml index 7c9c23645d..5e40ab275e 100644 --- a/vector/src/main/res/layout/composer_layout.xml +++ b/vector/src/main/res/layout/composer_layout.xml @@ -131,10 +131,16 @@ 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" /> + <!-- + <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" /> + --> </merge> diff --git a/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml b/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml index b51f69302a..e429cf7d16 100644 --- a/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml +++ b/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml @@ -178,12 +178,18 @@ 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" + <!-- + <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" - app:layout_constraintStart_toStartOf="parent" /> + app:layout_constraintEnd_toEndOf="parent" /> + --> </androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_room_detail.xml b/vector/src/main/res/layout/fragment_room_detail.xml index ae7494894c..6a5cae4452 100644 --- a/vector/src/main/res/layout/fragment_room_detail.xml +++ b/vector/src/main/res/layout/fragment_room_detail.xml @@ -248,4 +248,12 @@ android:background="?vctr_chat_effect_snow_background" android:visibility="invisible" /> + <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> diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml index e4d7aa7d9f..f753bc18a9 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -132,6 +132,13 @@ android:layout_marginEnd="56dp" android:layout="@layout/item_timeline_event_option_buttons_stub" /> + <ViewStub + android:id="@+id/messageContentVoiceStub" + style="@style/TimelineContentStubBaseParams" + android:layout="@layout/item_timeline_event_voice_stub" + android:layout_marginEnd="56dp" + tools:visibility="visible" /> + </FrameLayout> <im.vector.app.core.ui.views.SendStateImageView diff --git a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml index 35e1b097d7..797323832b 100644 --- a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml +++ b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml @@ -10,8 +10,8 @@ android:id="@+id/messageSelectedBackground" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_alignParentBottom="true" android:layout_alignParentTop="true" + android:layout_alignParentBottom="true" android:background="@drawable/highlighted_message_background" /> <View diff --git a/vector/src/main/res/layout/item_timeline_event_voice_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_stub.xml new file mode 100644 index 0000000000..6809fa6722 --- /dev/null +++ b/vector/src/main/res/layout/item_timeline_event_voice_stub.xml @@ -0,0 +1,87 @@ +<?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/voiceLayout" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/voicePlaybackLayout" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:background="@drawable/bg_voice_playback" + android:minHeight="48dp" + android:paddingStart="8dp" + android:paddingTop="6dp" + android:paddingEnd="8dp" + android:paddingBottom="6dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + <ImageButton + android:id="@+id/voicePlaybackControlButton" + android:layout_width="32dp" + android:layout_height="32dp" + android:background="@drawable/bg_voice_play_pause_button" + android:contentDescription="@string/a11y_play_voice_message" + android:paddingStart="3dp" + android:paddingEnd="0dp" + android:src="@drawable/ic_voice_play" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/voicePlaybackTime" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="6dp" + android:lineSpacingExtra="4sp" + android:textColor="@color/palette_gray_200" + android:textSize="14sp" + app:layout_constraintBottom_toBottomOf="@id/voicePlaybackControlButton" + app:layout_constraintStart_toEndOf="@id/voicePlaybackControlButton" + app:layout_constraintTop_toTopOf="@id/voicePlaybackControlButton" + tools:text="0:23" /> + + <com.visualizer.amplitude.AudioRecordView + android:id="@+id/voicePlaybackWaveform" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginStart="8dp" + android:layout_marginEnd="8dp" + app:chunkAlignTo="center" + app:chunkColor="@color/palette_gray_300" + app:chunkMinHeight="1dp" + app:chunkRoundedCorners="true" + app:chunkSoftTransition="true" + app:chunkSpace="2dp" + app:chunkWidth="2dp" + app:direction="leftToRight" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/voicePlaybackTime" + app:layout_constraintTop_toTopOf="parent" /> + + + </androidx.constraintlayout.widget.ConstraintLayout> + + + <include + android:id="@+id/messageFileUploadProgressLayout" + layout="@layout/media_upload_download_progress_layout" + android:layout_width="0dp" + android:layout_height="46dp" + android:layout_marginStart="8dp" + android:layout_marginTop="8dp" + android:layout_marginEnd="32dp" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/voicePlaybackLayout" + tools:visibility="visible" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/vector/src/main/res/layout/view_voice_message_recorder.xml b/vector/src/main/res/layout/view_voice_message_recorder.xml index 3b124ae7ef..bf48ee1513 100644 --- a/vector/src/main/res/layout/view_voice_message_recorder.xml +++ b/vector/src/main/res/layout/view_voice_message_recorder.xml @@ -2,68 +2,217 @@ <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/voice_message_recording_layout" + android:minHeight="200dp" android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_marginEnd="2dp"> + android:layout_height="match_parent"> <View android:id="@+id/voiceMessageLockBackground" - android:layout_width="56dp" + android:layout_width="52dp" android:layout_height="160dp" android:background="@drawable/bg_voice_message_lock" android:visibility="gone" + tools:visibility="visible" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="parent" - tools:visibility="visible" /> + app:layout_constraintTop_toBottomOf="@id/voiceMessageMicButton" /> - <com.devlomi.record_view.RecordView - android:id="@+id/voiceMessageRecordView" - android:layout_width="0dp" + <ImageButton + android:id="@+id/voiceMessageMicButton" + android:layout_width="wrap_content" 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" + android:layout_marginBottom="12dp" + android:layout_marginEnd="12dp" + android:src="@drawable/ic_voice_mic" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:mic_icon="@drawable/ic_voice_mic" + app:layout_constraintEnd_toEndOf="parent" /> + + <ImageButton + android:id="@+id/voiceMessageSendButton" + android:layout_width="56dp" + android:layout_height="56dp" + android:contentDescription="@string/send" + android:background="@drawable/bg_send" + android:scaleType="center" + android:visibility="gone" + tools:visibility="visible" + android:src="@drawable/ic_send" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" /> + + <ImageView + android:id="@+id/voiceMessageTimerIndicator" + android:layout_width="10dp" + android:layout_height="10dp" + android:layout_marginStart="20dp" + android:contentDescription="@string/a11y_recording_voice_message" + android:src="@drawable/circle" + android:visibility="gone" + tools:visibility="visible" + app:layout_constraintBottom_toBottomOf="@id/voiceMessageMicButton" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@id/voiceMessageMicButton" + app:tint="@color/palette_vermilion" tools:ignore="MissingPrefix" /> + <TextView + android:id="@+id/voiceMessageTimer" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="4dp" + android:lineSpacingExtra="4dp" + android:textColor="@color/palette_gray_200" + android:textSize="14sp" + android:visibility="gone" + tools:visibility="visible" + app:layout_constraintBottom_toBottomOf="@id/voiceMessageMicButton" + app:layout_constraintStart_toEndOf="@id/voiceMessageTimerIndicator" + app:layout_constraintTop_toTopOf="@id/voiceMessageMicButton" + tools:text="00:03" /> + + <TextView + android:id="@+id/voiceMessageSlideToCancel" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center" + android:text="@string/voice_message_slide_to_cancel" + android:textColor="@color/palette_gray_300" + android:textSize="14sp" + android:visibility="gone" + tools:visibility="visible" + app:drawableStartCompat="@drawable/ic_voice_slide_to_cancel_arrow" + app:layout_constraintBottom_toBottomOf="@id/voiceMessageMicButton" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@id/voiceMessageMicButton" /> + + <ImageView + android:id="@+id/voiceMessageLockImage" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:contentDescription="@string/a11y_lock_voice_message" + android:src="@drawable/ic_voice_message_unlocked" + android:visibility="gone" + tools:visibility="visible" + app:layout_constraintEnd_toEndOf="@id/voiceMessageLockBackground" + app:layout_constraintStart_toStartOf="@id/voiceMessageLockBackground" + app:layout_constraintTop_toTopOf="@id/voiceMessageLockBackground" /> + <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" + tools:visibility="visible" + app:layout_constraintBottom_toTopOf="@id/voiceMessageMicButton" app:layout_constraintEnd_toEndOf="@id/voiceMessageLockBackground" app:layout_constraintStart_toStartOf="@id/voiceMessageLockBackground" - android:layout_marginBottom="24dp" - tools:ignore="ContentDescription" - tools:visibility="visible" /> + tools:ignore="ContentDescription" /> - <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" + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/voiceMessagePlaybackLayout" + android:layout_width="0dp" + android:layout_height="44dp" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" android:visibility="gone" - app:layout_constraintEnd_toEndOf="@id/voiceMessageLockBackground" - app:layout_constraintStart_toStartOf="@id/voiceMessageLockBackground" - app:layout_constraintTop_toTopOf="@id/voiceMessageLockBackground" - tools:visibility="visible" /> + tools:visibility="visible" + android:layout_marginBottom="4dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/voiceMessageMicButton" + app:layout_constraintStart_toStartOf="parent" > + + <ImageButton + android:id="@+id/voiceMessageDeletePlayback" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="?android:attr/selectableItemBackground" + android:contentDescription="@string/a11y_delete_recorded_voice_message" + android:src="@drawable/recv_ic_delete" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:tint="@color/palette_gray_200" + tools:ignore="MissingPrefix" /> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="0dp" + android:layout_height="0dp" + app:layout_constraintStart_toEndOf="@id/voiceMessageDeletePlayback" + app:layout_constraintEnd_toEndOf="parent" + android:backgroundTint="?vctr_voice_message_recording_playback_background" + android:background="@drawable/bg_voice_playback" + android:layout_marginStart="16dp" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent"> + + <ImageView + android:id="@+id/voiceMessagePlaybackTimerIndicator" + android:layout_width="10dp" + android:layout_height="10dp" + android:layout_marginStart="8dp" + android:contentDescription="@string/a11y_recording_voice_message" + android:src="@drawable/circle" + app:layout_goneMarginStart="24dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:tint="@color/palette_vermilion" + tools:ignore="MissingPrefix" /> + + <ImageButton + android:id="@+id/voicePlaybackControlButton" + android:layout_width="32dp" + android:layout_height="32dp" + android:background="@drawable/bg_voice_play_pause_button" + android:contentDescription="@string/a11y_play_voice_message" + android:paddingStart="3dp" + android:layout_marginStart="8dp" + android:paddingEnd="0dp" + android:src="@drawable/ic_voice_play" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/voicePlaybackTime" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="6dp" + android:lineSpacingExtra="4sp" + android:textColor="@color/palette_gray_200" + app:layout_goneMarginStart="24dp" + android:textSize="14sp" + app:layout_constraintBottom_toBottomOf="@id/voicePlaybackControlButton" + app:layout_constraintStart_toEndOf="@id/voicePlaybackControlButton" + app:layout_constraintTop_toTopOf="@id/voicePlaybackControlButton" + tools:text="0:23" /> + + <com.visualizer.amplitude.AudioRecordView + android:id="@+id/voicePlaybackWaveform" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginStart="8dp" + android:layout_marginEnd="8dp" + app:chunkAlignTo="center" + app:chunkColor="@color/palette_gray_300" + app:chunkMinHeight="1dp" + app:chunkRoundedCorners="true" + app:chunkSoftTransition="true" + app:chunkSpace="2dp" + app:chunkWidth="2dp" + app:direction="leftToRight" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/voicePlaybackTime" + app:layout_constraintTop_toTopOf="parent" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + </androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/vector/src/main/res/values/colors.xml b/vector/src/main/res/values/colors.xml index f66476a795..f756b4f243 100644 --- a/vector/src/main/res/values/colors.xml +++ b/vector/src/main/res/values/colors.xml @@ -135,6 +135,17 @@ <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> + + <attr name="vctr_voice_message_playback_background" format="color" /> + <color name="vctr_voice_message_playback_background_light">#FFE3E8F0</color> + <color name="vctr_voice_message_playback_background_dark">#FF394049</color> + + <attr name="vctr_voice_message_play_pause_button_background" format="color" /> + <color name="vctr_voice_message_play_pause_button_background_light">@android:color/white</color> + <color name="vctr_voice_message_play_pause_button_background_dark">#FF8E99A4</color> + + <attr name="vctr_voice_message_recording_playback_background" format="color" /> + <color name="vctr_voice_message_recording_playback_background_light">#FFE3E8F0</color> + <color name="vctr_voice_message_recording_playback_background_dark">#FF394049</color> </resources> diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index b2519f60b2..0dbb65a138 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -2605,6 +2605,7 @@ <string name="sent_a_video">Video.</string> <string name="sent_an_image">Image.</string> <string name="sent_an_audio_file">Audio</string> + <string name="sent_a_voice_message">Voice</string> <string name="sent_a_file">File</string> <string name="send_a_sticker">Sticker</string> <string name="sent_a_poll">Poll</string> @@ -3403,4 +3404,11 @@ <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> + <string name="a11y_play_voice_message">Play Voice Message</string> + <string name="a11y_pause_voice_message">Pause Voice Message</string> + <string name="a11y_recording_voice_message">Recording voice message</string> + <string name="a11y_delete_recorded_voice_message">Delete recorded voice message</string> + <string name="voice_message_release_to_send_toast">Release to send</string> + <string name="voice_message_n_seconds_warning_toast">%1$ds left</string> + <string name="voice_message_tap_on_waveform_to_stop_toast">Tap on the waveform to stop and playback</string> </resources> diff --git a/vector/src/main/res/values/theme_dark.xml b/vector/src/main/res/values/theme_dark.xml index 040e73501a..385388d3d4 100644 --- a/vector/src/main/res/values/theme_dark.xml +++ b/vector/src/main/res/values/theme_dark.xml @@ -144,6 +144,12 @@ <item name="vctr_social_login_button_gitlab_style">@style/WidgetButtonSocialLogin.Gitlab.Dark</item> <item name="actionModeStyle">@style/ActionModeTheme</item> + + <!-- Voice Message --> + <item name="vctr_voice_message_lock_background">@color/vctr_voice_message_lock_background_dark</item> + <item name="vctr_voice_message_playback_background">@color/vctr_voice_message_playback_background_dark</item> + <item name="vctr_voice_message_play_pause_button_background">@color/vctr_voice_message_play_pause_button_background_dark</item> + <item name="vctr_voice_message_recording_playback_background">@color/vctr_voice_message_recording_playback_background_dark</item> </style> <style name="AppTheme.Dark" parent="AppTheme.Base.Dark" /> diff --git a/vector/src/main/res/values/theme_light.xml b/vector/src/main/res/values/theme_light.xml index 9469b0f5e6..360eea0532 100644 --- a/vector/src/main/res/values/theme_light.xml +++ b/vector/src/main/res/values/theme_light.xml @@ -146,6 +146,12 @@ <item name="vctr_social_login_button_gitlab_style">@style/WidgetButtonSocialLogin.Gitlab.Light</item> <item name="actionModeStyle">@style/ActionModeTheme</item> + + <!-- Voice Message --> + <item name="vctr_voice_message_lock_background">@color/vctr_voice_message_lock_background_light</item> + <item name="vctr_voice_message_playback_background">@color/vctr_voice_message_playback_background_light</item> + <item name="vctr_voice_message_play_pause_button_background">@color/vctr_voice_message_play_pause_button_background_light</item> + <item name="vctr_voice_message_recording_playback_background">@color/vctr_voice_message_recording_playback_background_light</item> </style> <style name="AppTheme.Light" parent="AppTheme.Base.Light" />