Merge branch 'develop' of https://github.com/vector-im/element-x-android into langleyd/live_waveform

This commit is contained in:
David Langley
2023-10-27 13:54:18 +01:00
37 changed files with 153 additions and 91 deletions

View File

@@ -38,6 +38,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.FloatingActionButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
@@ -59,6 +60,8 @@ import io.element.android.features.messages.impl.timeline.components.TimelineIte
import io.element.android.features.messages.impl.timeline.components.TimelineItemVirtualRow
import io.element.android.features.messages.impl.timeline.components.group.GroupHeaderView
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineLoadingMoreIndicator
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
import io.element.android.features.messages.impl.timeline.di.aFakeTimelineItemPresenterFactories
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider
@@ -333,15 +336,19 @@ internal fun TimelineViewPreview(
@PreviewParameter(TimelineItemEventContentProvider::class) content: TimelineItemEventContent
) = ElementPreview {
val timelineItems = aTimelineItemList(content)
TimelineView(
state = aTimelineState(timelineItems),
onMessageClicked = {},
onTimestampClicked = {},
onUserDataClicked = {},
onMessageLongClicked = {},
onReactionClicked = { _, _ -> },
onReactionLongClicked = { _, _ -> },
onMoreReactionsClicked = {},
onSwipeToReply = {},
)
CompositionLocalProvider(
LocalTimelineItemPresenterFactories provides aFakeTimelineItemPresenterFactories(),
) {
TimelineView(
state = aTimelineState(timelineItems),
onMessageClicked = {},
onTimestampClicked = {},
onUserDataClicked = {},
onMessageLongClicked = {},
onReactionClicked = { _, _ -> },
onReactionLongClicked = { _, _ -> },
onMoreReactionsClicked = {},
onSwipeToReply = {},
)
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright (c) 2023 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 io.element.android.features.messages.impl.timeline.di
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageState
import io.element.android.features.messages.impl.voicemessages.timeline.aVoiceMessageState
import io.element.android.libraries.architecture.Presenter
/**
* A fake [TimelineItemPresenterFactories] for screenshot tests.
*/
fun aFakeTimelineItemPresenterFactories() = TimelineItemPresenterFactories(
mapOf(
Pair(
TimelineItemVoiceContent::class.java,
TimelineItemPresenterFactory<TimelineItemVoiceContent, VoiceMessageState> { Presenter { aVoiceMessageState() } },
),
)
)

View File

@@ -29,7 +29,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
import io.element.android.features.messages.impl.timeline.util.FileExtensionExtractor
import io.element.android.features.messages.impl.timeline.util.toHtmlDocument
import io.element.android.features.messages.impl.voicemessages.fromMSC3246range
import io.element.android.libraries.androidutils.filesize.FileSizeFormatter
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.featureflag.api.FeatureFlagService
@@ -118,7 +117,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
mediaSource = messageType.source,
duration = messageType.info?.duration ?: Duration.ZERO,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
waveform = messageType.details?.waveform?.fromMSC3246range()?.toImmutableList() ?: persistentListOf(),
waveform = messageType.details?.waveform?.toImmutableList() ?: persistentListOf(),
)
else -> TimelineItemAudioContent(
body = messageType.body,

View File

@@ -28,8 +28,12 @@ class TimelineItemEventContentProvider : PreviewParameterProvider<TimelineItemEv
aTimelineItemVideoContent(),
aTimelineItemFileContent(),
aTimelineItemFileContent("A bigger file name which doesn't fit.pdf"),
aTimelineItemAudioContent(),
aTimelineItemAudioContent("An even bigger bigger bigger bigger bigger bigger bigger sound name which doesn't fit .mp3"),
aTimelineItemVoiceContent(),
aTimelineItemLocationContent(),
aTimelineItemLocationContent("Location description"),
aTimelineItemPollContent(),
aTimelineItemNoticeContent(),
aTimelineItemRedactedContent(),
aTimelineItemTextContent(),

View File

@@ -1,28 +0,0 @@
/*
* Copyright (c) 2023 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 io.element.android.features.messages.impl.voicemessages
/**
* Resizes the given [0;1024] int list as per unstable MSC3246 spec
* to a [0;1] range float list to be used for waveform rendering.
*/
internal fun List<Int>.fromMSC3246range(): List<Float> = map { it / 1024f }
/**
* Resizes the given [0;1] float list to [0;1024] int list as per unstable MSC3246 spec.
*/
internal fun List<Float>.toMSC3246range(): List<Int> = map { (it * 1024).toInt() }

View File

@@ -28,7 +28,6 @@ import androidx.compose.runtime.setValue
import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle
import io.element.android.features.messages.impl.voicemessages.VoiceMessageException
import io.element.android.features.messages.impl.voicemessages.toMSC3246range
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.di.SingleIn
@@ -220,7 +219,7 @@ class VoiceMessageComposerPresenter @Inject constructor(
val result = mediaSender.sendVoiceMessage(
uri = file.toUri(),
mimeType = mimeType,
waveForm = waveform.toMSC3246range(),
waveForm = waveform,
)
if (result.isFailure) {

View File

@@ -21,35 +21,41 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
open class VoiceMessageStateProvider : PreviewParameterProvider<VoiceMessageState> {
override val values: Sequence<VoiceMessageState>
get() = sequenceOf(
VoiceMessageState(
aVoiceMessageState(
VoiceMessageState.Button.Downloading,
progress = 0f,
time = "0:00",
eventSink = {},
),
VoiceMessageState(
aVoiceMessageState(
VoiceMessageState.Button.Retry,
progress = 0.5f,
time = "0:01",
eventSink = {}
),
VoiceMessageState(
aVoiceMessageState(
VoiceMessageState.Button.Play,
progress = 1f,
time = "1:00",
eventSink = {}
),
VoiceMessageState(
aVoiceMessageState(
VoiceMessageState.Button.Pause,
progress = 0.2f,
time = "10:00",
eventSink = {}
),
VoiceMessageState(
aVoiceMessageState(
VoiceMessageState.Button.Disabled,
progress = 0.2f,
time = "30:00",
eventSink = {}
),
)
}
fun aVoiceMessageState(
button: VoiceMessageState.Button = VoiceMessageState.Button.Play,
progress: Float = 0f,
time: String = "1:00",
) = VoiceMessageState(
button = button,
progress = progress,
time = time,
eventSink = {},
)

View File

@@ -18,7 +18,7 @@ package io.element.android.libraries.architecture
import androidx.compose.runtime.Composable
interface Presenter<State> {
fun interface Presenter<State> {
@Composable
fun present(): State
}

View File

@@ -20,5 +20,5 @@ import java.time.Duration
data class AudioDetails(
val duration: Duration,
val waveform: List<Int>,
val waveform: List<Float>,
)

View File

@@ -195,7 +195,7 @@ interface MatrixRoom : Closeable {
suspend fun sendVoiceMessage(
file: File,
audioInfo: AudioInfo,
waveform: List<Int>,
waveform: List<Float>,
progressCallback: ProgressCallback?
): Result<MediaUploadHandler>

View File

@@ -21,10 +21,26 @@ import org.matrix.rustcomponents.sdk.UnstableAudioDetailsContent as RustAudioDet
fun RustAudioDetails.map(): AudioDetails = AudioDetails(
duration = duration,
waveform = waveform.map { it.toInt() },
waveform = waveform.fromMSC3246range(),
)
fun AudioDetails.map(): RustAudioDetails = RustAudioDetails(
duration = duration,
waveform = waveform.map { it.toUShort() }
waveform = waveform.toMSC3246range()
)
/**
* Resizes the given [0;1024] int list as per unstable MSC3246 spec
* to a [0;1] float list to be used for waveform rendering.
*
* https://github.com/matrix-org/matrix-spec-proposals/blob/travis/msc/audio-waveform/proposals/3246-audio-waveform.md
*/
internal fun List<UShort>.fromMSC3246range(): List<Float> = map { it.toInt() / 1024f }
/**
* Resizes the given [0;1] float list as per unstable MSC3246 spec
* to a [0;1024] int list to be used for waveform rendering.
*
* https://github.com/matrix-org/matrix-spec-proposals/blob/travis/msc/audio-waveform/proposals/3246-audio-waveform.md
*/
internal fun List<Float>.toMSC3246range(): List<UShort> = map { (it * 1024).toInt().toUShort() }

View File

@@ -46,6 +46,7 @@ import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
import io.element.android.libraries.matrix.impl.core.toProgressWatcher
import io.element.android.libraries.matrix.impl.media.MediaUploadHandlerImpl
import io.element.android.libraries.matrix.impl.media.map
import io.element.android.libraries.matrix.impl.media.toMSC3246range
import io.element.android.libraries.matrix.impl.notificationsettings.RustNotificationSettingsService
import io.element.android.libraries.matrix.impl.poll.toInner
import io.element.android.libraries.matrix.impl.room.location.toInner
@@ -499,13 +500,13 @@ class RustMatrixRoom(
override suspend fun sendVoiceMessage(
file: File,
audioInfo: AudioInfo,
waveform: List<Int>,
waveform: List<Float>,
progressCallback: ProgressCallback?,
): Result<MediaUploadHandler> = sendAttachment(listOf(file)) {
innerRoom.sendVoiceMessage(
url = file.path,
audioInfo = audioInfo.map(),
waveform = waveform.map { it.toUShort() },
waveform = waveform.toMSC3246range(),
progressWatcher = progressCallback?.toProgressWatcher(),
)
}

View File

@@ -387,7 +387,7 @@ class FakeMatrixRoom(
override suspend fun sendVoiceMessage(
file: File,
audioInfo: AudioInfo,
waveform: List<Int>,
waveform: List<Float>,
progressCallback: ProgressCallback?
): Result<MediaUploadHandler> = fakeSendMedia(progressCallback)

View File

@@ -55,7 +55,7 @@ class MediaSender @Inject constructor(
suspend fun sendVoiceMessage(
uri: Uri,
mimeType: String,
waveForm: List<Int>,
waveForm: List<Float>,
progressCallback: ProgressCallback? = null
): Result<Unit> {
return preProcessor

View File

@@ -29,6 +29,6 @@ sealed interface MediaUploadInfo {
data class Image(override val file: File, val imageInfo: ImageInfo, val thumbnailFile: File) : MediaUploadInfo
data class Video(override val file: File, val videoInfo: VideoInfo, val thumbnailFile: File) : MediaUploadInfo
data class Audio(override val file: File, val audioInfo: AudioInfo) : MediaUploadInfo
data class VoiceMessage(override val file: File, val audioInfo: AudioInfo, val waveform: List<Int>) : MediaUploadInfo
data class VoiceMessage(override val file: File, val audioInfo: AudioInfo, val waveform: List<Float>) : MediaUploadInfo
data class AnyFile(override val file: File, val fileInfo: FileInfo) : MediaUploadInfo
}