Merge remote-tracking branch 'origin/develop' into
feature/fre/create_room_select_avatar
This commit is contained in:
commit
afc32f4810
149 changed files with 1030 additions and 600 deletions
|
|
@ -37,6 +37,7 @@ fun File.safeDelete() {
|
|||
)
|
||||
}
|
||||
|
||||
suspend fun Context.createTmpFile(baseDir: File = cacheDir): File = withContext(Dispatchers.IO) {
|
||||
File.createTempFile(UUID.randomUUID().toString(), null, baseDir).apply { mkdirs() }
|
||||
suspend fun Context.createTmpFile(baseDir: File = cacheDir, extension: String? = null): File = withContext(Dispatchers.IO) {
|
||||
val suffix = extension?.let { ".$extension" }
|
||||
File.createTempFile(UUID.randomUUID().toString(), suffix, baseDir).apply { mkdirs() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ object MimeTypes {
|
|||
const val Gif = "image/gif"
|
||||
|
||||
const val Videos = "video/*"
|
||||
const val Mp4 = "video/mp4"
|
||||
|
||||
const val Audio = "audio/*"
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold
|
|||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PreferenceView(
|
||||
title: String,
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
package io.element.android.libraries.designsystem.theme.components
|
||||
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
package io.element.android.libraries.designsystem.theme.components
|
||||
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
|
|
@ -57,6 +55,7 @@ fun MediumTopAppBar(
|
|||
internal fun MediumTopAppBarPreview() =
|
||||
ElementThemedPreview { ContentToPreview() }
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
MediumTopAppBar(title = { Text(text = "Title") })
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalMaterialApi::class, ExperimentalMaterialApi::class)
|
||||
|
||||
package io.element.android.libraries.designsystem.theme.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
|
|
@ -107,6 +105,7 @@ internal fun ModalBottomSheetLayoutLightPreview() =
|
|||
internal fun ModalBottomSheetLayoutDarkPreview() =
|
||||
ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
ModalBottomSheetLayout(
|
||||
|
|
|
|||
|
|
@ -23,10 +23,9 @@ import androidx.compose.foundation.layout.height
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.TextFieldColors
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
|
|
@ -49,7 +48,6 @@ import io.element.android.libraries.designsystem.preview.PreviewGroup
|
|||
import io.element.android.libraries.designsystem.utils.allBooleans
|
||||
import io.element.android.libraries.designsystem.utils.asInt
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun OutlinedTextField(
|
||||
value: String,
|
||||
|
|
@ -70,8 +68,8 @@ fun OutlinedTextField(
|
|||
singleLine: Boolean = false,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
shape: Shape = TextFieldDefaults.outlinedShape,
|
||||
colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors()
|
||||
shape: Shape = OutlinedTextFieldDefaults.shape,
|
||||
colors: TextFieldColors = OutlinedTextFieldDefaults.colors()
|
||||
) {
|
||||
androidx.compose.material3.OutlinedTextField(
|
||||
value = value,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ package io.element.android.libraries.designsystem.theme.components
|
|||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FabPosition
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ScaffoldDefaults
|
||||
|
|
@ -27,7 +26,6 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun Scaffold(
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
package io.element.android.libraries.designsystem.theme.components
|
||||
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
|
|
@ -77,6 +75,7 @@ fun SearchBar(
|
|||
@Composable
|
||||
internal fun DockedSearchBarPreview() = ElementThemedPreview { ContentToPreview() }
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
SearchBar(
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import androidx.compose.foundation.layout.height
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.TextFieldColors
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
|
|
@ -50,7 +49,6 @@ import io.element.android.libraries.designsystem.preview.PreviewGroup
|
|||
import io.element.android.libraries.designsystem.utils.allBooleans
|
||||
import io.element.android.libraries.designsystem.utils.asInt
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TextField(
|
||||
value: String,
|
||||
|
|
@ -71,8 +69,8 @@ fun TextField(
|
|||
singleLine: Boolean = false,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
shape: Shape = TextFieldDefaults.filledShape,
|
||||
colors: TextFieldColors = TextFieldDefaults.textFieldColors()
|
||||
shape: Shape = TextFieldDefaults.shape,
|
||||
colors: TextFieldColors = TextFieldDefaults.colors()
|
||||
) {
|
||||
androidx.compose.material3.TextField(
|
||||
value = value,
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
package io.element.android.libraries.designsystem.theme.components
|
||||
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
|
|
|
|||
|
|
@ -18,5 +18,6 @@ package io.element.android.libraries.matrix.api.media
|
|||
|
||||
data class AudioInfo(
|
||||
val duration: Long?,
|
||||
val size: Long?
|
||||
val size: Long?,
|
||||
val mimeType: String?,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -20,10 +20,15 @@ import io.element.android.libraries.matrix.api.core.EventId
|
|||
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.UserId
|
||||
import io.element.android.libraries.matrix.api.media.AudioInfo
|
||||
import io.element.android.libraries.matrix.api.media.FileInfo
|
||||
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.timeline.MatrixTimeline
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
|
||||
interface MatrixRoom : Closeable {
|
||||
val sessionId: SessionId
|
||||
|
|
@ -67,6 +72,14 @@ interface MatrixRoom : Closeable {
|
|||
|
||||
suspend fun redactEvent(eventId: EventId, reason: String? = null): Result<Unit>
|
||||
|
||||
suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo): Result<Unit>
|
||||
|
||||
suspend fun sendVideo(file: File, thumbnailFile: File, videoInfo: VideoInfo): Result<Unit>
|
||||
|
||||
suspend fun sendAudio(file: File, audioInfo: AudioInfo): Result<Unit>
|
||||
|
||||
suspend fun sendFile(file: File, fileInfo: FileInfo): Result<Unit>
|
||||
|
||||
suspend fun leave(): Result<Unit>
|
||||
|
||||
suspend fun acceptInvitation(): Result<Unit>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
package io.element.android.libraries.matrix.api.room
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
|
||||
data class RoomMember(
|
||||
val userId: UserId,
|
||||
|
|
@ -30,12 +29,6 @@ data class RoomMember(
|
|||
val isIgnored: Boolean,
|
||||
)
|
||||
|
||||
fun RoomMember.toMatrixUser() = MatrixUser(
|
||||
userId = userId,
|
||||
displayName = displayName,
|
||||
avatarUrl = avatarUrl,
|
||||
)
|
||||
|
||||
enum class RoomMembershipState {
|
||||
BAN, INVITE, JOIN, KNOCK, LEAVE
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ import io.element.android.libraries.matrix.impl.verification.RustSessionVerifica
|
|||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
|
|
@ -307,7 +306,6 @@ class RustMatrixClient constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
override fun close() {
|
||||
slidingSyncUpdateJob?.cancel()
|
||||
stopSync()
|
||||
|
|
|
|||
|
|
@ -21,5 +21,12 @@ import org.matrix.rustcomponents.sdk.AudioInfo as RustAudioInfo
|
|||
|
||||
fun RustAudioInfo.map(): AudioInfo = AudioInfo(
|
||||
duration = duration?.toLong(),
|
||||
size = size?.toLong()
|
||||
size = size?.toLong(),
|
||||
mimeType = mimetype
|
||||
)
|
||||
|
||||
fun AudioInfo.map(): RustAudioInfo = RustAudioInfo(
|
||||
duration = duration?.toULong(),
|
||||
size = size?.toULong(),
|
||||
mimetype = mimeType,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -27,3 +27,10 @@ fun RustFileInfo.map(): FileInfo = FileInfo(
|
|||
thumbnailInfo = thumbnailInfo?.map(),
|
||||
thumbnailUrl = thumbnailSource?.useUrl()
|
||||
)
|
||||
|
||||
fun FileInfo.map(): RustFileInfo = RustFileInfo(
|
||||
mimetype = mimetype,
|
||||
size = size?.toULong(),
|
||||
thumbnailInfo = thumbnailInfo?.map(),
|
||||
thumbnailSource = null
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package io.element.android.libraries.matrix.impl.media
|
||||
|
||||
import io.element.android.libraries.matrix.api.media.ImageInfo
|
||||
import org.matrix.rustcomponents.sdk.MediaSource
|
||||
import org.matrix.rustcomponents.sdk.ImageInfo as RustImageInfo
|
||||
|
||||
fun RustImageInfo.map(): ImageInfo = ImageInfo(
|
||||
|
|
@ -28,3 +29,13 @@ fun RustImageInfo.map(): ImageInfo = ImageInfo(
|
|||
thumbnailUrl = thumbnailSource?.useUrl(),
|
||||
blurhash = blurhash
|
||||
)
|
||||
|
||||
fun ImageInfo.map(): RustImageInfo = RustImageInfo(
|
||||
height = height?.toULong(),
|
||||
width = width?.toULong(),
|
||||
mimetype = mimetype,
|
||||
size = size?.toULong(),
|
||||
thumbnailInfo = thumbnailInfo?.map(),
|
||||
thumbnailSource = null,
|
||||
blurhash = blurhash,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -25,3 +25,10 @@ fun RustThumbnailInfo.map(): ThumbnailInfo = ThumbnailInfo(
|
|||
mimetype = mimetype,
|
||||
size = size?.toLong()
|
||||
)
|
||||
|
||||
fun ThumbnailInfo.map(): RustThumbnailInfo = RustThumbnailInfo(
|
||||
height = height?.toULong(),
|
||||
width = width?.toULong(),
|
||||
mimetype = mimetype,
|
||||
size = size?.toULong()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -29,3 +29,14 @@ fun RustVideoInfo.map(): VideoInfo = VideoInfo(
|
|||
thumbnailUrl = thumbnailSource?.useUrl(),
|
||||
blurhash = blurhash
|
||||
)
|
||||
|
||||
fun VideoInfo.map(): RustVideoInfo = RustVideoInfo(
|
||||
duration = duration?.toULong(),
|
||||
height = height?.toULong(),
|
||||
width = width?.toULong(),
|
||||
mimetype = mimetype,
|
||||
size = size?.toULong(),
|
||||
thumbnailInfo = thumbnailInfo?.map(),
|
||||
thumbnailSource = null,
|
||||
blurhash = blurhash
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package io.element.android.libraries.matrix.impl.notification
|
||||
|
||||
import io.element.android.libraries.core.bool.orFalse
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
|
@ -36,7 +37,7 @@ class NotificationMapper @Inject constructor() {
|
|||
senderDisplayName = it.senderDisplayName,
|
||||
roomAvatarUrl = it.roomAvatarUrl,
|
||||
isDirect = it.isDirect,
|
||||
isEncrypted = it.isEncrypted,
|
||||
isEncrypted = it.isEncrypted.orFalse(),
|
||||
isNoisy = it.isNoisy
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,17 +24,18 @@ import org.matrix.rustcomponents.sdk.RoomMember as RustRoomMember
|
|||
|
||||
object RoomMemberMapper {
|
||||
|
||||
fun map(roomMember: RustRoomMember): RoomMember =
|
||||
fun map(roomMember: RustRoomMember): RoomMember = roomMember.use {
|
||||
RoomMember(
|
||||
UserId(roomMember.userId()),
|
||||
roomMember.displayName(),
|
||||
roomMember.avatarUrl(),
|
||||
mapMembership(roomMember.membership()),
|
||||
roomMember.isNameAmbiguous(),
|
||||
roomMember.powerLevel(),
|
||||
roomMember.normalizedPowerLevel(),
|
||||
roomMember.isIgnored(),
|
||||
UserId(it.userId()),
|
||||
it.displayName(),
|
||||
it.avatarUrl(),
|
||||
mapMembership(it.membership()),
|
||||
it.isNameAmbiguous(),
|
||||
it.powerLevel(),
|
||||
it.normalizedPowerLevel(),
|
||||
it.isIgnored(),
|
||||
)
|
||||
}
|
||||
|
||||
fun mapMembership(membershipState: RustMembershipState): RoomMembershipState =
|
||||
when (membershipState) {
|
||||
|
|
|
|||
|
|
@ -21,10 +21,15 @@ import io.element.android.libraries.matrix.api.core.EventId
|
|||
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.UserId
|
||||
import io.element.android.libraries.matrix.api.media.AudioInfo
|
||||
import io.element.android.libraries.matrix.api.media.FileInfo
|
||||
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.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||
import io.element.android.libraries.matrix.impl.media.map
|
||||
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
|
@ -39,6 +44,7 @@ import org.matrix.rustcomponents.sdk.SlidingSyncRoom
|
|||
import org.matrix.rustcomponents.sdk.UpdateSummary
|
||||
import org.matrix.rustcomponents.sdk.genTransactionId
|
||||
import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown
|
||||
import java.io.File
|
||||
|
||||
class RustMatrixRoom(
|
||||
override val sessionId: SessionId,
|
||||
|
|
@ -155,9 +161,10 @@ class RustMatrixRoom(
|
|||
|
||||
override suspend fun sendMessage(message: String): Result<Unit> = withContext(coroutineDispatchers.io) {
|
||||
val transactionId = genTransactionId()
|
||||
val content = messageEventContentFromMarkdown(message)
|
||||
runCatching {
|
||||
innerRoom.send(content, transactionId)
|
||||
messageEventContentFromMarkdown(message).use { content ->
|
||||
runCatching {
|
||||
innerRoom.send(content, transactionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -202,4 +209,28 @@ class RustMatrixRoom(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo): Result<Unit> {
|
||||
return runCatching {
|
||||
innerRoom.sendImage(file.path, thumbnailFile.path, imageInfo.map())
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun sendVideo(file: File, thumbnailFile: File, videoInfo: VideoInfo): Result<Unit> {
|
||||
return runCatching {
|
||||
innerRoom.sendVideo(file.path, thumbnailFile.path, videoInfo.map())
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun sendAudio(file: File, audioInfo: AudioInfo): Result<Unit> {
|
||||
return runCatching {
|
||||
innerRoom.sendAudio(file.path, audioInfo.map())
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun sendFile(file: File, fileInfo: FileInfo): Result<Unit> {
|
||||
return runCatching {
|
||||
innerRoom.sendFile(file.path, fileInfo.map())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,7 +91,6 @@ internal class RustRoomSummaryDataSource(
|
|||
coroutineScope.cancel()
|
||||
}
|
||||
|
||||
//@OptIn(FlowPreview::class)
|
||||
override fun roomSummaries(): StateFlow<List<RoomSummary>> {
|
||||
return roomSummaries
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,12 +27,12 @@ class MatrixTimelineItemMapper(
|
|||
) {
|
||||
|
||||
fun map(timelineItem: TimelineItem): MatrixTimelineItem = timelineItem.use {
|
||||
val asEvent = timelineItem.asEvent()
|
||||
val asEvent = it.asEvent()
|
||||
if (asEvent != null) {
|
||||
val eventTimelineItem = eventTimelineItemMapper.map(asEvent)
|
||||
return MatrixTimelineItem.Event(eventTimelineItem)
|
||||
}
|
||||
val asVirtual = timelineItem.asVirtual()
|
||||
val asVirtual = it.asVirtual()
|
||||
if (asVirtual != null) {
|
||||
val virtualTimelineItem = virtualTimelineItemMapper.map(asVirtual)
|
||||
return MatrixTimelineItem.Virtual(virtualTimelineItem)
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ import org.matrix.rustcomponents.sdk.MessageFormat as RustMessageFormat
|
|||
class EventMessageMapper {
|
||||
|
||||
fun map(message: Message): MessageContent = message.use {
|
||||
val type = message.msgtype().use { type ->
|
||||
val type = it.msgtype().use { type ->
|
||||
when (type) {
|
||||
is MessageType.Audio -> {
|
||||
AudioMessageType(type.content.body, type.content.source.useUrl(), type.content.info?.map())
|
||||
|
|
@ -68,9 +68,9 @@ class EventMessageMapper {
|
|||
}
|
||||
}
|
||||
MessageContent(
|
||||
body = message.body(),
|
||||
inReplyTo = message.inReplyTo()?.eventId?.let(::EventId),
|
||||
isEdited = message.isEdited(),
|
||||
body = it.body(),
|
||||
inReplyTo = it.inReplyTo()?.eventId?.let(::EventId),
|
||||
isEdited = it.isEdited(),
|
||||
type = type
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,18 +31,18 @@ class EventTimelineItemMapper(private val contentMapper: TimelineEventContentMap
|
|||
|
||||
fun map(eventTimelineItem: RustEventTimelineItem): EventTimelineItem = eventTimelineItem.use {
|
||||
EventTimelineItem(
|
||||
uniqueIdentifier = eventTimelineItem.uniqueIdentifier(),
|
||||
eventId = eventTimelineItem.eventId()?.let { EventId(it) },
|
||||
isEditable = eventTimelineItem.isEditable(),
|
||||
isLocal = eventTimelineItem.isLocal(),
|
||||
isOwn = eventTimelineItem.isOwn(),
|
||||
isRemote = eventTimelineItem.isRemote(),
|
||||
localSendState = eventTimelineItem.localSendState()?.map(),
|
||||
reactions = eventTimelineItem.reactions().map(),
|
||||
sender = UserId(eventTimelineItem.sender()),
|
||||
senderProfile = eventTimelineItem.senderProfile().map(),
|
||||
timestamp = eventTimelineItem.timestamp().toLong(),
|
||||
content = contentMapper.map(eventTimelineItem.content())
|
||||
uniqueIdentifier = it.uniqueIdentifier(),
|
||||
eventId = it.eventId()?.let { EventId(it) },
|
||||
isEditable = it.isEditable(),
|
||||
isLocal = it.isLocal(),
|
||||
isOwn = it.isOwn(),
|
||||
isRemote = it.isRemote(),
|
||||
localSendState = it.localSendState()?.map(),
|
||||
reactions = it.reactions().map(),
|
||||
sender = UserId(it.sender()),
|
||||
senderProfile = it.senderProfile().map(),
|
||||
timestamp = it.timestamp().toLong(),
|
||||
content = contentMapper.map(it.content())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package io.element.android.libraries.matrix.impl.timeline.item.event
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
|
||||
|
|
@ -26,7 +27,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.RedactedConte
|
|||
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent
|
||||
import io.element.android.libraries.matrix.impl.media.map
|
||||
|
|
@ -39,7 +39,7 @@ import org.matrix.rustcomponents.sdk.OtherState as RustOtherState
|
|||
class TimelineEventContentMapper(private val eventMessageMapper: EventMessageMapper = EventMessageMapper()) {
|
||||
|
||||
fun map(content: TimelineItemContent): EventContent = content.use {
|
||||
when (val kind = content.kind()) {
|
||||
when (val kind = it.kind()) {
|
||||
is TimelineItemContentKind.FailedToParseMessageLike -> {
|
||||
FailedToParseMessageLikeContent(
|
||||
eventType = kind.eventType,
|
||||
|
|
@ -54,7 +54,7 @@ class TimelineEventContentMapper(private val eventMessageMapper: EventMessageMap
|
|||
)
|
||||
}
|
||||
TimelineItemContentKind.Message -> {
|
||||
val message = content.asMessage()
|
||||
val message = it.asMessage()
|
||||
if (message == null) {
|
||||
UnknownContent
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,10 @@ import io.element.android.libraries.matrix.api.core.EventId
|
|||
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.UserId
|
||||
import io.element.android.libraries.matrix.api.media.AudioInfo
|
||||
import io.element.android.libraries.matrix.api.media.FileInfo
|
||||
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.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||
|
|
@ -30,6 +34,7 @@ import kotlinx.coroutines.delay
|
|||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import java.io.File
|
||||
|
||||
class FakeMatrixRoom(
|
||||
override val sessionId: SessionId = A_SESSION_ID,
|
||||
|
|
@ -54,6 +59,9 @@ class FakeMatrixRoom(
|
|||
private var updateMembersResult: Result<Unit> = Result.success(Unit)
|
||||
private var acceptInviteResult = Result.success(Unit)
|
||||
private var rejectInviteResult = Result.success(Unit)
|
||||
private var sendMediaResult = Result.success(Unit)
|
||||
var sendMediaCount = 0
|
||||
private set
|
||||
|
||||
var isInviteAccepted: Boolean = false
|
||||
private set
|
||||
|
|
@ -128,6 +136,14 @@ class FakeMatrixRoom(
|
|||
return rejectInviteResult
|
||||
}
|
||||
|
||||
override suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo): Result<Unit> = sendMediaResult.also { sendMediaCount++ }
|
||||
|
||||
override suspend fun sendVideo(file: File, thumbnailFile: File, videoInfo: VideoInfo): Result<Unit> = sendMediaResult.also { sendMediaCount++ }
|
||||
|
||||
override suspend fun sendAudio(file: File, audioInfo: AudioInfo): Result<Unit> = sendMediaResult.also { sendMediaCount++ }
|
||||
|
||||
override suspend fun sendFile(file: File, fileInfo: FileInfo): Result<Unit> = sendMediaResult.also { sendMediaCount++ }
|
||||
|
||||
override fun close() = Unit
|
||||
|
||||
fun givenLeaveRoomError(throwable: Throwable?) {
|
||||
|
|
@ -165,4 +181,8 @@ class FakeMatrixRoom(
|
|||
fun givenUnIgnoreResult(result: Result<Unit>) {
|
||||
unignoreResult = result
|
||||
}
|
||||
|
||||
fun givenSendMediaResult(result: Result<Unit>) {
|
||||
sendMediaResult = result
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ import io.element.android.libraries.matrix.api.media.VideoInfo
|
|||
import java.io.File
|
||||
|
||||
sealed interface MediaUploadInfo {
|
||||
data class Image(val file: File, val info: ImageInfo, val thumbnailInfo: ThumbnailProcessingInfo?) : MediaUploadInfo
|
||||
data class Video(val file: File, val info: VideoInfo, val thumbnailInfo: ThumbnailProcessingInfo?) : MediaUploadInfo
|
||||
data class Image(val file: File, val info: ImageInfo, val thumbnailInfo: ThumbnailProcessingInfo) : MediaUploadInfo
|
||||
data class Video(val file: File, val info: VideoInfo, val thumbnailInfo: ThumbnailProcessingInfo) : MediaUploadInfo
|
||||
data class Audio(val file: File, val info: AudioInfo) : MediaUploadInfo
|
||||
data class AnyFile(val file: File, val info: FileInfo) : MediaUploadInfo
|
||||
}
|
||||
|
|
@ -33,4 +33,5 @@ sealed interface MediaUploadInfo {
|
|||
data class ThumbnailProcessingInfo(
|
||||
val file: File,
|
||||
val info: ThumbnailInfo,
|
||||
val blurhash: String,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ android {
|
|||
implementation(libs.androidx.exifinterface)
|
||||
implementation(libs.coroutines.core)
|
||||
implementation(libs.otaliastudios.transcoder)
|
||||
implementation(libs.vanniktech.blurhash)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package io.element.android.libraries.mediaupload
|
|||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import com.vanniktech.blurhash.BlurHash
|
||||
import io.element.android.libraries.androidutils.bitmap.calculateInSampleSize
|
||||
import io.element.android.libraries.androidutils.bitmap.resizeToMax
|
||||
import io.element.android.libraries.androidutils.bitmap.rotateToMetadataOrientation
|
||||
|
|
@ -48,14 +49,21 @@ class ImageCompressor @Inject constructor(
|
|||
): Result<ImageCompressionResult> = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val compressedBitmap = compressToBitmap(inputStream, resizeMode).getOrThrow()
|
||||
val blurhash = BlurHash.encode(compressedBitmap, 3, 3)
|
||||
|
||||
// Encode bitmap to the destination temporary file
|
||||
val tmpFile = context.createTmpFile()
|
||||
val tmpFile = context.createTmpFile(extension = "jpeg")
|
||||
tmpFile.outputStream().use {
|
||||
compressedBitmap.compress(format, desiredQuality, it)
|
||||
}
|
||||
|
||||
ImageCompressionResult(tmpFile, compressedBitmap.width, compressedBitmap.height, tmpFile.length())
|
||||
ImageCompressionResult(
|
||||
file = tmpFile,
|
||||
width = compressedBitmap.width,
|
||||
height = compressedBitmap.height,
|
||||
size = tmpFile.length(),
|
||||
blurhash = blurhash
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -108,6 +116,7 @@ data class ImageCompressionResult(
|
|||
val width: Int,
|
||||
val height: Int,
|
||||
val size: Long,
|
||||
val blurhash: String,
|
||||
)
|
||||
|
||||
sealed interface ResizeMode {
|
||||
|
|
|
|||
|
|
@ -26,9 +26,7 @@ import io.element.android.libraries.androidutils.file.createTmpFile
|
|||
import io.element.android.libraries.androidutils.media.runAndRelease
|
||||
import io.element.android.libraries.core.data.tryOrNull
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeAudio
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeImage
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.matrix.api.media.AudioInfo
|
||||
|
|
@ -41,7 +39,6 @@ import io.element.android.libraries.mediaupload.api.MediaType
|
|||
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
||||
import io.element.android.libraries.mediaupload.api.ThumbnailProcessingInfo
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
|
@ -93,19 +90,23 @@ class MediaPreProcessorImpl @Inject constructor(
|
|||
deleteOriginal: Boolean,
|
||||
): Result<MediaUploadInfo> = runCatching {
|
||||
// Camera returns an 'octet-stream' mimetype, so it needs to be overridden
|
||||
val originalMimeType = contentResolver.getType(uri)
|
||||
val mimeType = when (mediaType) {
|
||||
MediaType.Image -> MimeTypes.Images
|
||||
MediaType.Video -> MimeTypes.Videos
|
||||
MediaType.Audio -> MimeTypes.Audio
|
||||
else -> originalMimeType
|
||||
val mimeType = contentResolver.getType(uri)
|
||||
val mimeTypeOrDefault = if (mimeType == MimeTypes.OctetStream) {
|
||||
when(mediaType) {
|
||||
MediaType.Image -> MimeTypes.Jpeg
|
||||
MediaType.Video -> MimeTypes.Mp4
|
||||
MediaType.Audio -> MimeTypes.Ogg
|
||||
else -> mimeType
|
||||
}
|
||||
} else {
|
||||
mimeType
|
||||
}
|
||||
val compressBeforeSending = mediaType in sequenceOf(MediaType.Image, MediaType.Video)
|
||||
val result = if (compressBeforeSending && mimeType != MimeTypes.Gif) {
|
||||
when {
|
||||
mimeType.isMimeTypeImage() -> processImage(uri)
|
||||
mimeType.isMimeTypeVideo() -> processVideo(uri, mimeType)
|
||||
mimeType.isMimeTypeAudio() -> processAudio(uri)
|
||||
when(mediaType) {
|
||||
MediaType.Image -> processImage(uri)
|
||||
MediaType.Video -> processVideo(uri, mimeTypeOrDefault)
|
||||
MediaType.Audio -> processAudio(uri, mimeTypeOrDefault)
|
||||
else -> error("Cannot compress file of type: $mimeType")
|
||||
}
|
||||
} else {
|
||||
|
|
@ -115,7 +116,7 @@ class MediaPreProcessorImpl @Inject constructor(
|
|||
removeSensitiveImageMetadata(file)
|
||||
}
|
||||
val info = FileInfo(
|
||||
mimetype = originalMimeType,
|
||||
mimetype = mimeType,
|
||||
size = file.length(),
|
||||
thumbnailInfo = null,
|
||||
thumbnailUrl = null,
|
||||
|
|
@ -141,7 +142,7 @@ class MediaPreProcessorImpl @Inject constructor(
|
|||
removeSensitiveImageMetadata(compressedFileResult.file)
|
||||
|
||||
val thumbnailResult = compressedFileResult.file.inputStream().use { generateImageThumbnail(it) }
|
||||
val processingResult = compressedFileResult.toImageInfo(MimeTypes.Jpeg, thumbnailResult?.file?.path, thumbnailResult?.info)
|
||||
val processingResult = compressedFileResult.toImageInfo(MimeTypes.Jpeg, thumbnailResult.file.path, thumbnailResult.info)
|
||||
return MediaUploadInfo.Image(compressedFileResult.file, processingResult, thumbnailResult)
|
||||
}
|
||||
|
||||
|
|
@ -155,32 +156,33 @@ class MediaPreProcessorImpl @Inject constructor(
|
|||
.first()
|
||||
.file
|
||||
|
||||
val videoProcessingInfo = extractVideoMetadata(resultFile, mimeType, thumbnailInfo?.file?.path, thumbnailInfo?.info)
|
||||
val videoProcessingInfo = extractVideoMetadata(resultFile, mimeType, thumbnailInfo.file.path, thumbnailInfo)
|
||||
return MediaUploadInfo.Video(resultFile, videoProcessingInfo, thumbnailInfo)
|
||||
}
|
||||
|
||||
private suspend fun processAudio(uri: Uri): MediaUploadInfo {
|
||||
private suspend fun processAudio(uri: Uri, mimeType: String?): MediaUploadInfo {
|
||||
val file = copyToTmpFile(uri)
|
||||
return MediaMetadataRetriever().runAndRelease {
|
||||
setDataSource(context, Uri.fromFile(file))
|
||||
|
||||
val info = AudioInfo(
|
||||
duration = extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0L,
|
||||
size = file.length()
|
||||
size = file.length(),
|
||||
mimeType = mimeType,
|
||||
)
|
||||
|
||||
MediaUploadInfo.Audio(file, info)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun generateImageThumbnail(inputStream: InputStream): ThumbnailProcessingInfo? {
|
||||
private suspend fun generateImageThumbnail(inputStream: InputStream): ThumbnailProcessingInfo {
|
||||
val thumbnailResult = imageCompressor
|
||||
.compressToTmpFile(
|
||||
inputStream = inputStream,
|
||||
resizeMode = ResizeMode.Strict(THUMB_MAX_WIDTH, THUMB_MAX_HEIGHT),
|
||||
).getOrNull()
|
||||
).getOrThrow()
|
||||
|
||||
return thumbnailResult?.toThumbnailProcessingInfo(MimeTypes.Jpeg)
|
||||
return thumbnailResult.toThumbnailProcessingInfo(MimeTypes.Jpeg)
|
||||
}
|
||||
|
||||
private fun removeSensitiveImageMetadata(file: File) {
|
||||
|
|
@ -203,7 +205,7 @@ class MediaPreProcessorImpl @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun extractVideoMetadata(file: File, mimeType: String?, thumbnailUrl: String?, thumbnailInfo: ThumbnailInfo?): VideoInfo =
|
||||
private fun extractVideoMetadata(file: File, mimeType: String?, thumbnailUrl: String?, thumbnailInfo: ThumbnailProcessingInfo?): VideoInfo =
|
||||
MediaMetadataRetriever().runAndRelease {
|
||||
setDataSource(context, Uri.fromFile(file))
|
||||
|
||||
|
|
@ -213,16 +215,16 @@ class MediaPreProcessorImpl @Inject constructor(
|
|||
height = extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toLong() ?: 0L,
|
||||
mimetype = mimeType,
|
||||
size = file.length(),
|
||||
thumbnailInfo = thumbnailInfo,
|
||||
thumbnailInfo = thumbnailInfo?.info,
|
||||
thumbnailUrl = thumbnailUrl,
|
||||
blurhash = null,
|
||||
blurhash = thumbnailInfo?.blurhash,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun extractVideoThumbnail(uri: Uri): ThumbnailProcessingInfo? =
|
||||
private suspend fun extractVideoThumbnail(uri: Uri): ThumbnailProcessingInfo =
|
||||
MediaMetadataRetriever().runAndRelease {
|
||||
setDataSource(context, uri)
|
||||
val bitmap = getFrameAtTime(VIDEO_THUMB_FRAME) ?: return@runAndRelease null
|
||||
val bitmap = requireNotNull(getFrameAtTime(VIDEO_THUMB_FRAME))
|
||||
val inputStream = ByteArrayOutputStream().use {
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, it)
|
||||
ByteArrayInputStream(it.toByteArray())
|
||||
|
|
@ -249,7 +251,7 @@ fun ImageCompressionResult.toImageInfo(mimeType: String, thumbnailUrl: String?,
|
|||
size = size,
|
||||
thumbnailInfo = thumbnailInfo,
|
||||
thumbnailUrl = thumbnailUrl,
|
||||
blurhash = null,
|
||||
blurhash = blurhash,
|
||||
)
|
||||
|
||||
fun ImageCompressionResult.toThumbnailProcessingInfo(mimeType: String) = ThumbnailProcessingInfo(
|
||||
|
|
@ -260,4 +262,5 @@ fun ImageCompressionResult.toThumbnailProcessingInfo(mimeType: String) = Thumbna
|
|||
mimetype = mimeType,
|
||||
size = size,
|
||||
),
|
||||
blurhash = blurhash,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ class VideoCompressor @Inject constructor(
|
|||
) {
|
||||
|
||||
fun compress(uri: Uri) = callbackFlow {
|
||||
val tmpFile = context.createTmpFile()
|
||||
val tmpFile = context.createTmpFile(extension = "mp4")
|
||||
val future = Transcoder.into(tmpFile.path)
|
||||
.addDataSource(context, uri)
|
||||
.setListener(object : TranscoderListener {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class, ExperimentalPermissionsApi::class)
|
||||
@file:OptIn(ExperimentalPermissionsApi::class)
|
||||
|
||||
package io.element.android.libraries.permissions.impl
|
||||
|
||||
|
|
@ -25,7 +25,6 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
|||
import com.google.accompanist.permissions.PermissionStatus
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.permissions.api.PermissionsEvents
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
|
|
|
|||
|
|
@ -14,15 +14,12 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.libraries.permissions.noop
|
||||
|
||||
import app.cash.molecule.RecompositionClock
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
|
|
|
|||
|
|
@ -14,13 +14,10 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.libraries.pushstore.impl.clientsecret
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
|
|
|
|||
|
|
@ -20,12 +20,10 @@ import app.cash.turbine.test
|
|||
import com.google.common.truth.Truth.assertThat
|
||||
import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver
|
||||
import io.element.android.libraries.matrix.session.SessionData
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class DatabaseSessionStoreTests {
|
||||
|
||||
private lateinit var database: SessionDatabase
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue