Move code to the impl module

This commit is contained in:
Benoit Marty 2024-11-28 16:07:37 +01:00 committed by Benoit Marty
parent 74b5991b56
commit b05d9507ea
49 changed files with 386 additions and 244 deletions

View file

@ -19,6 +19,7 @@ import com.bumble.appyx.core.node.node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push import com.bumble.appyx.navmodel.backstack.operation.push
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
@ -66,8 +67,8 @@ import io.element.android.libraries.matrix.api.room.joinedRoomMembers
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.matrix.ui.messages.LocalRoomMemberProfilesCache import io.element.android.libraries.matrix.ui.messages.LocalRoomMemberProfilesCache
import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache
import io.element.android.libraries.mediaviewer.api.local.MediaInfo import io.element.android.libraries.mediaviewer.api.MediaInfo
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
import io.element.android.libraries.textcomposer.mentions.LocalMentionSpanTheme import io.element.android.libraries.textcomposer.mentions.LocalMentionSpanTheme
import io.element.android.libraries.textcomposer.mentions.MentionSpanTheme import io.element.android.libraries.textcomposer.mentions.MentionSpanTheme
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
@ -86,6 +87,7 @@ class MessagesFlowNode @AssistedInject constructor(
private val showLocationEntryPoint: ShowLocationEntryPoint, private val showLocationEntryPoint: ShowLocationEntryPoint,
private val createPollEntryPoint: CreatePollEntryPoint, private val createPollEntryPoint: CreatePollEntryPoint,
private val elementCallEntryPoint: ElementCallEntryPoint, private val elementCallEntryPoint: ElementCallEntryPoint,
private val mediaViewerEntryPoint: MediaViewerEntryPoint,
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
private val room: MatrixRoom, private val room: MatrixRoom,
private val roomMemberProfilesCache: RoomMemberProfilesCache, private val roomMemberProfilesCache: RoomMemberProfilesCache,
@ -228,14 +230,22 @@ class MessagesFlowNode @AssistedInject constructor(
createNode<MessagesNode>(buildContext, listOf(callback, inputs)) createNode<MessagesNode>(buildContext, listOf(callback, inputs))
} }
is NavTarget.MediaViewer -> { is NavTarget.MediaViewer -> {
val inputs = MediaViewerNode.Inputs( val params = MediaViewerEntryPoint.Params(
mediaInfo = navTarget.mediaInfo, mediaInfo = navTarget.mediaInfo,
mediaSource = navTarget.mediaSource, mediaSource = navTarget.mediaSource,
thumbnailSource = navTarget.thumbnailSource, thumbnailSource = navTarget.thumbnailSource,
canDownload = true, canDownload = true,
canShare = true, canShare = true,
) )
createNode<MediaViewerNode>(buildContext, listOf(inputs)) val callback = object : MediaViewerEntryPoint.Callback {
override fun onDone() {
backstack.pop()
}
}
mediaViewerEntryPoint.nodeBuilder(this, buildContext)
.params(params)
.callback(callback)
.build()
} }
is NavTarget.AttachmentPreview -> { is NavTarget.AttachmentPreview -> {
val inputs = AttachmentsPreviewNode.Inputs(navTarget.attachment) val inputs = AttachmentsPreviewNode.Inputs(navTarget.attachment)

View file

@ -20,12 +20,14 @@ import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.mediaviewer.api.local.LocalMediaRenderer
@ContributesNode(RoomScope::class) @ContributesNode(RoomScope::class)
class AttachmentsPreviewNode @AssistedInject constructor( class AttachmentsPreviewNode @AssistedInject constructor(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
presenterFactory: AttachmentsPreviewPresenter.Factory, presenterFactory: AttachmentsPreviewPresenter.Factory,
private val localMediaRenderer: LocalMediaRenderer,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
data class Inputs(val attachment: Attachment) : NodeInputs data class Inputs(val attachment: Attachment) : NodeInputs
@ -46,6 +48,7 @@ class AttachmentsPreviewNode @AssistedInject constructor(
val state = presenter.present() val state = presenter.present()
AttachmentsPreviewView( AttachmentsPreviewView(
state = state, state = state,
localMediaRenderer = localMediaRenderer,
modifier = modifier modifier = modifier
) )
} }

View file

@ -10,12 +10,9 @@ package io.element.android.features.messages.impl.attachments.preview
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.core.net.toUri import androidx.core.net.toUri
import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.libraries.mediaviewer.api.MediaInfo
import io.element.android.libraries.mediaviewer.api.anImageMediaInfo
import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.api.local.MediaInfo
import io.element.android.libraries.mediaviewer.api.local.aVideoMediaInfo
import io.element.android.libraries.mediaviewer.api.local.anApkMediaInfo
import io.element.android.libraries.mediaviewer.api.local.anAudioMediaInfo
import io.element.android.libraries.mediaviewer.api.local.anImageMediaInfo
import io.element.android.libraries.textcomposer.model.TextEditorState import io.element.android.libraries.textcomposer.model.TextEditorState
import io.element.android.libraries.textcomposer.model.aTextEditorStateMarkdown import io.element.android.libraries.textcomposer.model.aTextEditorStateMarkdown
@ -23,9 +20,6 @@ open class AttachmentsPreviewStateProvider : PreviewParameterProvider<Attachment
override val values: Sequence<AttachmentsPreviewState> override val values: Sequence<AttachmentsPreviewState>
get() = sequenceOf( get() = sequenceOf(
anAttachmentsPreviewState(), anAttachmentsPreviewState(),
anAttachmentsPreviewState(mediaInfo = aVideoMediaInfo()),
anAttachmentsPreviewState(mediaInfo = anAudioMediaInfo()),
anAttachmentsPreviewState(mediaInfo = anApkMediaInfo()),
anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Processing), anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Processing),
anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Uploading(0.5f)), anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Uploading(0.5f)),
anAttachmentsPreviewState(sendActionState = SendActionState.Failure(RuntimeException("error"))), anAttachmentsPreviewState(sendActionState = SendActionState.Failure(RuntimeException("error"))),

View file

@ -8,6 +8,7 @@
package io.element.android.features.messages.impl.attachments.preview package io.element.android.features.messages.impl.attachments.preview
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.IntrinsicSize
@ -20,6 +21,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
@ -34,20 +36,20 @@ import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.mediaviewer.api.local.LocalMediaView import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.mediaviewer.api.local.rememberLocalMediaViewState import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.api.local.LocalMediaRenderer
import io.element.android.libraries.textcomposer.TextComposer import io.element.android.libraries.textcomposer.TextComposer
import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.libraries.textcomposer.model.VoiceMessageState import io.element.android.libraries.textcomposer.model.VoiceMessageState
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.wysiwyg.display.TextDisplay import io.element.android.wysiwyg.display.TextDisplay
import me.saket.telephoto.zoomable.ZoomSpec
import me.saket.telephoto.zoomable.rememberZoomableState
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AttachmentsPreviewView( fun AttachmentsPreviewView(
state: AttachmentsPreviewState, state: AttachmentsPreviewState,
localMediaRenderer: LocalMediaRenderer,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
fun postSendAttachment() { fun postSendAttachment() {
@ -82,6 +84,7 @@ fun AttachmentsPreviewView(
) { ) {
AttachmentPreviewContent( AttachmentPreviewContent(
state = state, state = state,
localMediaRenderer = localMediaRenderer,
onSendClick = ::postSendAttachment, onSendClick = ::postSendAttachment,
) )
} }
@ -129,6 +132,7 @@ private fun AttachmentSendStateView(
@Composable @Composable
private fun AttachmentPreviewContent( private fun AttachmentPreviewContent(
state: AttachmentsPreviewState, state: AttachmentsPreviewState,
localMediaRenderer: LocalMediaRenderer,
onSendClick: () -> Unit, onSendClick: () -> Unit,
) { ) {
Box( Box(
@ -142,17 +146,7 @@ private fun AttachmentPreviewContent(
) { ) {
when (val attachment = state.attachment) { when (val attachment = state.attachment) {
is Attachment.Media -> { is Attachment.Media -> {
val localMediaViewState = rememberLocalMediaViewState( localMediaRenderer.Render(attachment.localMedia)
zoomableState = rememberZoomableState(
zoomSpec = ZoomSpec(maxZoomFactor = 4f, preventOverOrUnderZoom = false)
)
)
LocalMediaView(
modifier = Modifier.fillMaxSize(),
localMedia = attachment.localMedia,
localMediaViewState = localMediaViewState,
onClick = {}
)
} }
} }
} }
@ -205,5 +199,15 @@ private fun AttachmentsPreviewBottomActions(
internal fun AttachmentsPreviewViewPreview(@PreviewParameter(AttachmentsPreviewStateProvider::class) state: AttachmentsPreviewState) = ElementPreviewDark { internal fun AttachmentsPreviewViewPreview(@PreviewParameter(AttachmentsPreviewStateProvider::class) state: AttachmentsPreviewState) = ElementPreviewDark {
AttachmentsPreviewView( AttachmentsPreviewView(
state = state, state = state,
localMediaRenderer = object : LocalMediaRenderer {
@Composable
override fun Render(localMedia: LocalMedia) {
Image(
painter = painterResource(id = CommonDrawables.sample_background),
modifier = Modifier.fillMaxSize(),
contentDescription = null,
)
}
}
) )
} }

View file

@ -35,7 +35,7 @@ import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorWithoutValidation import io.element.android.libraries.mediaviewer.test.util.FileExtensionExtractorWithoutValidation
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope

View file

@ -64,7 +64,7 @@ import io.element.android.libraries.matrix.test.media.aMediaSource
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.matrix.test.timeline.aStickerContent import io.element.android.libraries.matrix.test.timeline.aStickerContent
import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorWithoutValidation import io.element.android.libraries.mediaviewer.test.util.FileExtensionExtractorWithoutValidation
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest

View file

@ -15,6 +15,7 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push import com.bumble.appyx.navmodel.backstack.operation.push
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
@ -32,20 +33,16 @@ import io.element.android.features.roomdetails.impl.members.details.RoomMemberDe
import io.element.android.features.roomdetails.impl.notificationsettings.RoomNotificationSettingsNode import io.element.android.features.roomdetails.impl.notificationsettings.RoomNotificationSettingsNode
import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsFlowNode import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsFlowNode
import io.element.android.features.userprofile.shared.UserProfileNodeHelper import io.element.android.features.userprofile.shared.UserProfileNodeHelper
import io.element.android.features.userprofile.shared.avatar.AvatarPreviewNode
import io.element.android.libraries.architecture.BackstackWithOverlayBox import io.element.android.libraries.architecture.BackstackWithOverlayBox
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.overlay.operation.show import io.element.android.libraries.architecture.overlay.operation.show
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.mediaviewer.api.local.MediaInfo import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@ -59,6 +56,7 @@ class RoomDetailsFlowNode @AssistedInject constructor(
private val room: MatrixRoom, private val room: MatrixRoom,
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
private val messagesEntryPoint: MessagesEntryPoint, private val messagesEntryPoint: MessagesEntryPoint,
private val mediaViewerEntryPoint: MediaViewerEntryPoint,
) : BaseFlowNode<RoomDetailsFlowNode.NavTarget>( ) : BaseFlowNode<RoomDetailsFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
initialElement = plugins.filterIsInstance<RoomDetailsEntryPoint.Params>().first().initialElement.toNavTarget(), initialElement = plugins.filterIsInstance<RoomDetailsEntryPoint.Params>().first().initialElement.toNavTarget(),
@ -202,22 +200,18 @@ class RoomDetailsFlowNode @AssistedInject constructor(
createNode<RoomMemberDetailsNode>(buildContext, plugins) createNode<RoomMemberDetailsNode>(buildContext, plugins)
} }
is NavTarget.AvatarPreview -> { is NavTarget.AvatarPreview -> {
// We need to fake the MimeType here for the viewer to work. val callback = object : MediaViewerEntryPoint.Callback {
val mimeType = MimeTypes.Images override fun onDone() {
val input = MediaViewerNode.Inputs( backstack.pop()
mediaInfo = MediaInfo( }
filename = navTarget.name, }
caption = null, mediaViewerEntryPoint.nodeBuilder(this, buildContext)
mimeType = mimeType, .avatar(
formattedFileSize = "", navTarget.name,
fileExtension = "" navTarget.avatarUrl,
), )
mediaSource = MediaSource(url = navTarget.avatarUrl), .callback(callback)
thumbnailSource = null, .build()
canDownload = false,
canShare = false,
)
createNode<AvatarPreviewNode>(buildContext, listOf(input))
} }
is NavTarget.PollHistory -> { is NavTarget.PollHistory -> {

View file

@ -15,6 +15,7 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push import com.bumble.appyx.navmodel.backstack.operation.push
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
@ -24,18 +25,14 @@ import io.element.android.features.call.api.ElementCallEntryPoint
import io.element.android.features.userprofile.api.UserProfileEntryPoint import io.element.android.features.userprofile.api.UserProfileEntryPoint
import io.element.android.features.userprofile.impl.root.UserProfileNode import io.element.android.features.userprofile.impl.root.UserProfileNode
import io.element.android.features.userprofile.shared.UserProfileNodeHelper import io.element.android.features.userprofile.shared.UserProfileNodeHelper
import io.element.android.features.userprofile.shared.avatar.AvatarPreviewNode
import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
import io.element.android.libraries.mediaviewer.api.local.MediaInfo import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class) @ContributesNode(SessionScope::class)
@ -44,6 +41,7 @@ class UserProfileFlowNode @AssistedInject constructor(
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val elementCallEntryPoint: ElementCallEntryPoint, private val elementCallEntryPoint: ElementCallEntryPoint,
private val sessionIdHolder: CurrentSessionIdHolder, private val sessionIdHolder: CurrentSessionIdHolder,
private val mediaViewerEntryPoint: MediaViewerEntryPoint,
) : BaseFlowNode<UserProfileFlowNode.NavTarget>( ) : BaseFlowNode<UserProfileFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
initialElement = NavTarget.Root, initialElement = NavTarget.Root,
@ -80,22 +78,18 @@ class UserProfileFlowNode @AssistedInject constructor(
createNode<UserProfileNode>(buildContext, listOf(callback, params)) createNode<UserProfileNode>(buildContext, listOf(callback, params))
} }
is NavTarget.AvatarPreview -> { is NavTarget.AvatarPreview -> {
// We need to fake the MimeType here for the viewer to work. val callback = object : MediaViewerEntryPoint.Callback {
val mimeType = MimeTypes.Images override fun onDone() {
val input = MediaViewerNode.Inputs( backstack.pop()
mediaInfo = MediaInfo( }
}
mediaViewerEntryPoint.nodeBuilder(this, buildContext)
.avatar(
filename = navTarget.name, filename = navTarget.name,
caption = null, avatarUrl = navTarget.avatarUrl
mimeType = mimeType, )
formattedFileSize = "", .callback(callback)
fileExtension = "", .build()
),
mediaSource = MediaSource(url = navTarget.avatarUrl),
thumbnailSource = null,
canDownload = false,
canShare = false,
)
createNode<AvatarPreviewNode>(buildContext, listOf(input))
} }
} }
} }

View file

@ -1,24 +0,0 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.userprofile.shared.avatar
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.plugin.Plugin
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerPresenter
@ContributesNode(SessionScope::class)
class AvatarPreviewNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
presenterFactory: MediaViewerPresenter.Factory,
) : MediaViewerNode(buildContext, plugins, presenterFactory)

View file

@ -13,45 +13,12 @@ plugins {
android { android {
namespace = "io.element.android.libraries.mediaviewer.api" namespace = "io.element.android.libraries.mediaviewer.api"
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
} }
setupAnvil() setupAnvil()
dependencies { dependencies {
implementation(libs.coil.compose)
implementation(libs.androidx.media3.exoplayer)
implementation(libs.androidx.media3.ui)
implementation(libs.coroutines.core)
implementation(libs.dagger)
implementation(libs.telephoto.zoomableimage)
implementation(libs.vanniktech.blurhash)
implementation(libs.telephoto.flick)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture)
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.dateformatter.api) implementation(projects.libraries.architecture)
implementation(projects.libraries.di)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.uiStrings)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.mediaviewer.test)
testImplementation(projects.tests.testutils)
testImplementation(libs.test.junit)
testImplementation(libs.test.truth)
testImplementation(libs.test.mockk)
testImplementation(libs.test.robolectric)
testImplementation(libs.test.turbine)
testImplementation(libs.coroutines.core)
testImplementation(libs.coroutines.test)
testImplementation(libs.androidx.compose.ui.test.junit)
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
} }

View file

@ -1,11 +1,11 @@
/* /*
* Copyright 2023, 2024 New Vector Ltd. * Copyright 2024 New Vector Ltd.
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details. * Please see LICENSE in the repository root for full details.
*/ */
package io.element.android.libraries.mediaviewer.api.local package io.element.android.libraries.mediaviewer.api
import android.os.Parcelable import android.os.Parcelable
import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.core.mimetype.MimeTypes

View file

@ -0,0 +1,38 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.mediaviewer.api
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.matrix.api.media.MediaSource
interface MediaViewerEntryPoint : FeatureEntryPoint {
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
interface NodeBuilder {
fun callback(callback: Callback): NodeBuilder
fun params(params: Params): NodeBuilder
fun avatar(filename: String, avatarUrl: String): NodeBuilder
fun build(): Node
}
interface Callback : Plugin {
fun onDone()
}
data class Params(
val mediaInfo: MediaInfo,
val mediaSource: MediaSource,
val thumbnailSource: MediaSource?,
val canDownload: Boolean,
val canShare: Boolean,
) : NodeInputs
}

View file

@ -10,6 +10,7 @@ package io.element.android.libraries.mediaviewer.api.local
import android.net.Uri import android.net.Uri
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import io.element.android.libraries.mediaviewer.api.MediaInfo
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize

View file

@ -9,6 +9,7 @@ package io.element.android.libraries.mediaviewer.api.local
import android.net.Uri import android.net.Uri
import io.element.android.libraries.matrix.api.media.MediaFile import io.element.android.libraries.matrix.api.media.MediaFile
import io.element.android.libraries.mediaviewer.api.MediaInfo
interface LocalMediaFactory { interface LocalMediaFactory {
/** /**

View file

@ -0,0 +1,15 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.mediaviewer.api.local
import androidx.compose.runtime.Composable
interface LocalMediaRenderer {
@Composable
fun Render(localMedia: LocalMedia)
}

View file

@ -7,30 +7,6 @@
package io.element.android.libraries.mediaviewer.api.util package io.element.android.libraries.mediaviewer.api.util
import android.webkit.MimeTypeMap
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import javax.inject.Inject
interface FileExtensionExtractor { interface FileExtensionExtractor {
fun extractFromName(name: String): String fun extractFromName(name: String): String
} }
@ContributesBinding(AppScope::class)
class FileExtensionExtractorWithValidation @Inject constructor() : FileExtensionExtractor {
override fun extractFromName(name: String): String {
val fileExtension = name.substringAfterLast('.', "")
// Makes sure the extension is known by the system, otherwise default to binary extension.
return if (MimeTypeMap.getSingleton().hasExtension(fileExtension)) {
fileExtension
} else {
"bin"
}
}
}
class FileExtensionExtractorWithoutValidation : FileExtensionExtractor {
override fun extractFromName(name: String): String {
return name.substringAfterLast('.', "")
}
}

View file

@ -13,6 +13,11 @@ plugins {
android { android {
namespace = "io.element.android.libraries.mediaviewer.impl" namespace = "io.element.android.libraries.mediaviewer.impl"
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
} }
setupAnvil() setupAnvil()
@ -21,6 +26,23 @@ dependencies {
implementation(libs.coroutines.core) implementation(libs.coroutines.core)
implementation(libs.dagger) implementation(libs.dagger)
implementation(libs.coil.compose)
implementation(libs.androidx.media3.exoplayer)
implementation(libs.androidx.media3.ui)
implementation(libs.telephoto.zoomableimage)
implementation(libs.vanniktech.blurhash)
implementation(libs.telephoto.flick)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture)
implementation(projects.libraries.core)
implementation(projects.libraries.dateformatter.api)
implementation(projects.libraries.di)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.uiStrings)
api(projects.libraries.mediaviewer.api) api(projects.libraries.mediaviewer.api)
implementation(projects.libraries.androidutils) implementation(projects.libraries.androidutils)
implementation(projects.libraries.core) implementation(projects.libraries.core)
@ -39,4 +61,6 @@ dependencies {
testImplementation(libs.test.turbine) testImplementation(libs.test.turbine)
testImplementation(libs.coroutines.core) testImplementation(libs.coroutines.core)
testImplementation(libs.coroutines.test) testImplementation(libs.coroutines.test)
testImplementation(libs.androidx.compose.ui.test.junit)
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
} }

View file

@ -0,0 +1,64 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.mediaviewer.impl
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.mediaviewer.api.MediaInfo
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
import io.element.android.libraries.mediaviewer.impl.viewer.MediaViewerNode
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultMediaViewerEntryPoint @Inject constructor() : MediaViewerEntryPoint {
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): MediaViewerEntryPoint.NodeBuilder {
val plugins = ArrayList<Plugin>()
return object : MediaViewerEntryPoint.NodeBuilder {
override fun callback(callback: MediaViewerEntryPoint.Callback): MediaViewerEntryPoint.NodeBuilder {
plugins += callback
return this
}
override fun params(params: MediaViewerEntryPoint.Params): MediaViewerEntryPoint.NodeBuilder {
plugins += params
return this
}
override fun avatar(filename: String, avatarUrl: String): MediaViewerEntryPoint.NodeBuilder {
// We need to fake the MimeType here for the viewer to work.
val mimeType = MimeTypes.Images
return params(
MediaViewerEntryPoint.Params(
mediaInfo = MediaInfo(
filename = filename,
caption = null,
mimeType = mimeType,
formattedFileSize = "",
fileExtension = ""
),
mediaSource = MediaSource(url = avatarUrl),
thumbnailSource = null,
canDownload = false,
canShare = false,
)
)
}
override fun build(): Node {
return parentNode.createNode<MediaViewerNode>(buildContext, plugins)
}
}
}
}

View file

@ -35,7 +35,6 @@ import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.api.local.LocalMediaActions
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber

View file

@ -20,9 +20,9 @@ import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.matrix.api.media.MediaFile import io.element.android.libraries.matrix.api.media.MediaFile
import io.element.android.libraries.matrix.api.media.toFile import io.element.android.libraries.matrix.api.media.toFile
import io.element.android.libraries.mediaviewer.api.MediaInfo
import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory
import io.element.android.libraries.mediaviewer.api.local.MediaInfo
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor
import javax.inject.Inject import javax.inject.Inject

View file

@ -0,0 +1,37 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.mediaviewer.impl.local
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.api.local.LocalMediaRenderer
import me.saket.telephoto.zoomable.ZoomSpec
import me.saket.telephoto.zoomable.rememberZoomableState
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultLocalMediaRenderer @Inject constructor() : LocalMediaRenderer {
@Composable
override fun Render(localMedia: LocalMedia) {
val localMediaViewState = rememberLocalMediaViewState(
zoomableState = rememberZoomableState(
zoomSpec = ZoomSpec(maxZoomFactor = 4f, preventOverOrUnderZoom = false)
)
)
LocalMediaView(
modifier = Modifier.fillMaxSize(),
localMedia = localMedia,
localMediaViewState = localMediaViewState,
onClick = {}
)
}
}

View file

@ -5,9 +5,10 @@
* Please see LICENSE in the repository root for full details. * Please see LICENSE in the repository root for full details.
*/ */
package io.element.android.libraries.mediaviewer.api.local package io.element.android.libraries.mediaviewer.impl.local
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
interface LocalMediaActions { interface LocalMediaActions {
@Composable @Composable

View file

@ -5,7 +5,7 @@
* Please see LICENSE in the repository root for full details. * Please see LICENSE in the repository root for full details.
*/ */
package io.element.android.libraries.mediaviewer.api.local package io.element.android.libraries.mediaviewer.impl.local
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.net.Uri import android.net.Uri
@ -67,12 +67,14 @@ import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.designsystem.utils.KeepScreenOn import io.element.android.libraries.designsystem.utils.KeepScreenOn
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
import io.element.android.libraries.mediaviewer.api.MediaInfo
import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize
import io.element.android.libraries.mediaviewer.api.local.exoplayer.ExoPlayerWrapper import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.api.local.pdf.PdfViewer import io.element.android.libraries.mediaviewer.impl.local.exoplayer.ExoPlayerWrapper
import io.element.android.libraries.mediaviewer.api.local.pdf.rememberPdfViewerState import io.element.android.libraries.mediaviewer.impl.local.pdf.PdfViewer
import io.element.android.libraries.mediaviewer.api.player.MediaPlayerControllerState import io.element.android.libraries.mediaviewer.impl.local.pdf.rememberPdfViewerState
import io.element.android.libraries.mediaviewer.api.player.MediaPlayerControllerView import io.element.android.libraries.mediaviewer.impl.player.MediaPlayerControllerState
import io.element.android.libraries.mediaviewer.impl.player.MediaPlayerControllerView
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import me.saket.telephoto.zoomable.coil.ZoomableAsyncImage import me.saket.telephoto.zoomable.coil.ZoomableAsyncImage
@ -153,8 +155,8 @@ private fun MediaVideoView(
if (LocalInspectionMode.current) { if (LocalInspectionMode.current) {
Text( Text(
modifier = modifier modifier = modifier
.background(ElementTheme.colors.bgSubtlePrimary) .background(ElementTheme.colors.bgSubtlePrimary)
.wrapContentSize(), .wrapContentSize(),
text = "A Video Player will render here", text = "A Video Player will render here",
) )
} else { } else {
@ -267,8 +269,8 @@ private fun ExoPlayerMediaVideoView(
KeepScreenOn(mediaPlayerControllerState.isPlaying) KeepScreenOn(mediaPlayerControllerState.isPlaying)
Box( Box(
modifier = modifier modifier = modifier
.background(ElementTheme.colors.bgSubtlePrimary) .background(ElementTheme.colors.bgSubtlePrimary)
.wrapContentSize(), .wrapContentSize(),
) { ) {
AndroidView( AndroidView(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@ -318,8 +320,8 @@ private fun ExoPlayerMediaVideoView(
exoPlayer.volume = if (exoPlayer.volume == 1f) 0f else 1f exoPlayer.volume = if (exoPlayer.volume == 1f) 0f else 1f
}, },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.align(Alignment.BottomCenter), .align(Alignment.BottomCenter),
) )
} }
@ -369,20 +371,20 @@ private fun MediaFileView(
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
Box( Box(
modifier = modifier modifier = modifier
.padding(horizontal = 8.dp) .padding(horizontal = 8.dp)
.clickable( .clickable(
onClick = onClick, onClick = onClick,
interactionSource = interactionSource, interactionSource = interactionSource,
indication = null indication = null
), ),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Column(horizontalAlignment = Alignment.CenterHorizontally) { Column(horizontalAlignment = Alignment.CenterHorizontally) {
Box( Box(
modifier = Modifier modifier = Modifier
.size(72.dp) .size(72.dp)
.clip(CircleShape) .clip(CircleShape)
.background(MaterialTheme.colorScheme.onBackground), .background(MaterialTheme.colorScheme.onBackground),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
Icon( Icon(
@ -390,8 +392,8 @@ private fun MediaFileView(
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colorScheme.background, tint = MaterialTheme.colorScheme.background,
modifier = Modifier modifier = Modifier
.size(32.dp) .size(32.dp)
.rotate(if (isAudio) 0f else -45f), .rotate(if (isAudio) 0f else -45f),
) )
} }
if (info != null) { if (info != null) {

View file

@ -5,7 +5,7 @@
* Please see LICENSE in the repository root for full details. * Please see LICENSE in the repository root for full details.
*/ */
package io.element.android.libraries.mediaviewer.api.local package io.element.android.libraries.mediaviewer.impl.local
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable

View file

@ -5,7 +5,7 @@
* Please see LICENSE in the repository root for full details. * Please see LICENSE in the repository root for full details.
*/ */
package io.element.android.libraries.mediaviewer.api.local.exoplayer package io.element.android.libraries.mediaviewer.impl.local.exoplayer
import android.content.Context import android.content.Context
import androidx.media3.common.Player import androidx.media3.common.Player

View file

@ -5,7 +5,7 @@
* Please see LICENSE in the repository root for full details. * Please see LICENSE in the repository root for full details.
*/ */
package io.element.android.libraries.mediaviewer.api.local.pdf package io.element.android.libraries.mediaviewer.impl.local.pdf
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri

View file

@ -5,7 +5,7 @@
* Please see LICENSE in the repository root for full details. * Please see LICENSE in the repository root for full details.
*/ */
package io.element.android.libraries.mediaviewer.api.local.pdf package io.element.android.libraries.mediaviewer.impl.local.pdf
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Canvas import android.graphics.Canvas

View file

@ -5,7 +5,7 @@
* Please see LICENSE in the repository root for full details. * Please see LICENSE in the repository root for full details.
*/ */
package io.element.android.libraries.mediaviewer.api.local.pdf package io.element.android.libraries.mediaviewer.impl.local.pdf
import android.graphics.pdf.PdfRenderer import android.graphics.pdf.PdfRenderer
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor

View file

@ -5,7 +5,7 @@
* Please see LICENSE in the repository root for full details. * Please see LICENSE in the repository root for full details.
*/ */
package io.element.android.libraries.mediaviewer.api.local.pdf package io.element.android.libraries.mediaviewer.impl.local.pdf
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background

View file

@ -5,7 +5,7 @@
* Please see LICENSE in the repository root for full details. * Please see LICENSE in the repository root for full details.
*/ */
package io.element.android.libraries.mediaviewer.api.local.pdf package io.element.android.libraries.mediaviewer.impl.local.pdf
import android.content.Context import android.content.Context
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState

View file

@ -5,7 +5,7 @@
* Please see LICENSE in the repository root for full details. * Please see LICENSE in the repository root for full details.
*/ */
package io.element.android.libraries.mediaviewer.api.player package io.element.android.libraries.mediaviewer.impl.player
data class MediaPlayerControllerState( data class MediaPlayerControllerState(
val isVisible: Boolean, val isVisible: Boolean,

View file

@ -5,7 +5,7 @@
* Please see LICENSE in the repository root for full details. * Please see LICENSE in the repository root for full details.
*/ */
package io.element.android.libraries.mediaviewer.api.player package io.element.android.libraries.mediaviewer.impl.player
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider

View file

@ -5,7 +5,7 @@
* Please see LICENSE in the repository root for full details. * Please see LICENSE in the repository root for full details.
*/ */
package io.element.android.libraries.mediaviewer.api.player package io.element.android.libraries.mediaviewer.impl.player
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn

View file

@ -0,0 +1,27 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.mediaviewer.impl.util
import android.webkit.MimeTypeMap
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class FileExtensionExtractorWithValidation @Inject constructor() : FileExtensionExtractor {
override fun extractFromName(name: String): String {
val fileExtension = name.substringAfterLast('.', "")
// Makes sure the extension is known by the system, otherwise default to binary extension.
return if (MimeTypeMap.getSingleton().hasExtension(fileExtension)) {
fileExtension
} else {
"bin"
}
}
}

View file

@ -5,7 +5,7 @@
* Please see LICENSE in the repository root for full details. * Please see LICENSE in the repository root for full details.
*/ */
package io.element.android.libraries.mediaviewer.api.viewer package io.element.android.libraries.mediaviewer.impl.viewer
sealed interface MediaViewerEvents { sealed interface MediaViewerEvents {
data object SaveOnDisk : MediaViewerEvents data object SaveOnDisk : MediaViewerEvents

View file

@ -5,22 +5,21 @@
* Please see LICENSE in the repository root for full details. * Please see LICENSE in the repository root for full details.
*/ */
package io.element.android.libraries.mediaviewer.api.viewer package io.element.android.libraries.mediaviewer.impl.viewer
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.compound.theme.ForcedDarkElementTheme import io.element.android.compound.theme.ForcedDarkElementTheme
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
import io.element.android.libraries.mediaviewer.api.local.MediaInfo
@ContributesNode(RoomScope::class) @ContributesNode(RoomScope::class)
open class MediaViewerNode @AssistedInject constructor( open class MediaViewerNode @AssistedInject constructor(
@ -28,15 +27,13 @@ open class MediaViewerNode @AssistedInject constructor(
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
presenterFactory: MediaViewerPresenter.Factory, presenterFactory: MediaViewerPresenter.Factory,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
data class Inputs( private val inputs = inputs<MediaViewerEntryPoint.Params>()
val mediaInfo: MediaInfo,
val mediaSource: MediaSource,
val thumbnailSource: MediaSource?,
val canDownload: Boolean,
val canShare: Boolean,
) : NodeInputs
private val inputs: Inputs = inputs() private fun onDone() {
plugins<MediaViewerEntryPoint.Callback>().forEach {
it.onDone()
}
}
private val presenter = presenterFactory.create(inputs) private val presenter = presenterFactory.create(inputs)
@ -47,7 +44,7 @@ open class MediaViewerNode @AssistedInject constructor(
MediaViewerView( MediaViewerView(
state = state, state = state,
modifier = modifier, modifier = modifier,
onBackClick = this::navigateUp onBackClick = ::onDone
) )
} }
} }

View file

@ -5,7 +5,7 @@
* Please see LICENSE in the repository root for full details. * Please see LICENSE in the repository root for full details.
*/ */
package io.element.android.libraries.mediaviewer.api.viewer package io.element.android.libraries.mediaviewer.impl.viewer
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -27,16 +27,17 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.media.MediaFile import io.element.android.libraries.matrix.api.media.MediaFile
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.api.local.LocalMediaActions
import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory
import io.element.android.libraries.mediaviewer.impl.local.LocalMediaActions
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import io.element.android.libraries.androidutils.R as UtilsR import io.element.android.libraries.androidutils.R as UtilsR
class MediaViewerPresenter @AssistedInject constructor( class MediaViewerPresenter @AssistedInject constructor(
@Assisted private val inputs: MediaViewerNode.Inputs, @Assisted private val inputs: MediaViewerEntryPoint.Params,
private val localMediaFactory: LocalMediaFactory, private val localMediaFactory: LocalMediaFactory,
private val mediaLoader: MatrixMediaLoader, private val mediaLoader: MatrixMediaLoader,
private val localMediaActions: LocalMediaActions, private val localMediaActions: LocalMediaActions,
@ -44,7 +45,7 @@ class MediaViewerPresenter @AssistedInject constructor(
) : Presenter<MediaViewerState> { ) : Presenter<MediaViewerState> {
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
fun create(inputs: MediaViewerNode.Inputs): MediaViewerPresenter fun create(inputs: MediaViewerEntryPoint.Params): MediaViewerPresenter
} }
@Composable @Composable

View file

@ -5,13 +5,13 @@
* Please see LICENSE in the repository root for full details. * Please see LICENSE in the repository root for full details.
*/ */
package io.element.android.libraries.mediaviewer.api.viewer package io.element.android.libraries.mediaviewer.impl.viewer
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.mediaviewer.api.MediaInfo
import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.api.local.MediaInfo
data class MediaViewerState( data class MediaViewerState(
val mediaInfo: MediaInfo, val mediaInfo: MediaInfo,

View file

@ -5,18 +5,18 @@
* Please see LICENSE in the repository root for full details. * Please see LICENSE in the repository root for full details.
*/ */
package io.element.android.libraries.mediaviewer.api.viewer package io.element.android.libraries.mediaviewer.impl.viewer
import android.net.Uri import android.net.Uri
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.mediaviewer.api.MediaInfo
import io.element.android.libraries.mediaviewer.api.aPdfMediaInfo
import io.element.android.libraries.mediaviewer.api.aVideoMediaInfo
import io.element.android.libraries.mediaviewer.api.anApkMediaInfo
import io.element.android.libraries.mediaviewer.api.anAudioMediaInfo
import io.element.android.libraries.mediaviewer.api.anImageMediaInfo
import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.api.local.MediaInfo
import io.element.android.libraries.mediaviewer.api.local.aPdfMediaInfo
import io.element.android.libraries.mediaviewer.api.local.aVideoMediaInfo
import io.element.android.libraries.mediaviewer.api.local.anApkMediaInfo
import io.element.android.libraries.mediaviewer.api.local.anAudioMediaInfo
import io.element.android.libraries.mediaviewer.api.local.anImageMediaInfo
open class MediaViewerStateProvider : PreviewParameterProvider<MediaViewerState> { open class MediaViewerStateProvider : PreviewParameterProvider<MediaViewerState> {
override val values: Sequence<MediaViewerState> override val values: Sequence<MediaViewerState>

View file

@ -7,8 +7,9 @@
@file:OptIn(ExperimentalMaterial3Api::class) @file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.libraries.mediaviewer.api.viewer package io.element.android.libraries.mediaviewer.impl.viewer
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
@ -55,12 +56,12 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.ui.media.MediaRequestData import io.element.android.libraries.matrix.ui.media.MediaRequestData
import io.element.android.libraries.mediaviewer.api.R import io.element.android.libraries.mediaviewer.api.MediaInfo
import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.api.local.LocalMediaView import io.element.android.libraries.mediaviewer.impl.R
import io.element.android.libraries.mediaviewer.api.local.MediaInfo import io.element.android.libraries.mediaviewer.impl.local.LocalMediaView
import io.element.android.libraries.mediaviewer.api.local.PlayableState import io.element.android.libraries.mediaviewer.impl.local.PlayableState
import io.element.android.libraries.mediaviewer.api.local.rememberLocalMediaViewState import io.element.android.libraries.mediaviewer.impl.local.rememberLocalMediaViewState
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import me.saket.telephoto.flick.FlickToDismiss import me.saket.telephoto.flick.FlickToDismiss
@ -79,6 +80,7 @@ fun MediaViewerView(
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
var showOverlay by remember { mutableStateOf(true) } var showOverlay by remember { mutableStateOf(true) }
BackHandler { onBackClick() }
Scaffold( Scaffold(
modifier, modifier,
containerColor = Color.Transparent, containerColor = Color.Transparent,
@ -146,8 +148,8 @@ private fun MediaViewerPage(
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.navigationBarsPadding() .navigationBarsPadding()
) { ) {
Box(contentAlignment = Alignment.Center) { Box(contentAlignment = Alignment.Center) {
val zoomableState = rememberZoomableState( val zoomableState = rememberZoomableState(
@ -191,8 +193,8 @@ private fun MediaViewerPage(
if (showProgress) { if (showProgress) {
LinearProgressIndicator( LinearProgressIndicator(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(2.dp) .height(2.dp)
) )
} }
} }

View file

@ -12,9 +12,9 @@ import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter
import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.matrix.api.media.MediaFile import io.element.android.libraries.matrix.api.media.MediaFile
import io.element.android.libraries.matrix.test.media.FakeMediaFile import io.element.android.libraries.matrix.test.media.FakeMediaFile
import io.element.android.libraries.mediaviewer.api.local.MediaInfo import io.element.android.libraries.mediaviewer.api.MediaInfo
import io.element.android.libraries.mediaviewer.api.local.anImageMediaInfo import io.element.android.libraries.mediaviewer.api.anImageMediaInfo
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorWithoutValidation import io.element.android.libraries.mediaviewer.test.util.FileExtensionExtractorWithoutValidation
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner import org.robolectric.RobolectricTestRunner

View file

@ -5,9 +5,10 @@
* Please see LICENSE in the repository root for full details. * Please see LICENSE in the repository root for full details.
*/ */
package io.element.android.libraries.mediaviewer.api.util package io.element.android.libraries.mediaviewer.impl.util
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.mediaviewer.test.util.FileExtensionExtractorWithoutValidation
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner import org.robolectric.RobolectricTestRunner

View file

@ -7,7 +7,7 @@
@file:OptIn(ExperimentalCoroutinesApi::class) @file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.libraries.mediaviewer package io.element.android.libraries.mediaviewer.impl.viewer
import android.net.Uri import android.net.Uri
import app.cash.molecule.RecompositionMode import app.cash.molecule.RecompositionMode
@ -18,10 +18,8 @@ import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.matrix.test.media.FakeMatrixMediaLoader import io.element.android.libraries.matrix.test.media.FakeMatrixMediaLoader
import io.element.android.libraries.matrix.test.media.aMediaSource import io.element.android.libraries.matrix.test.media.aMediaSource
import io.element.android.libraries.mediaviewer.api.local.anApkMediaInfo import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerEvents import io.element.android.libraries.mediaviewer.api.anApkMediaInfo
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerPresenter
import io.element.android.libraries.mediaviewer.test.FakeLocalMediaActions import io.element.android.libraries.mediaviewer.test.FakeLocalMediaActions
import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
@ -144,7 +142,7 @@ class MediaViewerPresenterTest {
canDownload: Boolean = true, canDownload: Boolean = true,
): MediaViewerPresenter { ): MediaViewerPresenter {
return MediaViewerPresenter( return MediaViewerPresenter(
inputs = MediaViewerNode.Inputs( inputs = MediaViewerEntryPoint.Params(
mediaInfo = TESTED_MEDIA_INFO, mediaInfo = TESTED_MEDIA_INFO,
mediaSource = aMediaSource(), mediaSource = aMediaSource(),
thumbnailSource = null, thumbnailSource = null,

View file

@ -5,7 +5,7 @@
* Please see LICENSE in the repository root for full details. * Please see LICENSE in the repository root for full details.
*/ */
package io.element.android.libraries.mediaviewer.api.viewer package io.element.android.libraries.mediaviewer.impl.viewer
import android.net.Uri import android.net.Uri
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
@ -18,8 +18,8 @@ import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeDown import androidx.compose.ui.test.swipeDown
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.mediaviewer.api.anImageMediaInfo
import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.api.local.anImageMediaInfo
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.EventsRecorder

View file

@ -9,7 +9,7 @@ package io.element.android.libraries.mediaviewer.test
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.api.local.LocalMediaActions import io.element.android.libraries.mediaviewer.impl.local.LocalMediaActions
import io.element.android.tests.testutils.simulateLongTask import io.element.android.tests.testutils.simulateLongTask
class FakeLocalMediaActions : LocalMediaActions { class FakeLocalMediaActions : LocalMediaActions {

View file

@ -10,11 +10,11 @@ package io.element.android.libraries.mediaviewer.test
import android.net.Uri import android.net.Uri
import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.matrix.api.media.MediaFile import io.element.android.libraries.matrix.api.media.MediaFile
import io.element.android.libraries.mediaviewer.api.MediaInfo
import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory
import io.element.android.libraries.mediaviewer.api.local.MediaInfo
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorWithoutValidation import io.element.android.libraries.mediaviewer.test.util.FileExtensionExtractorWithoutValidation
import io.element.android.libraries.mediaviewer.test.viewer.aLocalMedia import io.element.android.libraries.mediaviewer.test.viewer.aLocalMedia
class FakeLocalMediaFactory( class FakeLocalMediaFactory(

View file

@ -0,0 +1,16 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.mediaviewer.test.util
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor
class FileExtensionExtractorWithoutValidation : FileExtensionExtractor {
override fun extractFromName(name: String): String {
return name.substringAfterLast('.', "")
}
}

View file

@ -8,9 +8,9 @@
package io.element.android.libraries.mediaviewer.test.viewer package io.element.android.libraries.mediaviewer.test.viewer
import android.net.Uri import android.net.Uri
import io.element.android.libraries.mediaviewer.api.MediaInfo
import io.element.android.libraries.mediaviewer.api.anImageMediaInfo
import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.api.local.MediaInfo
import io.element.android.libraries.mediaviewer.api.local.anImageMediaInfo
fun aLocalMedia( fun aLocalMedia(
uri: Uri, uri: Uri,