Open room member avatar in a media viewer (#1911)

* Open room member avatar in viewer.

The `MediaViewer` was extracted to its own library module.

* Update screenshots

* Restore KSP processor in `:libraries:mediaviewer:api`, this should generate Showkase components again.

---------

Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
Jorge Martin Espinosa
2023-11-28 18:39:41 +01:00
committed by GitHub
parent d0cc8c0b2f
commit bbc4d18a9d
68 changed files with 439 additions and 122 deletions

1
changelog.d/1907.feature Normal file
View File

@@ -0,0 +1 @@
Open room member avatar when you click on it inside the member details screen.

View File

@@ -53,6 +53,7 @@ dependencies {
implementation(projects.libraries.dateformatter.api)
implementation(projects.libraries.eventformatter.api)
implementation(projects.libraries.mediapickers.api)
implementation(projects.libraries.mediaviewer.api)
implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.mediaupload.api)
implementation(projects.libraries.permissions.api)
@@ -92,6 +93,7 @@ dependencies {
testImplementation(projects.libraries.textcomposer.test)
testImplementation(projects.libraries.voicerecorder.test)
testImplementation(projects.libraries.mediaplayer.test)
testImplementation(projects.libraries.mediaviewer.test)
testImplementation(libs.test.mockk)
testImplementation(libs.test.junitext)
testImplementation(libs.test.robolectric)

View File

@@ -39,8 +39,6 @@ import io.element.android.features.messages.api.MessagesEntryPoint
import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewNode
import io.element.android.features.messages.impl.forward.ForwardMessagesNode
import io.element.android.features.messages.impl.media.local.MediaInfo
import io.element.android.features.messages.impl.media.viewer.MediaViewerNode
import io.element.android.features.messages.impl.report.ReportMessageNode
import io.element.android.features.messages.impl.timeline.debug.EventDebugInfoNode
import io.element.android.features.messages.impl.timeline.model.TimelineItem
@@ -62,6 +60,8 @@ 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.media.MediaSource
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.mediaviewer.api.local.MediaInfo
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode
import kotlinx.collections.immutable.ImmutableList
import kotlinx.parcelize.Parcelize
@@ -180,6 +180,8 @@ class MessagesFlowNode @AssistedInject constructor(
mediaInfo = navTarget.mediaInfo,
mediaSource = navTarget.mediaSource,
thumbnailSource = navTarget.thumbnailSource,
canDownload = true,
canShare = true,
)
createNode<MediaViewerNode>(buildContext, listOf(inputs))
}

View File

@@ -18,7 +18,7 @@ package io.element.android.features.messages.impl.attachments
import android.os.Parcelable
import androidx.compose.runtime.Immutable
import io.element.android.features.messages.impl.media.local.LocalMedia
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import kotlinx.parcelize.Parcelize
@Immutable

View File

@@ -19,10 +19,10 @@ package io.element.android.features.messages.impl.attachments.preview
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.core.net.toUri
import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.features.messages.impl.media.local.LocalMedia
import io.element.android.features.messages.impl.media.local.MediaInfo
import io.element.android.features.messages.impl.media.local.aFileInfo
import io.element.android.features.messages.impl.media.local.anImageInfo
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.aFileInfo
import io.element.android.libraries.mediaviewer.api.local.anImageInfo
open class AttachmentsPreviewStateProvider : PreviewParameterProvider<AttachmentsPreviewState> {
override val values: Sequence<AttachmentsPreviewState>

View File

@@ -32,7 +32,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError
import io.element.android.features.messages.impl.media.local.LocalMediaView
import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule
import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.ProgressDialogType
@@ -40,6 +39,7 @@ import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
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.TextButton
import io.element.android.libraries.mediaviewer.api.local.LocalMediaView
import io.element.android.libraries.ui.strings.CommonStrings
@Composable

View File

@@ -34,7 +34,6 @@ import androidx.media3.common.util.UnstableApi
import im.vector.app.features.analytics.plan.Composer
import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError
import io.element.android.features.messages.impl.media.local.LocalMediaFactory
import io.element.android.features.messages.impl.mentions.MentionSuggestion
import io.element.android.features.messages.impl.mentions.MentionSuggestionsProcessor
import io.element.android.libraries.architecture.Presenter
@@ -51,6 +50,7 @@ import io.element.android.libraries.matrix.api.room.Mention
import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
import io.element.android.libraries.mediapickers.api.PickerProvider
import io.element.android.libraries.mediaupload.api.MediaSender
import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory
import io.element.android.libraries.permissions.api.PermissionsEvents
import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.textcomposer.model.Message

View File

@@ -27,7 +27,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
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.libraries.androidutils.filesize.FileSizeFormatter
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.featureflag.api.FeatureFlagService
@@ -45,6 +44,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageTy
import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType
import io.element.android.libraries.matrix.ui.messages.toHtmlDocument
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import javax.inject.Inject
@@ -52,7 +52,7 @@ import kotlin.time.Duration
class TimelineItemContentMessageFactory @Inject constructor(
private val fileSizeFormatter: FileSizeFormatter,
private val fileExtensionExtractor: FileExtensionExtractor,
private val fileExtensionExtractor: io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor,
private val featureFlagService: FeatureFlagService,
) {

View File

@@ -16,7 +16,6 @@
package io.element.android.features.messages.impl.timeline.model.event
import io.element.android.features.messages.impl.media.helper.formatFileExtensionAndSize
import io.element.android.libraries.matrix.api.media.MediaSource
import kotlin.time.Duration
@@ -29,6 +28,10 @@ data class TimelineItemAudioContent(
val fileExtension: String,
) : TimelineItemEventContent {
val fileExtensionAndSize = formatFileExtensionAndSize(fileExtension, formattedFileSize)
val fileExtensionAndSize =
io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize(
fileExtension,
formattedFileSize
)
override val type: String = "TimelineItemAudioContent"
}

View File

@@ -16,8 +16,8 @@
package io.element.android.features.messages.impl.timeline.model.event
import io.element.android.features.messages.impl.media.helper.formatFileExtensionAndSize
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize
data class TimelineItemFileContent(
val body: String,

View File

@@ -27,7 +27,6 @@ import io.element.android.features.messages.impl.actionlist.ActionListState
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.fixtures.aMessageEvent
import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactory
import io.element.android.features.messages.impl.media.FakeLocalMediaFactory
import io.element.android.features.messages.impl.messagecomposer.MessageComposerContextImpl
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
import io.element.android.features.messages.impl.messagesummary.FakeMessageSummaryFormatter
@@ -80,6 +79,7 @@ import io.element.android.libraries.mediapickers.test.FakePickerProvider
import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer
import io.element.android.libraries.mediaupload.api.MediaSender
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory
import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory

View File

@@ -26,13 +26,13 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewEvents
import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewPresenter
import io.element.android.features.messages.impl.attachments.preview.SendActionState
import io.element.android.features.messages.impl.fixtures.aLocalMedia
import io.element.android.features.messages.impl.media.local.LocalMedia
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
import io.element.android.libraries.mediaupload.api.MediaSender
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.test.viewer.aLocalMedia
import io.element.android.tests.testutils.WarmUpRule
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi

View File

@@ -16,22 +16,10 @@
package io.element.android.features.messages.impl.fixtures
import android.net.Uri
import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.features.messages.impl.media.local.LocalMedia
import io.element.android.features.messages.impl.media.local.MediaInfo
import io.element.android.features.messages.impl.media.local.anImageInfo
fun aLocalMedia(
uri: Uri,
mediaInfo: MediaInfo = anImageInfo(),
) = LocalMedia(
uri = uri,
info = mediaInfo
)
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
fun aMediaAttachment(localMedia: LocalMedia, compressIfPossible: Boolean = true) = Attachment.Media(
localMedia = localMedia,
compressIfPossible = compressIfPossible,
)

View File

@@ -32,7 +32,6 @@ import io.element.android.features.messages.impl.timeline.factories.event.Timeli
import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemDaySeparatorFactory
import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory
import io.element.android.features.messages.impl.timeline.groups.TimelineItemGrouper
import io.element.android.features.messages.impl.timeline.util.FileExtensionExtractorWithoutValidation
import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter
import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
@@ -40,6 +39,7 @@ import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
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.test.FakeMatrixClient
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorWithoutValidation
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.test.TestScope

View File

@@ -26,7 +26,6 @@ import app.cash.turbine.ReceiveTurbine
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.Composer
import io.element.android.features.messages.impl.media.FakeLocalMediaFactory
import io.element.android.features.messages.impl.mentions.MentionSuggestion
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
import io.element.android.features.messages.impl.messagecomposer.MessageComposerContextImpl
@@ -67,6 +66,7 @@ import io.element.android.libraries.mediaupload.api.MediaPreProcessor
import io.element.android.libraries.mediaupload.api.MediaSender
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory
import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory

View File

@@ -27,7 +27,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
import io.element.android.features.messages.impl.timeline.util.FileExtensionExtractorWithoutValidation
import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.featureflag.api.FeatureFlagService
@@ -55,6 +54,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageT
import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorWithoutValidation
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.test.runTest
import org.junit.Test

View File

@@ -42,6 +42,7 @@ dependencies {
implementation(projects.libraries.androidutils)
implementation(projects.libraries.mediapickers.api)
implementation(projects.libraries.mediaupload.api)
implementation(projects.libraries.mediaviewer.api)
implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.permissions.api)
implementation(projects.libraries.preferences.api)

View File

@@ -34,12 +34,17 @@ import io.element.android.features.roomdetails.impl.edit.RoomDetailsEditNode
import io.element.android.features.roomdetails.impl.invite.RoomInviteMembersNode
import io.element.android.features.roomdetails.impl.members.RoomMemberListNode
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsNode
import io.element.android.features.roomdetails.impl.members.details.avatar.RoomMemberAvatarPreviewNode
import io.element.android.features.roomdetails.impl.notificationsettings.RoomNotificationSettingsNode
import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.mediaviewer.api.local.MediaInfo
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode
import kotlinx.parcelize.Parcelize
@ContributesNode(RoomScope::class)
@@ -79,6 +84,9 @@ class RoomDetailsFlowNode @AssistedInject constructor(
@Parcelize
data class RoomMemberDetails(val roomMemberId: UserId) : NavTarget
@Parcelize
data class MemberAvatarPreview(val userName: String, val avatarUrl: String) : NavTarget
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@@ -100,6 +108,10 @@ class RoomDetailsFlowNode @AssistedInject constructor(
override fun openRoomNotificationSettings() {
backstack.push(NavTarget.RoomNotificationSettings(showUserDefinedSettingStyle = false))
}
override fun openAvatarPreview(username: String, url: String) {
backstack.push(NavTarget.MemberAvatarPreview(username, url))
}
}
createNode<RoomDetailsNode>(buildContext, listOf(roomDetailsCallback))
}
@@ -136,9 +148,31 @@ class RoomDetailsFlowNode @AssistedInject constructor(
}
is NavTarget.RoomMemberDetails -> {
val plugins = listOf(RoomMemberDetailsNode.RoomMemberDetailsInput(navTarget.roomMemberId))
val callback = object : RoomMemberDetailsNode.Callback {
override fun openAvatarPreview(username: String, avatarUrl: String) {
backstack.push(NavTarget.MemberAvatarPreview(username, avatarUrl))
}
}
val plugins = listOf(RoomMemberDetailsNode.RoomMemberDetailsInput(navTarget.roomMemberId), callback)
createNode<RoomMemberDetailsNode>(buildContext, plugins)
}
is NavTarget.MemberAvatarPreview -> {
// We need to fake the MimeType here for the viewer to work.
val mimeType = MimeTypes.Images
val input = MediaViewerNode.Inputs(
mediaInfo = MediaInfo(
name = navTarget.userName,
mimeType = mimeType,
formattedFileSize = "",
fileExtension = ""
),
mediaSource = MediaSource(url = navTarget.avatarUrl),
thumbnailSource = MediaSource(url = navTarget.avatarUrl),
canDownload = false,
canShare = false,
)
createNode<RoomMemberAvatarPreviewNode>(buildContext, listOf(input))
}
}
}

View File

@@ -52,6 +52,7 @@ class RoomDetailsNode @AssistedInject constructor(
fun openInviteMembers()
fun editRoomDetails()
fun openRoomNotificationSettings()
fun openAvatarPreview(username: String, url: String)
}
private val callbacks = plugins<Callback>()
@@ -110,6 +111,10 @@ class RoomDetailsNode @AssistedInject constructor(
callbacks.forEach { it.editRoomDetails() }
}
private fun openAvatarPreview(username: String, url: String) {
callbacks.forEach { it.openAvatarPreview(username, url) }
}
@Composable
override fun View(modifier: Modifier) {
val context = LocalContext.current
@@ -140,6 +145,7 @@ class RoomDetailsNode @AssistedInject constructor(
openRoomMemberList = ::openRoomMemberList,
openRoomNotificationSettings = ::openRoomNotificationSettings,
invitePeople = ::invitePeople,
openAvatarPreview = ::openAvatarPreview,
)
}
}

View File

@@ -89,6 +89,7 @@ fun RoomDetailsView(
openRoomMemberList: () -> Unit,
openRoomNotificationSettings: () -> Unit,
invitePeople: () -> Unit,
openAvatarPreview: (username: String, url: String) -> Unit,
modifier: Modifier = Modifier,
) {
fun onShareMember() {
@@ -132,7 +133,12 @@ fun RoomDetailsView(
RoomMemberHeaderSection(
avatarUrl = state.roomAvatarUrl ?: member.avatarUrl,
userId = member.userId.value,
userName = state.roomName
userName = state.roomName,
openAvatarPreview = {
if (member.avatarUrl != null) {
openAvatarPreview(member.displayName ?: member.userId.value, member.avatarUrl!!)
}
},
)
RoomMemberMainActionsSection(onShareUser = ::onShareMember)
}
@@ -411,5 +417,6 @@ private fun ContentToPreview(state: RoomDetailsState) {
openRoomMemberList = {},
openRoomNotificationSettings = {},
invitePeople = {},
openAvatarPreview = { _, _ -> },
)
}

View File

@@ -46,11 +46,16 @@ class RoomMemberDetailsNode @AssistedInject constructor(
presenterFactory: RoomMemberDetailsPresenter.Factory,
) : Node(buildContext, plugins = plugins) {
interface Callback: NodeInputs {
fun openAvatarPreview(username: String, avatarUrl: String)
}
data class RoomMemberDetailsInput(
val roomMemberId: UserId
) : NodeInputs
private val inputs = inputs<RoomMemberDetailsInput>()
private val callback = inputs<Callback>()
private val presenter = presenterFactory.create(inputs.roomMemberId)
init {
@@ -84,7 +89,8 @@ class RoomMemberDetailsNode @AssistedInject constructor(
state = state,
modifier = modifier,
goBack = this::navigateUp,
onShareUser = ::onShareUser
onShareUser = ::onShareUser,
openAvatarPreview = callback::openAvatarPreview,
)
}
}

View File

@@ -17,7 +17,6 @@
package io.element.android.features.roomdetails.impl.members.details
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.height
@@ -38,12 +37,13 @@ import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.TopAppBar
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RoomMemberDetailsView(
state: RoomMemberDetailsState,
onShareUser: () -> Unit,
goBack: () -> Unit,
openAvatarPreview: (username: String, url: String) -> Unit,
modifier: Modifier = Modifier,
) {
Scaffold(
@@ -62,6 +62,9 @@ fun RoomMemberDetailsView(
avatarUrl = state.avatarUrl,
userId = state.userId,
userName = state.userName,
openAvatarPreview = { avatarUrl ->
openAvatarPreview(state.userName ?: state.userId, avatarUrl)
},
)
RoomMemberMainActionsSection(onShareUser = onShareUser)
@@ -110,5 +113,6 @@ private fun ContentToPreview(state: RoomMemberDetailsState) {
state = state,
onShareUser = {},
goBack = {},
openAvatarPreview = { _, _ -> }
)
}

View File

@@ -16,6 +16,7 @@
package io.element.android.features.roomdetails.impl.members.details
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@@ -41,13 +42,16 @@ fun RoomMemberHeaderSection(
avatarUrl: String?,
userId: String,
userName: String?,
openAvatarPreview: (url: String) -> Unit,
modifier: Modifier = Modifier
) {
Column(modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
Box(modifier = Modifier.size(70.dp)) {
Avatar(
avatarData = AvatarData(userId, userName, avatarUrl, AvatarSize.UserHeader),
modifier = Modifier.fillMaxSize()
modifier = Modifier
.clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) }
.fillMaxSize()
)
}
Spacer(modifier = Modifier.height(24.dp))

View File

@@ -0,0 +1,33 @@
/*
* 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.roomdetails.impl.members.details.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.RoomScope
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerPresenter
@ContributesNode(RoomScope::class)
class RoomMemberAvatarPreviewNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
presenterFactory: MediaViewerPresenter.Factory,
) : MediaViewerNode(buildContext, plugins, presenterFactory)

View File

@@ -60,4 +60,11 @@ object MimeTypes {
else -> OctetStream
}
}
fun hasSubtype(mimeType: String): Boolean {
val components = mimeType.split("/")
if (components.size != 2) return false
val subType = components.last()
return subType.isNotBlank() && subType != "*"
}
}

View File

@@ -82,7 +82,7 @@ class RustMediaLoader(
val mediaFile = innerClient.getMediaFile(
mediaSource = mediaSource,
body = body,
mimeType = mimeType ?: MimeTypes.OctetStream,
mimeType = mimeType?.takeIf { MimeTypes.hasSubtype(it) } ?: MimeTypes.OctetStream,
useCache = useCache,
tempDir = cacheDirectory.path,
)

View File

@@ -0,0 +1,63 @@
/*
* 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.
*/
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.anvil)
alias(libs.plugins.ksp)
id("kotlin-parcelize")
}
android {
namespace = "io.element.android.libraries.mediaviewer.api"
}
anvil {
generateDaggerFactories.set(true)
}
dependencies {
anvil(projects.anvilcodegen)
implementation(projects.anvilannotations)
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(projects.libraries.androidutils)
implementation(projects.libraries.architecture)
implementation(projects.libraries.core)
implementation(projects.libraries.di)
implementation(projects.libraries.designsystem)
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.turbine)
testImplementation(libs.coroutines.core)
testImplementation(libs.coroutines.test)
ksp(libs.showkase.processor)
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.messages.impl.media.helper
package io.element.android.libraries.mediaviewer.api.helper
fun formatFileExtensionAndSize(extension: String, size: String?): String {
return buildString {

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.messages.impl.media.local
package io.element.android.libraries.mediaviewer.api.local
import android.net.Uri
import android.os.Parcelable

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.messages.impl.media.local
package io.element.android.libraries.mediaviewer.api.local
import androidx.compose.runtime.Composable

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.messages.impl.media.local
package io.element.android.libraries.mediaviewer.api.local
import android.net.Uri
import io.element.android.libraries.matrix.api.media.MediaFile

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.messages.impl.media.local
package io.element.android.libraries.mediaviewer.api.local
import android.annotation.SuppressLint
import android.net.Uri
@@ -56,10 +56,9 @@ import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.PlayerView
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.messages.impl.media.helper.formatFileExtensionAndSize
import io.element.android.features.messages.impl.media.local.exoplayer.ExoPlayerWrapper
import io.element.android.features.messages.impl.media.local.pdf.PdfViewer
import io.element.android.features.messages.impl.media.local.pdf.rememberPdfViewerState
import io.element.android.libraries.mediaviewer.api.local.exoplayer.ExoPlayerWrapper
import io.element.android.libraries.mediaviewer.api.local.pdf.PdfViewer
import io.element.android.libraries.mediaviewer.api.local.pdf.rememberPdfViewerState
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeAudio
@@ -70,6 +69,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.designsystem.utils.KeepScreenOn
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize
import io.element.android.libraries.ui.strings.CommonStrings
import me.saket.telephoto.zoomable.ZoomSpec
import me.saket.telephoto.zoomable.ZoomableState

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.messages.impl.media.local
package io.element.android.libraries.mediaviewer.api.local
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.messages.impl.media.local
package io.element.android.libraries.mediaviewer.api.local
import android.os.Parcelable
import io.element.android.libraries.core.mimetype.MimeTypes

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.messages.impl.media.local.exoplayer
package io.element.android.libraries.mediaviewer.api.local.exoplayer
import android.content.Context
import androidx.media3.common.Player

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.messages.impl.media.local.pdf
package io.element.android.libraries.mediaviewer.api.local.pdf
import android.content.Context
import android.net.Uri

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.messages.impl.media.local.pdf
package io.element.android.libraries.mediaviewer.api.local.pdf
import android.graphics.Bitmap
import android.graphics.Canvas

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.messages.impl.media.local.pdf
package io.element.android.libraries.mediaviewer.api.local.pdf
import android.graphics.pdf.PdfRenderer
import android.os.ParcelFileDescriptor

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.messages.impl.media.local.pdf
package io.element.android.libraries.mediaviewer.api.local.pdf
import androidx.compose.foundation.Image
import androidx.compose.foundation.background

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.messages.impl.media.local.pdf
package io.element.android.libraries.mediaviewer.api.local.pdf
import android.content.Context
import androidx.compose.foundation.lazy.LazyListState

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.messages.impl.timeline.util
package io.element.android.libraries.mediaviewer.api.util
import android.webkit.MimeTypeMap
import com.squareup.anvil.annotations.ContributesBinding

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.messages.impl.media.viewer
package io.element.android.libraries.mediaviewer.api.viewer
sealed interface MediaViewerEvents {
data object SaveOnDisk: MediaViewerEvents

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.messages.impl.media.viewer
package io.element.android.libraries.mediaviewer.api.viewer
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -25,14 +25,14 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.compound.theme.ForcedDarkElementTheme
import io.element.android.features.messages.impl.media.local.MediaInfo
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.mediaviewer.api.local.MediaInfo
@ContributesNode(RoomScope::class)
class MediaViewerNode @AssistedInject constructor(
open class MediaViewerNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
presenterFactory: MediaViewerPresenter.Factory,
@@ -42,6 +42,8 @@ class MediaViewerNode @AssistedInject constructor(
val mediaInfo: MediaInfo,
val mediaSource: MediaSource,
val thumbnailSource: MediaSource?,
val canDownload: Boolean,
val canShare: Boolean,
) : NodeInputs
private val inputs: Inputs = inputs()

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.messages.impl.media.viewer
package io.element.android.libraries.mediaviewer.api.viewer
import android.content.ActivityNotFoundException
import androidx.compose.runtime.Composable
@@ -29,9 +29,6 @@ import androidx.compose.runtime.setValue
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.element.android.features.messages.impl.media.local.LocalMedia
import io.element.android.features.messages.impl.media.local.LocalMediaActions
import io.element.android.features.messages.impl.media.local.LocalMediaFactory
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
@@ -39,6 +36,9 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
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.MediaFile
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.ui.strings.CommonStrings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -91,6 +91,8 @@ class MediaViewerPresenter @AssistedInject constructor(
thumbnailSource = inputs.thumbnailSource,
downloadedMedia = localMedia.value,
snackbarMessage = snackbarMessage,
canDownload = inputs.canDownload,
canShare = inputs.canShare,
eventSink = ::handleEvents
)
}

View File

@@ -14,18 +14,20 @@
* limitations under the License.
*/
package io.element.android.features.messages.impl.media.viewer
package io.element.android.libraries.mediaviewer.api.viewer
import io.element.android.features.messages.impl.media.local.LocalMedia
import io.element.android.features.messages.impl.media.local.MediaInfo
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.api.local.MediaInfo
data class MediaViewerState(
val mediaInfo: MediaInfo,
val thumbnailSource: MediaSource?,
val downloadedMedia: Async<LocalMedia>,
val snackbarMessage: SnackbarMessage?,
val canDownload: Boolean,
val canShare: Boolean,
val eventSink: (MediaViewerEvents) -> Unit,
)

View File

@@ -14,18 +14,18 @@
* limitations under the License.
*/
package io.element.android.features.messages.impl.media.viewer
package io.element.android.libraries.mediaviewer.api.viewer
import android.net.Uri
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.messages.impl.media.local.LocalMedia
import io.element.android.features.messages.impl.media.local.MediaInfo
import io.element.android.features.messages.impl.media.local.aFileInfo
import io.element.android.features.messages.impl.media.local.aPdfInfo
import io.element.android.features.messages.impl.media.local.aVideoInfo
import io.element.android.features.messages.impl.media.local.anAudioInfo
import io.element.android.features.messages.impl.media.local.anImageInfo
import io.element.android.libraries.architecture.Async
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.aFileInfo
import io.element.android.libraries.mediaviewer.api.local.aPdfInfo
import io.element.android.libraries.mediaviewer.api.local.aVideoInfo
import io.element.android.libraries.mediaviewer.api.local.anAudioInfo
import io.element.android.libraries.mediaviewer.api.local.anImageInfo
open class MediaViewerStateProvider : PreviewParameterProvider<MediaViewerState> {
override val values: Sequence<MediaViewerState>
@@ -71,15 +71,27 @@ open class MediaViewerStateProvider : PreviewParameterProvider<MediaViewerState>
),
anAudioInfo(),
),
aMediaViewerState(
Async.Success(
LocalMedia(Uri.EMPTY, anImageInfo())
),
anImageInfo(),
canDownload = false,
canShare = false,
),
)
}
fun aMediaViewerState(
downloadedMedia: Async<LocalMedia> = Async.Uninitialized,
mediaInfo: MediaInfo = anImageInfo(),
canDownload: Boolean = true,
canShare: Boolean = true,
) = MediaViewerState(
mediaInfo = mediaInfo,
thumbnailSource = null,
downloadedMedia = downloadedMedia,
snackbarMessage = null
snackbarMessage = null,
canDownload = canDownload,
canShare = canShare,
) {}

View File

@@ -16,7 +16,7 @@
@file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.features.messages.impl.media.viewer
package io.element.android.libraries.mediaviewer.api.viewer
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
@@ -47,11 +47,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import io.element.android.features.messages.impl.R
import io.element.android.features.messages.impl.media.local.LocalMedia
import io.element.android.features.messages.impl.media.local.LocalMediaView
import io.element.android.features.messages.impl.media.local.MediaInfo
import io.element.android.features.messages.impl.media.local.rememberLocalMediaViewState
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.designsystem.components.button.BackButton
@@ -66,6 +61,11 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.ui.media.MediaRequestData
import io.element.android.libraries.mediaviewer.api.R
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.api.local.LocalMediaView
import io.element.android.libraries.mediaviewer.api.local.MediaInfo
import io.element.android.libraries.mediaviewer.api.local.rememberLocalMediaViewState
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.delay
@@ -96,6 +96,8 @@ fun MediaViewerView(
actionsEnabled = state.downloadedMedia is Async.Success,
mimeType = state.mediaInfo.mimeType,
onBackPressed = onBackPressed,
canDownload = state.canDownload,
canShare = state.canShare,
eventSink = state.eventSink
)
},
@@ -162,9 +164,12 @@ private fun rememberShowProgress(downloadedMedia: Async<LocalMedia>): Boolean {
return showProgress
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun MediaViewerTopBar(
actionsEnabled: Boolean,
canDownload: Boolean,
canShare: Boolean,
mimeType: String,
onBackPressed: () -> Unit,
eventSink: (MediaViewerEvents) -> Unit,
@@ -190,27 +195,31 @@ private fun MediaViewerTopBar(
)
}
}
IconButton(
enabled = actionsEnabled,
onClick = {
eventSink(MediaViewerEvents.SaveOnDisk)
},
) {
Icon(
resourceId = CompoundDrawables.ic_download,
contentDescription = stringResource(id = CommonStrings.action_save),
)
if (canDownload) {
IconButton(
enabled = actionsEnabled,
onClick = {
eventSink(MediaViewerEvents.SaveOnDisk)
},
) {
Icon(
resourceId = CompoundDrawables.ic_download,
contentDescription = stringResource(id = CommonStrings.action_save),
)
}
}
IconButton(
enabled = actionsEnabled,
onClick = {
eventSink(MediaViewerEvents.Share)
},
) {
Icon(
resourceId = CompoundDrawables.ic_share_android,
contentDescription = stringResource(id = CommonStrings.action_share)
)
if (canShare) {
IconButton(
enabled = actionsEnabled,
onClick = {
eventSink(MediaViewerEvents.Share)
},
) {
Icon(
resourceId = CompoundDrawables.ic_share_android,
contentDescription = stringResource(id = CommonStrings.action_share)
)
}
}
}
)

View File

@@ -16,20 +16,23 @@
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.features.messages.impl.media.viewer
package io.element.android.libraries.mediaviewer
import android.net.Uri
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.messages.impl.media.FakeLocalMediaActions
import io.element.android.features.messages.impl.media.FakeLocalMediaFactory
import io.element.android.features.messages.impl.media.local.aFileInfo
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.matrix.test.media.FakeMediaLoader
import io.element.android.libraries.matrix.test.media.aMediaSource
import io.element.android.libraries.mediaviewer.api.local.aFileInfo
import io.element.android.libraries.mediaviewer.test.FakeLocalMediaActions
import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerEvents
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerPresenter
import io.element.android.tests.testutils.WarmUpRule
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -148,12 +151,16 @@ class MediaViewerPresenterTest {
mediaLoader: FakeMediaLoader,
localMediaActions: FakeLocalMediaActions,
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
canShare: Boolean = true,
canDownload: Boolean = true,
): MediaViewerPresenter {
return MediaViewerPresenter(
inputs = MediaViewerNode.Inputs(
mediaInfo = TESTED_MEDIA_INFO,
mediaSource = aMediaSource(),
thumbnailSource = null
thumbnailSource = null,
canShare = canShare,
canDownload = canDownload,
),
localMediaFactory = localMediaFactory,
mediaLoader = mediaLoader,

View File

@@ -0,0 +1,52 @@
/*
* 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.
*/
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.anvil)
id("kotlin-parcelize")
}
android {
namespace = "io.element.android.libraries.mediaviewer.impl"
}
anvil {
generateDaggerFactories.set(true)
}
dependencies {
anvil(projects.anvilcodegen)
implementation(projects.anvilannotations)
implementation(libs.coroutines.core)
implementation(libs.dagger)
api(projects.libraries.mediaviewer.api)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.core)
implementation(projects.libraries.di)
implementation(projects.libraries.matrix.api)
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.turbine)
testImplementation(libs.coroutines.core)
testImplementation(libs.coroutines.test)
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.messages.impl.media.local
package io.element.android.libraries.mediaviewer.local
import android.app.Activity
import android.content.ContentResolver
@@ -43,6 +43,8 @@ import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
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.withContext
import timber.log.Timber

View File

@@ -14,13 +14,12 @@
* limitations under the License.
*/
package io.element.android.features.messages.impl.media.local
package io.element.android.libraries.mediaviewer.local
import android.content.Context
import android.net.Uri
import androidx.core.net.toUri
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.messages.impl.timeline.util.FileExtensionExtractor
import io.element.android.libraries.androidutils.file.getFileName
import io.element.android.libraries.androidutils.file.getFileSize
import io.element.android.libraries.androidutils.file.getMimeType
@@ -30,6 +29,10 @@ import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.matrix.api.media.MediaFile
import io.element.android.libraries.matrix.api.media.toFile
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.MediaInfo
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor
import javax.inject.Inject
@ContributesBinding(AppScope::class)

View File

@@ -0,0 +1,30 @@
/*
* 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.
*/
plugins {
id("io.element.android-compose-library")
}
android {
namespace = "io.element.android.libraries.mediaviewer.test"
}
dependencies {
api(projects.libraries.mediaviewer.impl)
implementation(projects.libraries.core)
implementation(projects.tests.testutils)
implementation(projects.libraries.matrix.api)
}

View File

@@ -14,11 +14,11 @@
* limitations under the License.
*/
package io.element.android.features.messages.impl.media
package io.element.android.libraries.mediaviewer.test
import androidx.compose.runtime.Composable
import io.element.android.features.messages.impl.media.local.LocalMedia
import io.element.android.features.messages.impl.media.local.LocalMediaActions
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.api.local.LocalMediaActions
import io.element.android.tests.testutils.simulateLongTask
class FakeLocalMediaActions : LocalMediaActions {

View File

@@ -14,17 +14,17 @@
* limitations under the License.
*/
package io.element.android.features.messages.impl.media
package io.element.android.libraries.mediaviewer.test
import android.net.Uri
import io.element.android.features.messages.impl.fixtures.aLocalMedia
import io.element.android.features.messages.impl.media.local.LocalMedia
import io.element.android.features.messages.impl.media.local.LocalMediaFactory
import io.element.android.features.messages.impl.media.local.MediaInfo
import io.element.android.features.messages.impl.timeline.util.FileExtensionExtractor
import io.element.android.features.messages.impl.timeline.util.FileExtensionExtractorWithoutValidation
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.matrix.api.media.MediaFile
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.MediaInfo
import io.element.android.libraries.mediaviewer.test.viewer.aLocalMedia
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorWithoutValidation
class FakeLocalMediaFactory(
private val localMediaUri: Uri,

View File

@@ -0,0 +1,31 @@
/*
* 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.libraries.mediaviewer.test.viewer
import android.net.Uri
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.anImageInfo
fun aLocalMedia(
uri: Uri,
mediaInfo: MediaInfo = anImageInfo(),
) = LocalMedia(
uri = uri,
info = mediaInfo
)

View File

@@ -105,6 +105,7 @@ fun DependencyHandlerScope.allLibrariesImpl() {
implementation(project(":libraries:cryptography:impl"))
implementation(project(":libraries:voicerecorder:impl"))
implementation(project(":libraries:mediaplayer:impl"))
implementation(project(":libraries:mediaviewer:impl"))
}
fun DependencyHandlerScope.allServicesImpl() {