Merge pull request #551 from vector-im/feature/fga/media_viewer_actions

Feature/fga/media viewer actions
This commit is contained in:
ganfra 2023-06-07 17:46:29 +02:00 committed by GitHub
commit db2a9f2ff1
75 changed files with 1105 additions and 360 deletions

View file

@ -20,17 +20,24 @@ import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import android.provider.OpenableColumns
import java.io.File
import androidx.core.net.toFile
fun Context.getMimeType(uri: Uri): String? = when (uri.scheme) {
ContentResolver.SCHEME_CONTENT -> contentResolver.getType(uri)
else -> null
}
fun Context.getFileName(uri: Uri): String? = when (uri.scheme) {
ContentResolver.SCHEME_CONTENT -> getContentFileName(uri)
else -> uri.path?.let(::File)?.name
ContentResolver.SCHEME_FILE -> uri.toFile().name
else -> null
}
fun Context.getFileSize(uri: Uri): Long {
return when (uri.scheme) {
ContentResolver.SCHEME_CONTENT -> getContentFileSize(uri)
else -> uri.path?.let(::File)?.length()
ContentResolver.SCHEME_FILE -> uri.toFile().length()
else -> 0
} ?: 0
}

View file

@ -66,6 +66,7 @@ fun Text(
lineHeight: TextUnit = TextUnit.Unspecified,
overflow: TextOverflow = TextOverflow.Clip,
softWrap: Boolean = true,
minLines: Int = 1,
maxLines: Int = Int.MAX_VALUE,
onTextLayout: (TextLayoutResult) -> Unit = {},
style: TextStyle = LocalTextStyle.current
@ -84,6 +85,7 @@ fun Text(
lineHeight = lineHeight,
overflow = overflow,
softWrap = softWrap,
minLines = minLines,
maxLines = maxLines,
onTextLayout = onTextLayout,
style = style,
@ -105,6 +107,7 @@ fun Text(
lineHeight: TextUnit = TextUnit.Unspecified,
overflow: TextOverflow = TextOverflow.Clip,
softWrap: Boolean = true,
minLines: Int = 1,
maxLines: Int = Int.MAX_VALUE,
inlineContent: ImmutableMap<String, InlineTextContent> = persistentMapOf(),
onTextLayout: (TextLayoutResult) -> Unit = {},
@ -124,6 +127,7 @@ fun Text(
lineHeight = lineHeight,
overflow = overflow,
softWrap = softWrap,
minLines = minLines,
maxLines = maxLines,
inlineContent = inlineContent,
onTextLayout = onTextLayout,

View file

@ -18,11 +18,14 @@ package io.element.android.libraries.designsystem.utils
import androidx.annotation.StringRes
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import kotlinx.coroutines.Dispatchers
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.res.stringResource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
@ -56,7 +59,7 @@ fun handleSnackbarMessage(
val snackbarMessage by snackbarDispatcher.snackbarMessage.collectAsState(initial = null)
LaunchedEffect(snackbarMessage) {
if (snackbarMessage != null) {
launch(Dispatchers.Main) {
launch {
snackbarDispatcher.clear()
}
}
@ -64,6 +67,25 @@ fun handleSnackbarMessage(
return snackbarMessage
}
@Composable
fun rememberSnackbarHostState(snackbarMessage: SnackbarMessage?): SnackbarHostState {
val snackbarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()
val snackbarMessageText = snackbarMessage?.let {
stringResource(id = snackbarMessage.messageResId)
}
LaunchedEffect(snackbarMessage) {
if (snackbarMessageText == null) return@LaunchedEffect
coroutineScope.launch {
snackbarHostState.showSnackbar(
message = snackbarMessageText,
duration = snackbarMessage.duration,
)
}
}
return snackbarHostState
}
data class SnackbarMessage(
@StringRes val messageResId: Int,
val duration: SnackbarDuration = SnackbarDuration.Short,

View file

@ -33,8 +33,9 @@ interface MatrixMediaLoader {
/**
* @param source to fetch the data for.
* @param mimeType: optional mime type
* @param mimeType: optional mime type.
* @param body: optional body which will be used to name the file.
* @return a [Result] of [MediaFile]
*/
suspend fun downloadMediaFile(source: MediaSource, mimeType: String?): Result<MediaFile>
suspend fun downloadMediaFile(source: MediaSource, mimeType: String?, body: String?): Result<MediaFile>
}

View file

@ -17,6 +17,7 @@
package io.element.android.libraries.matrix.api.media
import java.io.Closeable
import java.io.File
/**
* A wrapper around a media file on the disk.
@ -25,3 +26,7 @@ import java.io.Closeable
interface MediaFile : Closeable {
fun path(): String
}
fun MediaFile.toFile(): File {
return File(path())
}

View file

@ -79,6 +79,7 @@ class RustMatrixClient constructor(
private val coroutineScope: CoroutineScope,
private val dispatchers: CoroutineDispatchers,
private val baseDirectory: File,
private val baseCacheDirectory: File,
private val clock: SystemClock,
) : MatrixClient {
@ -188,7 +189,7 @@ class RustMatrixClient constructor(
override val invitesDataSource: RoomSummaryDataSource
get() = rustInvitesDataSource
private val rustMediaLoader = RustMediaLoader(dispatchers, client)
private val rustMediaLoader = RustMediaLoader(baseCacheDirectory, dispatchers, client)
override val mediaLoader: MatrixMediaLoader
get() = rustMediaLoader

View file

@ -16,10 +16,12 @@
package io.element.android.libraries.matrix.impl.auth
import android.content.Context
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.extensions.mapFailure
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
@ -48,6 +50,7 @@ import org.matrix.rustcomponents.sdk.AuthenticationService as RustAuthentication
@ContributesBinding(AppScope::class)
@SingleIn(AppScope::class)
class RustMatrixAuthenticationService @Inject constructor(
@ApplicationContext private val context: Context,
private val baseDirectory: File,
private val coroutineScope: CoroutineScope,
private val coroutineDispatchers: CoroutineDispatchers,
@ -179,6 +182,7 @@ class RustMatrixAuthenticationService @Inject constructor(
coroutineScope = coroutineScope,
dispatchers = coroutineDispatchers,
baseDirectory = baseDirectory,
baseCacheDirectory = context.cacheDir,
clock = clock,
)
}

View file

@ -24,13 +24,21 @@ import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.mediaSourceFromUrl
import org.matrix.rustcomponents.sdk.use
import java.io.File
import org.matrix.rustcomponents.sdk.MediaSource as RustMediaSource
class RustMediaLoader(
baseCacheDirectory: File,
private val dispatchers: CoroutineDispatchers,
private val innerClient: Client
private val innerClient: Client,
) : MatrixMediaLoader {
private val cacheDirectory = File(baseCacheDirectory, "temp/media").apply {
if (!exists()) {
mkdirs()
}
}
@OptIn(ExperimentalUnsignedTypes::class)
override suspend fun loadMediaContent(source: MediaSource): Result<ByteArray> =
withContext(dispatchers.io) {
@ -59,14 +67,16 @@ class RustMediaLoader(
}
}
override suspend fun downloadMediaFile(source: MediaSource, mimeType: String?): Result<MediaFile> =
override suspend fun downloadMediaFile(source: MediaSource, mimeType: String?, body: String?): Result<MediaFile> =
withContext(dispatchers.io) {
runCatching {
source.toRustMediaSource().use { mediaSource ->
val mediaFile = innerClient.getMediaFile(
mediaSource = mediaSource,
body = null,
mimeType = mimeType ?: "application/octet-stream"
body = body,
mimeType = mimeType ?: "application/octet-stream",
//TODO uncomment when rust api will be merged
//tempDir = cacheDirectory.path,
)
RustMediaFile(mediaFile)
}

View file

@ -26,4 +26,6 @@ dependencies {
api(projects.libraries.core)
api(projects.libraries.matrix.api)
api(libs.coroutines.core)
implementation(libs.coroutines.test)
implementation(projects.tests.testutils)
}

View file

@ -16,6 +16,7 @@
package io.element.android.libraries.matrix.test
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
@ -36,15 +37,18 @@ import io.element.android.libraries.matrix.test.pushers.FakePushersService
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.StandardTestDispatcher
class FakeMatrixClient(
override val sessionId: SessionId = A_SESSION_ID,
private val coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
private val userDisplayName: Result<String> = Result.success(A_USER_NAME),
private val userAvatarURLString: Result<String> = Result.success(AN_AVATAR_URL),
override val roomSummaryDataSource: RoomSummaryDataSource = FakeRoomSummaryDataSource(),
override val invitesDataSource: RoomSummaryDataSource = FakeRoomSummaryDataSource(),
override val mediaLoader: MatrixMediaLoader = FakeMediaLoader(),
override val mediaLoader: MatrixMediaLoader = FakeMediaLoader(coroutineDispatchers),
private val sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
private val pushersService: FakePushersService = FakePushersService(),
private val notificationService: FakeNotificationService = FakeNotificationService(),

View file

@ -16,40 +16,40 @@
package io.element.android.libraries.matrix.test.media
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
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.MediaSource
import io.element.android.libraries.matrix.test.FAKE_DELAY_IN_MS
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import kotlin.coroutines.coroutineContext
class FakeMediaLoader : MatrixMediaLoader {
class FakeMediaLoader(private val coroutineDispatchers: CoroutineDispatchers) : MatrixMediaLoader {
var shouldFail = false
override suspend fun loadMediaContent(source: MediaSource): Result<ByteArray> {
delay(FAKE_DELAY_IN_MS)
return if (shouldFail) {
override suspend fun loadMediaContent(source: MediaSource): Result<ByteArray> = withContext(coroutineDispatchers.io){
if (shouldFail) {
Result.failure(RuntimeException())
} else {
return Result.success(ByteArray(0))
Result.success(ByteArray(0))
}
}
override suspend fun loadMediaThumbnail(source: MediaSource, width: Long, height: Long): Result<ByteArray> {
delay(FAKE_DELAY_IN_MS)
return if (shouldFail) {
override suspend fun loadMediaThumbnail(source: MediaSource, width: Long, height: Long): Result<ByteArray> = withContext(coroutineDispatchers.io){
if (shouldFail) {
Result.failure(RuntimeException())
} else {
return Result.success(ByteArray(0))
Result.success(ByteArray(0))
}
}
override suspend fun downloadMediaFile(source: MediaSource, mimeType: String?): Result<MediaFile> {
delay(FAKE_DELAY_IN_MS)
return if (shouldFail) {
override suspend fun downloadMediaFile(source: MediaSource, mimeType: String?, body: String?): Result<MediaFile> = withContext(coroutineDispatchers.io){
if (shouldFail) {
Result.failure(RuntimeException())
} else {
return Result.success(FakeMediaFile(""))
Result.success(FakeMediaFile(""))
}
}
}

View file

@ -7,6 +7,7 @@
<string name="notification_inline_reply_failed">"** Failed to send - please open room"</string>
<string name="notification_invitation_action_join">"Join"</string>
<string name="notification_invitation_action_reject">"Reject"</string>
<string name="notification_invite_body">"invited you"</string>
<string name="notification_new_messages">"New Messages"</string>
<string name="notification_room_action_mark_as_read">"Mark as read"</string>
<string name="notification_sender_me">"Me"</string>

View file

@ -34,6 +34,7 @@
<string name="action_no">"No"</string>
<string name="action_not_now">"Not now"</string>
<string name="action_ok">"OK"</string>
<string name="action_open_with">"Open with"</string>
<string name="action_quick_reply">"Quick reply"</string>
<string name="action_quote">"Quote"</string>
<string name="action_remove">"Remove"</string>
@ -145,7 +146,20 @@
<string name="room_timeline_beginning_of_room">"This is the beginning of %1$s."</string>
<string name="room_timeline_beginning_of_room_no_name">"This is the beginning of this conversation."</string>
<string name="room_timeline_read_marker_title">"New"</string>
<string name="screen_account_provider_change">"Change account provider"</string>
<string name="screen_account_provider_continue">"Continue"</string>
<string name="screen_account_provider_form_hint">"Homeserver address"</string>
<string name="screen_account_provider_form_notice">"Enter a search term or a domain address."</string>
<string name="screen_account_provider_form_subtitle">"Search for a company, community, or private server."</string>
<string name="screen_account_provider_form_title">"Find an account provider"</string>
<string name="screen_account_provider_signin_title">"Youre about to sign in to %s"</string>
<string name="screen_account_provider_signup_subtitle">"This is where you conversations will live — just like you would use an email provider to keep your emails."</string>
<string name="screen_account_provider_signup_title">"Youre about to create an account on %s"</string>
<string name="screen_analytics_settings_share_data">"Share analytics data"</string>
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org is an open network for secure, decentralized communication."</string>
<string name="screen_change_account_provider_other">"Other"</string>
<string name="screen_change_account_provider_subtitle">"Use a different account provider, such as your own private server or a work account."</string>
<string name="screen_change_account_provider_title">"Change account provider"</string>
<string name="screen_media_picker_error_failed_selection">"Failed selecting media, please try again."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Failed processing media to upload, please try again."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Failed uploading media, please try again."</string>
@ -168,4 +182,4 @@
<string name="screen_analytics_settings_read_terms">"You can read all our terms %1$s."</string>
<string name="screen_analytics_settings_read_terms_content_link">"here"</string>
<string name="screen_report_content_block_user">"Block user"</string>
</resources>
</resources>