Merge branch 'develop' into feature/fga/room_list_api

This commit is contained in:
ganfra 2023-06-22 11:31:49 +02:00
commit 5a9f0caed7
35 changed files with 368 additions and 65 deletions

View file

@ -18,9 +18,7 @@ jobs:
if: ${{ github.repository == 'vector-im/element-x-android' }} if: ${{ github.repository == 'vector-im/element-x-android' }}
steps: steps:
- name: ⏬ Checkout with LFS - name: ⏬ Checkout with LFS
uses: actions/checkout@v3 uses: nschloe/action-cached-lfs-checkout@v1.2.1
with:
lfs: 'true'
- name: Use JDK 17 - name: Use JDK 17
uses: actions/setup-java@v3 uses: actions/setup-java@v3

View file

@ -14,10 +14,9 @@ jobs:
steps: steps:
- name: ⏬ Checkout with LFS - name: ⏬ Checkout with LFS
uses: actions/checkout@v3 uses: nschloe/action-cached-lfs-checkout@v1.2.1
with: with:
persist-credentials: false persist-credentials: false
lfs: 'true'
- name: ☕️ Use JDK 17 - name: ☕️ Use JDK 17
uses: actions/setup-java@v3 uses: actions/setup-java@v3
with: with:

View file

@ -22,12 +22,11 @@ jobs:
cancel-in-progress: true cancel-in-progress: true
steps: steps:
- name: ⏬ Checkout with LFS - name: ⏬ Checkout with LFS
uses: actions/checkout@v3 uses: nschloe/action-cached-lfs-checkout@v1.2.1
with: with:
# Ensure we are building the branch and not the branch after being merged on develop # Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881 # https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
lfs: 'true'
- name: ☕️ Use JDK 17 - name: ☕️ Use JDK 17
uses: actions/setup-java@v3 uses: actions/setup-java@v3
with: with:
@ -81,8 +80,12 @@ jobs:
**/out/failures/ **/out/failures/
**/build/reports/tests/*UnitTest/ **/build/reports/tests/*UnitTest/
- name: 🔊 Publish results to Sonar (disabled) - name: 🔊 Publish results to Sonar
run: echo "This is now done only once a day, see nightlyReports.yml" env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
ORG_GRADLE_PROJECT_SONAR_LOGIN: ${{ secrets.SONAR_TOKEN }}
if: ${{ always() && env.SONAR_TOKEN != '' && env.ORG_GRADLE_PROJECT_SONAR_LOGIN != '' }}
run: ./gradlew sonar $CI_GRADLE_ARG_PROPERTIES
# https://github.com/codecov/codecov-action # https://github.com/codecov/codecov-action
- name: ☂️ Upload coverage reports to codecov - name: ☂️ Upload coverage reports to codecov

View file

@ -7,9 +7,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Validate name: Validate
steps: steps:
- uses: actions/checkout@v3 - uses: nschloe/action-cached-lfs-checkout@v1.2.1
with:
lfs: 'true'
- run: | - run: |
./tools/git/validate_lfs.sh ./tools/git/validate_lfs.sh

View file

@ -20,6 +20,7 @@ import android.net.Uri
import io.element.android.features.createroom.impl.configureroom.RoomPrivacy import io.element.android.features.createroom.impl.configureroom.RoomPrivacy
import io.element.android.features.createroom.impl.di.CreateRoomScope import io.element.android.features.createroom.impl.di.CreateRoomScope
import io.element.android.features.createroom.impl.userlist.UserListDataStore import io.element.android.features.createroom.impl.userlist.UserListDataStore
import io.element.android.libraries.androidutils.file.safeDelete
import io.element.android.libraries.di.SingleIn import io.element.android.libraries.di.SingleIn
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -36,7 +37,7 @@ class CreateRoomDataStore @Inject constructor(
private val createRoomConfigFlow: MutableStateFlow<CreateRoomConfig> = MutableStateFlow(CreateRoomConfig()) private val createRoomConfigFlow: MutableStateFlow<CreateRoomConfig> = MutableStateFlow(CreateRoomConfig())
private var cachedAvatarUri: Uri? = null private var cachedAvatarUri: Uri? = null
set(value) { set(value) {
field?.path?.let { File(it) }?.delete() field?.path?.let { File(it) }?.safeDelete()
field = value field = value
} }

View file

@ -131,6 +131,6 @@ class ConfigureRoomPresenter @Inject constructor(
private suspend fun uploadAvatar(avatarUri: Uri): String { private suspend fun uploadAvatar(avatarUri: Uri): String {
val preprocessed = mediaPreProcessor.process(avatarUri, MimeTypes.Jpeg, compressIfPossible = false).getOrThrow() val preprocessed = mediaPreProcessor.process(avatarUri, MimeTypes.Jpeg, compressIfPossible = false).getOrThrow()
val byteArray = preprocessed.file.readBytes() val byteArray = preprocessed.file.readBytes()
return matrixClient.uploadMedia(MimeTypes.Jpeg, byteArray).getOrThrow() return matrixClient.uploadMedia(MimeTypes.Jpeg, byteArray, null).getOrThrow()
} }
} }

View file

@ -80,8 +80,7 @@ fun ConfirmAccountProviderView(
id = if (state.isAccountCreation) { id = if (state.isAccountCreation) {
R.string.screen_account_provider_signup_subtitle R.string.screen_account_provider_signup_subtitle
} else { } else {
// Use same value for now. R.string.screen_account_provider_signin_subtitle
R.string.screen_account_provider_signup_subtitle
}, },
) )
) )

View file

@ -55,8 +55,10 @@ import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.handleSnackbarMessage import io.element.android.libraries.designsystem.utils.handleSnackbarMessage
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
import io.element.android.libraries.matrix.ui.room.canSendEventAsState
import io.element.android.libraries.textcomposer.MessageComposerMode import io.element.android.libraries.textcomposer.MessageComposerMode
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -86,6 +88,7 @@ class MessagesPresenter @Inject constructor(
val retryState = retrySendMenuPresenter.present() val retryState = retrySendMenuPresenter.present()
val syncUpdateFlow = room.syncUpdateFlow().collectAsState(0L) val syncUpdateFlow = room.syncUpdateFlow().collectAsState(0L)
val userHasPermissionToSendMessage by room.canSendEventAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value)
val roomName: MutableState<String?> = rememberSaveable { val roomName: MutableState<String?> = rememberSaveable {
mutableStateOf(null) mutableStateOf(null)
} }
@ -125,6 +128,7 @@ class MessagesPresenter @Inject constructor(
roomId = room.roomId, roomId = room.roomId,
roomName = roomName.value, roomName = roomName.value,
roomAvatar = roomAvatar.value, roomAvatar = roomAvatar.value,
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
composerState = composerState, composerState = composerState,
timelineState = timelineState, timelineState = timelineState,
actionListState = actionListState, actionListState = actionListState,

View file

@ -31,6 +31,7 @@ data class MessagesState(
val roomId: RoomId, val roomId: RoomId,
val roomName: String?, val roomName: String?,
val roomAvatar: AvatarData?, val roomAvatar: AvatarData?,
val userHasPermissionToSendMessage: Boolean,
val composerState: MessageComposerState, val composerState: MessageComposerState,
val timelineState: TimelineState, val timelineState: TimelineState,
val actionListState: ActionListState, val actionListState: ActionListState,

View file

@ -35,6 +35,7 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
aMessagesState(), aMessagesState(),
aMessagesState().copy(hasNetworkConnection = false), aMessagesState().copy(hasNetworkConnection = false),
aMessagesState().copy(composerState = aMessageComposerState().copy(showAttachmentSourcePicker = true)), aMessagesState().copy(composerState = aMessageComposerState().copy(showAttachmentSourcePicker = true)),
aMessagesState().copy(userHasPermissionToSendMessage = false),
) )
} }
@ -42,6 +43,7 @@ fun aMessagesState() = MessagesState(
roomId = RoomId("!id:domain"), roomId = RoomId("!id:domain"),
roomName = "Room name", roomName = "Room name",
roomAvatar = AvatarData("!id:domain", "Room name"), roomAvatar = AvatarData("!id:domain", "Room name"),
userHasPermissionToSendMessage = true,
composerState = aMessageComposerState().copy( composerState = aMessageComposerState().copy(
text = StableCharSequence("Hello"), text = StableCharSequence("Hello"),
isFullScreen = false, isFullScreen = false,

View file

@ -16,7 +16,9 @@
package io.element.android.features.messages.impl package io.element.android.features.messages.impl
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -34,6 +36,7 @@ import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@ -41,7 +44,9 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
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
@ -231,12 +236,16 @@ fun MessagesViewContent(
onTimestampClicked = onTimestampClicked, onTimestampClicked = onTimestampClicked,
) )
} }
MessageComposerView( if (state.userHasPermissionToSendMessage) {
state = state.composerState, MessageComposerView(
modifier = Modifier state = state.composerState,
.fillMaxWidth() modifier = Modifier
.wrapContentHeight(Alignment.Bottom) .fillMaxWidth()
) .wrapContentHeight(Alignment.Bottom)
)
} else {
CantSendMessageBanner()
}
} }
} }
@ -281,6 +290,28 @@ fun MessagesViewTopBar(
) )
} }
@Composable
fun CantSendMessageBanner(
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.secondary)
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Text(
text = stringResource(id = R.string.screen_room_no_permission_to_post),
color = MaterialTheme.colorScheme.onSecondary,
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
fontStyle = FontStyle.Italic,
)
}
}
@Preview @Preview
@Composable @Composable
internal fun MessagesViewLightPreview(@PreviewParameter(MessagesStateProvider::class) state: MessagesState) = internal fun MessagesViewLightPreview(@PreviewParameter(MessagesStateProvider::class) state: MessagesState) =

View file

@ -46,6 +46,7 @@ import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
@ -166,7 +167,7 @@ class MessagesPresenterTest {
content = TimelineItemImageContent( content = TimelineItemImageContent(
body = "image.jpg", body = "image.jpg",
mediaSource = MediaSource(AN_AVATAR_URL), mediaSource = MediaSource(AN_AVATAR_URL),
thumbnailSource = null, thumbnailSource = null,
mimeType = MimeTypes.Jpeg, mimeType = MimeTypes.Jpeg,
blurhash = null, blurhash = null,
width = 20, width = 20,
@ -318,6 +319,32 @@ class MessagesPresenterTest {
} }
} }
@Test
fun `present - permission to post`() = runTest {
val matrixRoom = FakeMatrixRoom()
matrixRoom.givenCanSendEventResult(MessageEventType.ROOM_MESSAGE, Result.success(true))
val presenter = createMessagePresenter(matrixRoom = matrixRoom)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
skipItems(1)
assertThat(awaitItem().userHasPermissionToSendMessage).isTrue()
}
}
@Test
fun `present - no permission to post`() = runTest {
val matrixRoom = FakeMatrixRoom()
matrixRoom.givenCanSendEventResult(MessageEventType.ROOM_MESSAGE, Result.success(false))
val presenter = createMessagePresenter(matrixRoom = matrixRoom)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
skipItems(1)
assertThat(awaitItem().userHasPermissionToSendMessage).isFalse()
}
}
private fun TestScope.createMessagePresenter( private fun TestScope.createMessagePresenter(
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(), coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
matrixRoom: MatrixRoom = FakeMatrixRoom() matrixRoom: MatrixRoom = FakeMatrixRoom()

View file

@ -21,7 +21,7 @@ media3 = "1.0.2"
browser = "1.5.0" browser = "1.5.0"
# Compose # Compose
compose_bom = "2023.06.00" compose_bom = "2023.06.01"
composecompiler = "1.4.7" composecompiler = "1.4.7"
# Coroutines # Coroutines
@ -124,7 +124,7 @@ test_mockk = "io.mockk:mockk:1.13.5"
test_barista = "com.adevinta.android:barista:4.3.0" test_barista = "com.adevinta.android:barista:4.3.0"
test_hamcrest = "org.hamcrest:hamcrest:2.2" test_hamcrest = "org.hamcrest:hamcrest:2.2"
test_orchestrator = "androidx.test:orchestrator:1.4.2" test_orchestrator = "androidx.test:orchestrator:1.4.2"
test_turbine = "app.cash.turbine:turbine:0.13.0" test_turbine = "app.cash.turbine:turbine:1.0.0"
test_truth = "com.google.truth:truth:1.1.5" test_truth = "com.google.truth:truth:1.1.5"
test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.12" test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.12"
test_robolectric = "org.robolectric:robolectric:4.10.3" test_robolectric = "org.robolectric:robolectric:4.10.3"
@ -142,7 +142,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" } molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" }
timber = "com.jakewharton.timber:timber:5.0.1" timber = "com.jakewharton.timber:timber:5.0.1"
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.21" matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.22"
sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" } sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" }
sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" } sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" }
sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" } sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
@ -190,7 +190,7 @@ kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
anvil = { id = "com.squareup.anvil", version.ref = "anvil" } anvil = { id = "com.squareup.anvil", version.ref = "anvil" }
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
ktlint = "org.jlleitschuh.gradle.ktlint:11.4.0" ktlint = "org.jlleitschuh.gradle.ktlint:11.4.1"
dependencygraph = { id = "com.savvasdalkitsis.module-dependency-graph", version.ref = "dependencygraph" } dependencygraph = { id = "com.savvasdalkitsis.module-dependency-graph", version.ref = "dependencygraph" }
dependencycheck = { id = "org.owasp.dependencycheck", version.ref = "dependencycheck" } dependencycheck = { id = "org.owasp.dependencycheck", version.ref = "dependencycheck" }
dependencyanalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyanalysis" } dependencyanalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyanalysis" }

View file

@ -35,6 +35,19 @@ fun File.safeDelete() {
) )
} }
fun File.safeRenameTo(dest: File) {
tryOrNull(
onError = {
Timber.e(it, "Error, unable to rename file $path to ${dest.path}")
},
operation = {
if (renameTo(dest).not()) {
Timber.w("Warning, unable to rename file $path to ${dest.path}")
}
}
)
}
fun Context.createTmpFile(baseDir: File = cacheDir, extension: String? = null): File { fun Context.createTmpFile(baseDir: File = cacheDir, extension: String? = null): File {
val suffix = extension?.let { ".$extension" } val suffix = extension?.let { ".$extension" }
return File.createTempFile(UUID.randomUUID().toString(), suffix, baseDir).apply { mkdirs() } return File.createTempFile(UUID.randomUUID().toString(), suffix, baseDir).apply { mkdirs() }

View file

@ -16,6 +16,7 @@
package io.element.android.libraries.matrix.api package io.element.android.libraries.matrix.api
import io.element.android.libraries.matrix.api.core.ProgressCallback
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.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
@ -52,7 +53,7 @@ interface MatrixClient : Closeable {
suspend fun logout() suspend fun logout()
suspend fun loadUserDisplayName(): Result<String> suspend fun loadUserDisplayName(): Result<String>
suspend fun loadUserAvatarURLString(): Result<String?> suspend fun loadUserAvatarURLString(): Result<String?>
suspend fun uploadMedia(mimeType: String, data: ByteArray): Result<String> suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result<String>
fun onSlidingSyncUpdate() fun onSlidingSyncUpdate()
fun roomMembershipObserver(): RoomMembershipObserver fun roomMembershipObserver(): RoomMembershipObserver
} }

View file

@ -0,0 +1,21 @@
/*
* 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.matrix.api.core
interface ProgressCallback {
fun onProgress(current: Long, total: Long)
}

View file

@ -17,6 +17,7 @@
package io.element.android.libraries.matrix.api.room package io.element.android.libraries.matrix.api.room
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.ProgressCallback
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.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
@ -75,13 +76,13 @@ interface MatrixRoom : Closeable {
suspend fun redactEvent(eventId: EventId, reason: String? = null): Result<Unit> suspend fun redactEvent(eventId: EventId, reason: String? = null): Result<Unit>
suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo): Result<Unit> suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo, progressCallback: ProgressCallback?): Result<Unit>
suspend fun sendVideo(file: File, thumbnailFile: File, videoInfo: VideoInfo): Result<Unit> suspend fun sendVideo(file: File, thumbnailFile: File, videoInfo: VideoInfo, progressCallback: ProgressCallback?): Result<Unit>
suspend fun sendAudio(file: File, audioInfo: AudioInfo): Result<Unit> suspend fun sendAudio(file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?): Result<Unit>
suspend fun sendFile(file: File, fileInfo: FileInfo): Result<Unit> suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result<Unit>
suspend fun sendReaction(emoji: String, eventId: EventId): Result<Unit> suspend fun sendReaction(emoji: String, eventId: EventId): Result<Unit>
@ -101,6 +102,8 @@ interface MatrixRoom : Closeable {
suspend fun canSendStateEvent(type: StateEventType): Result<Boolean> suspend fun canSendStateEvent(type: StateEventType): Result<Boolean>
suspend fun canSendEvent(type: MessageEventType): Result<Boolean>
suspend fun updateAvatar(mimeType: String, data: ByteArray): Result<Unit> suspend fun updateAvatar(mimeType: String, data: ByteArray): Result<Unit>
suspend fun removeAvatar(): Result<Unit> suspend fun removeAvatar(): Result<Unit>

View file

@ -0,0 +1,36 @@
/*
* 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.matrix.api.room
enum class MessageEventType {
CALL_ANSWER,
CALL_INVITE,
CALL_HANGUP,
CALL_CANDIDATES,
KEY_VERIFICATION_READY,
KEY_VERIFICATION_START,
KEY_VERIFICATION_CANCEL,
KEY_VERIFICATION_ACCEPT,
KEY_VERIFICATION_KEY,
KEY_VERIFICATION_MAC,
KEY_VERIFICATION_DONE,
REACTION_SENT,
ROOM_ENCRYPTED,
ROOM_MESSAGE,
ROOM_REDACTION,
STICKER
}

View file

@ -21,6 +21,7 @@ package io.element.android.libraries.matrix.impl
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.coroutine.childScopeOf import io.element.android.libraries.core.coroutine.childScopeOf
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.ProgressCallback
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.createroom.CreateRoomParameters import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
@ -35,6 +36,7 @@ import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.impl.core.toProgressWatcher
import io.element.android.libraries.matrix.impl.media.RustMediaLoader import io.element.android.libraries.matrix.impl.media.RustMediaLoader
import io.element.android.libraries.matrix.impl.notification.RustNotificationService import io.element.android.libraries.matrix.impl.notification.RustNotificationService
import io.element.android.libraries.matrix.impl.pushers.RustPushersService import io.element.android.libraries.matrix.impl.pushers.RustPushersService
@ -56,7 +58,6 @@ import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.ClientDelegate import org.matrix.rustcomponents.sdk.ClientDelegate
import org.matrix.rustcomponents.sdk.TaskHandle
import org.matrix.rustcomponents.sdk.use import org.matrix.rustcomponents.sdk.use
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
@ -118,8 +119,6 @@ class RustMatrixClient constructor(
override val mediaLoader: MatrixMediaLoader override val mediaLoader: MatrixMediaLoader
get() = rustMediaLoader get() = rustMediaLoader
private var slidingSyncObserverToken: TaskHandle? = null
private val isSyncing = AtomicBoolean(false) private val isSyncing = AtomicBoolean(false)
private val roomMembershipObserver = RoomMembershipObserver() private val roomMembershipObserver = RoomMembershipObserver()
@ -225,13 +224,13 @@ class RustMatrixClient constructor(
override fun startSync() { override fun startSync() {
if (isSyncing.compareAndSet(false, true)) { if (isSyncing.compareAndSet(false, true)) {
slidingSyncObserverToken = roomList.sync() roomList.sync()
} }
} }
override fun stopSync() { override fun stopSync() {
if (isSyncing.compareAndSet(true, false)) { if (isSyncing.compareAndSet(true, false)) {
slidingSyncObserverToken?.use { it.cancel() } roomList.stopSync()
} }
} }
@ -268,9 +267,9 @@ class RustMatrixClient constructor(
} }
@OptIn(ExperimentalUnsignedTypes::class) @OptIn(ExperimentalUnsignedTypes::class)
override suspend fun uploadMedia(mimeType: String, data: ByteArray): Result<String> = withContext(dispatchers.io) { override suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result<String> = withContext(dispatchers.io) {
runCatching { runCatching {
client.uploadMedia(mimeType, data.toUByteArray().toList()) client.uploadMedia(mimeType, data.toUByteArray().toList(), progressCallback?.toProgressWatcher())
} }
} }

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.matrix.impl.core
import io.element.android.libraries.matrix.api.core.ProgressCallback
import org.matrix.rustcomponents.sdk.ProgressWatcher
import org.matrix.rustcomponents.sdk.TransmissionProgress
internal class ProgressWatcherWrapper(private val progressCallback: ProgressCallback) : ProgressWatcher {
override fun transmissionProgress(progress: TransmissionProgress) {
progressCallback.onProgress(progress.current.toLong(), progress.total.toLong())
}
}
internal fun ProgressCallback.toProgressWatcher(): ProgressWatcher {
return ProgressWatcherWrapper(this)
}

View file

@ -16,8 +16,6 @@
package io.element.android.libraries.matrix.impl.notification package io.element.android.libraries.matrix.impl.notification
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.NotificationEvent import io.element.android.libraries.matrix.api.notification.NotificationEvent
import org.matrix.rustcomponents.sdk.MessageLikeEventContent import org.matrix.rustcomponents.sdk.MessageLikeEventContent
import org.matrix.rustcomponents.sdk.MessageType import org.matrix.rustcomponents.sdk.MessageType
@ -105,5 +103,6 @@ private fun MessageType.toContent(): String {
is MessageType.Notice -> content.body is MessageType.Notice -> content.body
is MessageType.Text -> content.body is MessageType.Text -> content.body
is MessageType.Video -> content.use { it.body } is MessageType.Video -> content.use { it.body }
is MessageType.Location -> content.body
} }
} }

View file

@ -0,0 +1,58 @@
/*
* 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.matrix.impl.room
import io.element.android.libraries.matrix.api.room.MessageEventType
import org.matrix.rustcomponents.sdk.MessageLikeEventType
fun MessageEventType.map(): MessageLikeEventType = when (this) {
MessageEventType.CALL_ANSWER -> MessageLikeEventType.CALL_ANSWER
MessageEventType.CALL_INVITE -> MessageLikeEventType.CALL_INVITE
MessageEventType.CALL_HANGUP -> MessageLikeEventType.CALL_HANGUP
MessageEventType.CALL_CANDIDATES -> MessageLikeEventType.CALL_CANDIDATES
MessageEventType.KEY_VERIFICATION_READY -> MessageLikeEventType.KEY_VERIFICATION_READY
MessageEventType.KEY_VERIFICATION_START -> MessageLikeEventType.KEY_VERIFICATION_START
MessageEventType.KEY_VERIFICATION_CANCEL -> MessageLikeEventType.KEY_VERIFICATION_CANCEL
MessageEventType.KEY_VERIFICATION_ACCEPT -> MessageLikeEventType.KEY_VERIFICATION_ACCEPT
MessageEventType.KEY_VERIFICATION_KEY -> MessageLikeEventType.KEY_VERIFICATION_KEY
MessageEventType.KEY_VERIFICATION_MAC -> MessageLikeEventType.KEY_VERIFICATION_MAC
MessageEventType.KEY_VERIFICATION_DONE -> MessageLikeEventType.KEY_VERIFICATION_DONE
MessageEventType.REACTION_SENT -> MessageLikeEventType.REACTION_SENT
MessageEventType.ROOM_ENCRYPTED -> MessageLikeEventType.ROOM_ENCRYPTED
MessageEventType.ROOM_MESSAGE -> MessageLikeEventType.ROOM_MESSAGE
MessageEventType.ROOM_REDACTION -> MessageLikeEventType.ROOM_REDACTION
MessageEventType.STICKER -> MessageLikeEventType.STICKER
}
fun MessageLikeEventType.map(): MessageEventType = when (this) {
MessageLikeEventType.CALL_ANSWER -> MessageEventType.CALL_ANSWER
MessageLikeEventType.CALL_INVITE -> MessageEventType.CALL_INVITE
MessageLikeEventType.CALL_HANGUP -> MessageEventType.CALL_HANGUP
MessageLikeEventType.CALL_CANDIDATES -> MessageEventType.CALL_CANDIDATES
MessageLikeEventType.KEY_VERIFICATION_READY -> MessageEventType.KEY_VERIFICATION_READY
MessageLikeEventType.KEY_VERIFICATION_START -> MessageEventType.KEY_VERIFICATION_START
MessageLikeEventType.KEY_VERIFICATION_CANCEL -> MessageEventType.KEY_VERIFICATION_CANCEL
MessageLikeEventType.KEY_VERIFICATION_ACCEPT -> MessageEventType.KEY_VERIFICATION_ACCEPT
MessageLikeEventType.KEY_VERIFICATION_KEY -> MessageEventType.KEY_VERIFICATION_KEY
MessageLikeEventType.KEY_VERIFICATION_MAC -> MessageEventType.KEY_VERIFICATION_MAC
MessageLikeEventType.KEY_VERIFICATION_DONE -> MessageEventType.KEY_VERIFICATION_DONE
MessageLikeEventType.REACTION_SENT -> MessageEventType.REACTION_SENT
MessageLikeEventType.ROOM_ENCRYPTED -> MessageEventType.ROOM_ENCRYPTED
MessageLikeEventType.ROOM_MESSAGE -> MessageEventType.ROOM_MESSAGE
MessageLikeEventType.ROOM_REDACTION -> MessageEventType.ROOM_REDACTION
MessageLikeEventType.STICKER -> MessageEventType.STICKER
}

View file

@ -19,6 +19,7 @@ package io.element.android.libraries.matrix.impl.room
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.coroutine.childScopeOf import io.element.android.libraries.core.coroutine.childScopeOf
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.ProgressCallback
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.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
@ -28,10 +29,12 @@ import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.api.room.roomMembers
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.timeline.item.event.EventType import io.element.android.libraries.matrix.api.timeline.item.event.EventType
import io.element.android.libraries.matrix.impl.core.toProgressWatcher
import io.element.android.libraries.matrix.impl.media.map import io.element.android.libraries.matrix.impl.media.map
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
import io.element.android.libraries.matrix.impl.timeline.timelineDiffFlow import io.element.android.libraries.matrix.impl.timeline.timelineDiffFlow
@ -264,27 +267,37 @@ class RustMatrixRoom(
} }
} }
override suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo): Result<Unit> = withContext(coroutineDispatchers.io) { override suspend fun canSendEvent(type: MessageEventType): Result<Boolean> = withContext(coroutineDispatchers.io) {
runCatching { runCatching {
innerRoom.sendImage(file.path, thumbnailFile.path, imageInfo.map()) innerRoom.member(sessionId.value).use { it.canSendMessage(type.map()) }
} }
} }
override suspend fun sendVideo(file: File, thumbnailFile: File, videoInfo: VideoInfo): Result<Unit> = withContext(coroutineDispatchers.io) { override suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo, progressCallback: ProgressCallback?): Result<Unit> = withContext(
coroutineDispatchers.io
) {
runCatching { runCatching {
innerRoom.sendVideo(file.path, thumbnailFile.path, videoInfo.map()) innerRoom.sendImage(file.path, thumbnailFile.path, imageInfo.map(), progressCallback?.toProgressWatcher())
} }
} }
override suspend fun sendAudio(file: File, audioInfo: AudioInfo): Result<Unit> = withContext(coroutineDispatchers.io) { override suspend fun sendVideo(file: File, thumbnailFile: File, videoInfo: VideoInfo, progressCallback: ProgressCallback?): Result<Unit> = withContext(
coroutineDispatchers.io
) {
runCatching { runCatching {
innerRoom.sendAudio(file.path, audioInfo.map()) innerRoom.sendVideo(file.path, thumbnailFile.path, videoInfo.map(), progressCallback?.toProgressWatcher())
} }
} }
override suspend fun sendFile(file: File, fileInfo: FileInfo): Result<Unit> = withContext(coroutineDispatchers.io) { override suspend fun sendAudio(file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?): Result<Unit> = withContext(coroutineDispatchers.io) {
runCatching { runCatching {
innerRoom.sendFile(file.path, fileInfo.map()) innerRoom.sendAudio(file.path, audioInfo.map(), progressCallback?.toProgressWatcher())
}
}
override suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result<Unit> = withContext(coroutineDispatchers.io) {
runCatching {
innerRoom.sendFile(file.path, fileInfo.map(), progressCallback?.toProgressWatcher())
} }
} }

View file

@ -65,9 +65,11 @@ class EventMessageMapper {
is MessageType.Video -> { is MessageType.Video -> {
VideoMessageType(type.content.body, type.content.source.map(), type.content.info?.map()) VideoMessageType(type.content.body, type.content.source.map(), type.content.info?.map())
} }
is MessageType.Location,
null -> { null -> {
UnknownMessageType UnknownMessageType
} }
} }
} }
val inReplyToId = it.inReplyTo()?.eventId?.let(::EventId) val inReplyToId = it.inReplyTo()?.eventId?.let(::EventId)

View file

@ -17,6 +17,7 @@
package io.element.android.libraries.matrix.test package io.element.android.libraries.matrix.test
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.ProgressCallback
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.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
@ -116,7 +117,11 @@ class FakeMatrixClient(
return userAvatarURLString return userAvatarURLString
} }
override suspend fun uploadMedia(mimeType: String, data: ByteArray): Result<String> { override suspend fun uploadMedia(
mimeType: String,
data: ByteArray,
progressCallback: ProgressCallback?
): Result<String> {
return uploadMediaResult return uploadMediaResult
} }

View file

@ -17,6 +17,7 @@
package io.element.android.libraries.matrix.test.room package io.element.android.libraries.matrix.test.room
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.ProgressCallback
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.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
@ -26,6 +27,7 @@ import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
@ -64,6 +66,7 @@ class FakeMatrixRoom(
private var inviteUserResult = Result.success(Unit) private var inviteUserResult = Result.success(Unit)
private var canInviteResult = Result.success(true) private var canInviteResult = Result.success(true)
private val canSendStateResults = mutableMapOf<StateEventType, Result<Boolean>>() private val canSendStateResults = mutableMapOf<StateEventType, Result<Boolean>>()
private val canSendEventResults = mutableMapOf<MessageEventType, Result<Boolean>>()
private var sendMediaResult = Result.success(Unit) private var sendMediaResult = Result.success(Unit)
private var setNameResult = Result.success(Unit) private var setNameResult = Result.success(Unit)
private var setTopicResult = Result.success(Unit) private var setTopicResult = Result.success(Unit)
@ -198,13 +201,22 @@ class FakeMatrixRoom(
return canSendStateResults[type] ?: Result.failure(IllegalStateException("No fake answer")) return canSendStateResults[type] ?: Result.failure(IllegalStateException("No fake answer"))
} }
override suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo): Result<Unit> = fakeSendMedia() override suspend fun canSendEvent(type: MessageEventType): Result<Boolean> {
return canSendEventResults[type] ?: Result.failure(IllegalStateException("No fake answer"))
}
override suspend fun sendVideo(file: File, thumbnailFile: File, videoInfo: VideoInfo): Result<Unit> = fakeSendMedia() override suspend fun sendImage(
file: File,
thumbnailFile: File,
imageInfo: ImageInfo,
progressCallback: ProgressCallback?
): Result<Unit> = fakeSendMedia()
override suspend fun sendAudio(file: File, audioInfo: AudioInfo): Result<Unit> = fakeSendMedia() override suspend fun sendVideo(file: File, thumbnailFile: File, videoInfo: VideoInfo, progressCallback: ProgressCallback?): Result<Unit> = fakeSendMedia()
override suspend fun sendFile(file: File, fileInfo: FileInfo): Result<Unit> = fakeSendMedia() override suspend fun sendAudio(file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?): Result<Unit> = fakeSendMedia()
override suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result<Unit> = fakeSendMedia()
private suspend fun fakeSendMedia(): Result<Unit> = simulateLongTask { private suspend fun fakeSendMedia(): Result<Unit> = simulateLongTask {
sendMediaResult.onSuccess { sendMediaResult.onSuccess {
@ -274,6 +286,10 @@ class FakeMatrixRoom(
canSendStateResults[type] = result canSendStateResults[type] = result
} }
fun givenCanSendEventResult(type: MessageEventType, result: Result<Boolean>) {
canSendEventResults[type] = result
}
fun givenIgnoreResult(result: Result<Unit>) { fun givenIgnoreResult(result: Result<Unit>) {
ignoreResult = result ignoreResult = result
} }

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.matrix.ui.room
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.produceState
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MessageEventType
@Composable
fun MatrixRoom.canSendEventAsState(type: MessageEventType, updateKey: Long): State<Boolean> {
return produceState(initialValue = true, key1 = updateKey) {
value = canSendEvent(type).getOrElse { true }
}
}

View file

@ -47,7 +47,8 @@ class MediaSender @Inject constructor(
sendImage( sendImage(
file = info.file, file = info.file,
thumbnailFile = info.thumbnailFile, thumbnailFile = info.thumbnailFile,
imageInfo = info.info imageInfo = info.info,
progressCallback = null
) )
} }
@ -55,14 +56,16 @@ class MediaSender @Inject constructor(
sendVideo( sendVideo(
file = info.file, file = info.file,
thumbnailFile = info.thumbnailFile, thumbnailFile = info.thumbnailFile,
videoInfo = info.info videoInfo = info.info,
progressCallback = null
) )
} }
is MediaUploadInfo.AnyFile -> { is MediaUploadInfo.AnyFile -> {
sendFile( sendFile(
file = info.file, file = info.file,
fileInfo = info.info fileInfo = info.info,
progressCallback = null
) )
} }
else -> Result.failure(IllegalStateException("Unexpected MediaUploadInfo format: $info")) else -> Result.failure(IllegalStateException("Unexpected MediaUploadInfo format: $info"))

View file

@ -24,6 +24,7 @@ import androidx.exifinterface.media.ExifInterface
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.androidutils.file.createTmpFile import io.element.android.libraries.androidutils.file.createTmpFile
import io.element.android.libraries.androidutils.file.getFileName import io.element.android.libraries.androidutils.file.getFileName
import io.element.android.libraries.androidutils.file.safeRenameTo
import io.element.android.libraries.androidutils.media.runAndRelease import io.element.android.libraries.androidutils.media.runAndRelease
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.core.data.tryOrNull
@ -105,7 +106,7 @@ class AndroidMediaPreProcessor @Inject constructor(
private fun MediaUploadInfo.postProcess(uri: Uri): MediaUploadInfo { private fun MediaUploadInfo.postProcess(uri: Uri): MediaUploadInfo {
val name = context.getFileName(uri) ?: return this val name = context.getFileName(uri) ?: return this
val renamedFile = File(context.cacheDir, name).also { val renamedFile = File(context.cacheDir, name).also {
file.renameTo(it) file.safeRenameTo(it)
} }
return when (this) { return when (this) {
is MediaUploadInfo.AnyFile -> copy(file = renamedFile) is MediaUploadInfo.AnyFile -> copy(file = renamedFile)

View file

@ -21,6 +21,7 @@ import android.net.Uri
import com.otaliastudios.transcoder.Transcoder import com.otaliastudios.transcoder.Transcoder
import com.otaliastudios.transcoder.TranscoderListener import com.otaliastudios.transcoder.TranscoderListener
import io.element.android.libraries.androidutils.file.createTmpFile import io.element.android.libraries.androidutils.file.createTmpFile
import io.element.android.libraries.androidutils.file.safeDelete
import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.ApplicationContext
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
@ -46,12 +47,12 @@ class VideoCompressor @Inject constructor(
} }
override fun onTranscodeCanceled() { override fun onTranscodeCanceled() {
tmpFile.delete() tmpFile.safeDelete()
close() close()
} }
override fun onTranscodeFailed(exception: Throwable) { override fun onTranscodeFailed(exception: Throwable) {
tmpFile.delete() tmpFile.safeDelete()
close(exception) close(exception)
} }
}) })

View file

@ -18,6 +18,7 @@ package io.element.android.libraries.push.impl.notifications
import android.content.Context import android.content.Context
import io.element.android.libraries.androidutils.file.EncryptedFileFactory import io.element.android.libraries.androidutils.file.EncryptedFileFactory
import io.element.android.libraries.androidutils.file.safeDelete
import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.ApplicationContext
@ -70,7 +71,7 @@ class NotificationEventPersistence @Inject constructor(
fun persistEvents(queuedEvents: NotificationEventQueue) { fun persistEvents(queuedEvents: NotificationEventQueue) {
Timber.tag(loggerTag.value).d("Serializing ${queuedEvents.rawEvents().size} NotifiableEvent(s)") Timber.tag(loggerTag.value).d("Serializing ${queuedEvents.rawEvents().size} NotifiableEvent(s)")
// Always delete file before writing, or encryptedFile.openFileOutput() will throw // Always delete file before writing, or encryptedFile.openFileOutput() will throw
file.delete() file.safeDelete()
if (queuedEvents.isEmpty()) return if (queuedEvents.isEmpty()) return
try { try {
encryptedFile.openFileOutput().use { fos -> encryptedFile.openFileOutput().use { fos ->

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f145f138983fce06084866f28e14e6babe6b242ea40f89a7544a8c2580020249
size 49121

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:eb2aa0a032dcd1597fd2e7971f47a9025f41f4fb3bf29fac709dd6d33a0a2cf8
size 48551

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:c1c1eedbab868e0c2501220293608572850e052f685f7076ec939b3f1a9abf27 oid sha256:4aee0816507bc71c8649aae95e57c3b75fb5e7fbd3888531817f659e1153f5be
size 4464 size 7971

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b oid sha256:dac298843eacb59d4666d50712e5cdbe82620a25c5c01d23b77f4dcba9df9c43
size 4457 size 7979