Merge pull request #551 from vector-im/feature/fga/media_viewer_actions
Feature/fga/media viewer actions
This commit is contained in:
commit
db2a9f2ff1
75 changed files with 1105 additions and 360 deletions
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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(""))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">"You’re 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">"You’re 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue