From 023bfc2ffa2a08eda19bbe734471ad0659bc4ac7 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Wed, 30 Aug 2023 16:31:37 +0200 Subject: [PATCH 1/7] Upgrade rust sdk to v48 (#1186) - Sends content instead of string in message reply and edit - Adds poll response and end APIs - Adds logoUri to OidcConfiguration --- gradle/libs.versions.toml | 2 +- .../libraries/matrix/api/room/MatrixRoom.kt | 16 +++++++ .../libraries/matrix/impl/auth/OidcConfig.kt | 1 + .../matrix/impl/room/RustMatrixRoom.kt | 32 ++++++++++++-- .../matrix/test/room/FakeMatrixRoom.kt | 42 +++++++++++++++++++ 5 files changed, 88 insertions(+), 5 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c8af411131..0d52d1e0df 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -146,7 +146,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" } timber = "com.jakewharton.timber:timber:5.0.1" -matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.47" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.48" 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-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 88d35c83d4..41f105df8e 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -157,6 +157,22 @@ interface MatrixRoom : Closeable { pollKind: PollKind, ): Result + /** + * Send a response to a poll. + * + * @param pollStartId The event ID of the poll start event. + * @param answers The list of answer ids to send. + */ + suspend fun sendPollResponse(pollStartId: EventId, answers: List): Result + + /** + * Ends a poll in the room. + * + * @param pollStartId The event ID of the poll start event. + * @param text Fallback text of the poll end event. + */ + suspend fun endPoll(pollStartId: EventId, text: String): Result + override fun close() = destroy() } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfig.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfig.kt index 401fa0ce83..dae032f516 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfig.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfig.kt @@ -23,6 +23,7 @@ val oidcConfiguration: OidcConfiguration = OidcConfiguration( clientName = "Element", redirectUri = OidcConfig.redirectUri, clientUri = "https://element.io", + logoUri = "https://element.io/mobile-icon.png", tosUri = "https://element.io/user-terms-of-service", policyUri = "https://element.io/privacy", /** diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index ffe06379d3..71caed8f19 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -215,7 +215,7 @@ class RustMatrixRoom( override suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, message: String): Result = withContext(roomDispatcher) { if (originalEventId != null) { runCatching { - innerRoom.edit(/* TODO use content */ message, originalEventId.value, transactionId?.value) + innerRoom.edit(messageEventContentFromMarkdown(message), originalEventId.value, transactionId?.value) } } else { runCatching { @@ -226,10 +226,8 @@ class RustMatrixRoom( } override suspend fun replyMessage(eventId: EventId, message: String): Result = withContext(roomDispatcher) { - val transactionId = genTransactionId() - // val content = messageEventContentFromMarkdown(message) runCatching { - innerRoom.sendReply(/* TODO use content */ message, eventId.value, transactionId) + innerRoom.sendReply(messageEventContentFromMarkdown(message), eventId.value, genTransactionId()) } } @@ -402,6 +400,32 @@ class RustMatrixRoom( } } + override suspend fun sendPollResponse( + pollStartId: EventId, + answers: List + ): Result = withContext(roomDispatcher) { + runCatching { + innerRoom.sendPollResponse( + pollStartId = pollStartId.value, + answers = answers, + txnId = genTransactionId(), + ) + } + } + + override suspend fun endPoll( + pollStartId: EventId, + text: String + ): Result = withContext(roomDispatcher) { + runCatching { + innerRoom.endPoll( + pollStartId = pollStartId.value, + text = text, + txnId = genTransactionId(), + ) + } + } + private suspend fun sendAttachment(files: List, handle: () -> SendAttachmentJoinHandle): Result { return runCatching { MediaUploadHandlerImpl(files, handle()) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index 88f705162e..7bffb985bb 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -85,6 +85,8 @@ class FakeMatrixRoom( private var reportContentResult = Result.success(Unit) private var sendLocationResult = Result.success(Unit) private var createPollResult = Result.success(Unit) + private var sendPollResponseResult = Result.success(Unit) + private var endPollResult = Result.success(Unit) private var progressCallbackValues = emptyList>() val editMessageCalls = mutableListOf() @@ -109,6 +111,12 @@ class FakeMatrixRoom( private val _createPollInvocations = mutableListOf() val createPollInvocations: List = _createPollInvocations + private val _sendPollResponseInvocations = mutableListOf() + val sendPollResponseInvocations: List = _sendPollResponseInvocations + + private val _endPollInvocations = mutableListOf() + val endPollInvocations: List = _endPollInvocations + var invitedUserId: UserId? = null private set @@ -320,6 +328,22 @@ class FakeMatrixRoom( return createPollResult } + override suspend fun sendPollResponse( + pollStartId: EventId, + answers: List + ): Result = simulateLongTask { + _sendPollResponseInvocations.add(SendPollResponseInvocation(pollStartId, answers)) + return sendPollResponseResult + } + + override suspend fun endPoll( + pollStartId: EventId, + text: String + ): Result = simulateLongTask { + _endPollInvocations.add(EndPollInvocation(pollStartId, text)) + return endPollResult + } + fun givenLeaveRoomError(throwable: Throwable?) { this.leaveRoomError = throwable } @@ -416,6 +440,14 @@ class FakeMatrixRoom( createPollResult = result } + fun givenSendPollResponseResult(result: Result) { + sendPollResponseResult = result + } + + fun givenEndPollResult(result: Result) { + endPollResult = result + } + fun givenProgressCallbackValues(values: List>) { progressCallbackValues = values } @@ -435,3 +467,13 @@ data class CreatePollInvocation( val maxSelections: Int, val pollKind: PollKind, ) + +data class SendPollResponseInvocation( + val pollStartId: EventId, + val answers: List, +) + +data class EndPollInvocation( + val pollStartId: EventId, + val text: String, +) From 0ee57c83a9e9480138015a1f97acb2f238942c00 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 30 Aug 2023 16:47:40 +0200 Subject: [PATCH 2/7] Rename file and update `tosUri` value. --- .../matrix/impl/auth/{OidcConfig.kt => OidcConfiguration.kt} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/{OidcConfig.kt => OidcConfiguration.kt} (95%) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfig.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfiguration.kt similarity index 95% rename from libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfig.kt rename to libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfiguration.kt index dae032f516..f49ee65208 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfig.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfiguration.kt @@ -24,7 +24,7 @@ val oidcConfiguration: OidcConfiguration = OidcConfiguration( redirectUri = OidcConfig.redirectUri, clientUri = "https://element.io", logoUri = "https://element.io/mobile-icon.png", - tosUri = "https://element.io/user-terms-of-service", + tosUri = "https://element.io/acceptable-use-policy-terms", policyUri = "https://element.io/privacy", /** * Some homeservers/auth issuers don't support dynamic client registration, and have to be registered manually From e4dd312668f934f6732fc75de1a2ec1ad7707b4c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 30 Aug 2023 16:59:42 +0200 Subject: [PATCH 3/7] We are now using kotlinc 1.9.10, so Android Studio is updating this file. --- .idea/kotlinc.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index fdf8d994a6..f8467b458e 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file From 14791c36281d142d206c5d8d56dd192fd1c1d52a Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Wed, 30 Aug 2023 17:05:11 +0200 Subject: [PATCH 4/7] Poll vote (#1181) - Adds sendPollVote rust room API (still not operational, need to wait for a rust sdk release) - Adds an optional EventId in TimelineItemPollContent - Wires the poll answer click listeners all the way to the TimelinePresenter in order to call the new room API - Shows question as message summary in long press menu Closes https://github.com/vector-im/element-meta/issues/2025 --- .../messages/impl/timeline/TimelineEvents.kt | 4 +++ .../impl/timeline/TimelinePresenter.kt | 7 ++++ .../messages/impl/timeline/TimelineView.kt | 7 ++++ .../components/TimelineItemEventRow.kt | 17 ++++++++- .../components/TimelineItemStateEventRow.kt | 1 + .../event/TimelineItemContentView.kt | 4 ++- .../components/event/TimelineItemPollView.kt | 7 ++-- .../event/TimelineItemContentFactory.kt | 2 +- .../event/TimelineItemContentPollFactory.kt | 7 +++- .../model/event/TimelineItemPollContent.kt | 2 ++ .../event/TimelineItemPollContentProvider.kt | 2 ++ .../MessageSummaryFormatterImpl.kt | 2 +- .../actionlist/ActionListPresenterTest.kt | 2 ++ .../timeline/TimelinePresenterTest.kt | 32 ++++++++++++++++- .../TimelineItemContentPollFactoryTest.kt | 35 +++++++++++++------ .../features/poll/api/PollContentView.kt | 19 +++++++--- 16 files changed, 125 insertions(+), 25 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt index 30f9aade79..e427646a71 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt @@ -22,4 +22,8 @@ sealed interface TimelineEvents { data object LoadMore : TimelineEvents data class SetHighlightedEvent(val eventId: EventId?) : TimelineEvents data class OnScrollFinished(val firstIndex: Int) : TimelineEvents + data class PollAnswerSelected( + val pollStartId: EventId, + val answerId: String + ) : TimelineEvents } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index c53d8fc279..402c332855 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -87,6 +87,13 @@ class TimelinePresenter @Inject constructor( lastReadReceiptId = lastReadReceiptId ) } + is TimelineEvents.PollAnswerSelected -> appScope.launch { + room.sendPollResponse( + pollStartId = event.pollStartId, + answers = listOf(event.answerId), + ) + // TODO Polls: Send poll vote analytic + } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index 6e16a3b92d..ff90e8d29a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -100,6 +100,9 @@ fun TimelineView( // TODO implement this logic once we have support to 'jump to event X' in sliding sync } + fun onPollAnswerSelected(pollStartId: EventId, answerId: String) { + state.eventSink(TimelineEvents.PollAnswerSelected(pollStartId, answerId)) + } Box(modifier = modifier) { LazyColumn( @@ -125,6 +128,7 @@ fun TimelineView( onReactionLongClick = onReactionLongClicked, onMoreReactionsClick = onMoreReactionsClicked, onTimestampClicked = onTimestampClicked, + onPollAnswerSelected = ::onPollAnswerSelected, onSwipeToReply = onSwipeToReply, ) } @@ -162,6 +166,7 @@ fun TimelineItemRow( onMoreReactionsClick: (TimelineItem.Event) -> Unit, onTimestampClicked: (TimelineItem.Event) -> Unit, onSwipeToReply: (TimelineItem.Event) -> Unit, + onPollAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit, modifier: Modifier = Modifier ) { when (timelineItem) { @@ -194,6 +199,7 @@ fun TimelineItemRow( onMoreReactionsClick = onMoreReactionsClick, onTimestampClicked = onTimestampClicked, onSwipeToReply = { onSwipeToReply(timelineItem) }, + onPollAnswerSelected = onPollAnswerSelected, modifier = modifier, ) } @@ -231,6 +237,7 @@ fun TimelineItemRow( onReactionClick = onReactionClick, onReactionLongClick = onReactionLongClick, onMoreReactionsClick = onMoreReactionsClick, + onPollAnswerSelected = onPollAnswerSelected, onSwipeToReply = {}, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt index dd5a2df2e0..10671ee00f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt @@ -118,6 +118,7 @@ fun TimelineItemEventRow( onReactionLongClick: (emoji: String, eventId: TimelineItem.Event) -> Unit, onMoreReactionsClick: (eventId: TimelineItem.Event) -> Unit, onSwipeToReply: () -> Unit, + onPollAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit, modifier: Modifier = Modifier ) { val coroutineScope = rememberCoroutineScope() @@ -175,6 +176,7 @@ fun TimelineItemEventRow( onReactionClicked = { emoji -> onReactionClick(emoji, event) }, onReactionLongClicked = { emoji -> onReactionLongClick(emoji, event) }, onMoreReactionsClicked = { onMoreReactionsClick(event) }, + onPollAnswerSelected = onPollAnswerSelected, ) } } @@ -191,6 +193,7 @@ fun TimelineItemEventRow( onReactionClicked = { emoji -> onReactionClick(emoji, event) }, onReactionLongClicked = { emoji -> onReactionLongClick(emoji, event) }, onMoreReactionsClicked = { onMoreReactionsClick(event) }, + onPollAnswerSelected = onPollAnswerSelected, ) } } @@ -232,6 +235,7 @@ private fun TimelineItemEventRowContent( onReactionClicked: (emoji: String) -> Unit, onReactionLongClicked: (emoji: String) -> Unit, onMoreReactionsClicked: (event: TimelineItem.Event) -> Unit, + onPollAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit, modifier: Modifier = Modifier, ) { fun ConstrainScope.linkStartOrEnd(event: TimelineItem.Event) = if (event.isMine) { @@ -289,7 +293,8 @@ private fun TimelineItemEventRowContent( inReplyToClick = inReplyToClicked, onTimestampClicked = { onTimestampClicked(event) - } + }, + onPollAnswerSelected = onPollAnswerSelected, ) } @@ -360,6 +365,7 @@ private fun MessageEventBubbleContent( onMessageLongClick: () -> Unit, inReplyToClick: () -> Unit, onTimestampClicked: () -> Unit, + onPollAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit, @SuppressLint("ModifierParameter") bubbleModifier: Modifier = Modifier, // need to rename this modifier to distinguish it from the following ones ) { val timestampPosition = when (event.content) { @@ -385,6 +391,7 @@ private fun MessageEventBubbleContent( onClick = onMessageClick, onLongClick = onMessageLongClick, extraPadding = event.toExtraPadding(), + onPollAnswerSelected = onPollAnswerSelected, modifier = modifier, ) } @@ -607,6 +614,7 @@ private fun ContentToPreview() { onMoreReactionsClick = {}, onTimestampClicked = {}, onSwipeToReply = {}, + onPollAnswerSelected = { _, _ -> }, ) TimelineItemEventRow( event = aTimelineItemEvent( @@ -627,6 +635,7 @@ private fun ContentToPreview() { onMoreReactionsClick = {}, onTimestampClicked = {}, onSwipeToReply = {}, + onPollAnswerSelected = { _, _ -> }, ) } } @@ -674,6 +683,7 @@ private fun ContentToPreviewWithReply() { onMoreReactionsClick = {}, onTimestampClicked = {}, onSwipeToReply = {}, + onPollAnswerSelected = { _, _ -> }, ) TimelineItemEventRow( event = aTimelineItemEvent( @@ -695,6 +705,7 @@ private fun ContentToPreviewWithReply() { onMoreReactionsClick = {}, onTimestampClicked = {}, onSwipeToReply = {}, + onPollAnswerSelected = { _, _ -> }, ) } } @@ -752,6 +763,7 @@ private fun ContentTimestampToPreview(event: TimelineItem.Event) { onMoreReactionsClick = {}, onTimestampClicked = {}, onSwipeToReply = {}, + onPollAnswerSelected = { _, _ -> }, ) } } @@ -792,6 +804,7 @@ private fun ContentWithManyReactionsToPreview() { onMoreReactionsClick = {}, onSwipeToReply = {}, onTimestampClicked = {}, + onPollAnswerSelected = { _, _ -> }, ) } } @@ -816,6 +829,7 @@ internal fun TimelineItemEventRowLongSenderNamePreview() = ElementPreviewLight { onMoreReactionsClick = {}, onSwipeToReply = {}, onTimestampClicked = {}, + onPollAnswerSelected = { _, _ -> }, ) } @@ -836,5 +850,6 @@ internal fun TimelineItemEventTimestampBelowPreview() = ElementPreviewLight { onMoreReactionsClick = {}, onSwipeToReply = {}, onTimestampClicked = {}, + onPollAnswerSelected = { _, _ -> }, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt index d22182d098..b976d24a25 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt @@ -70,6 +70,7 @@ fun TimelineItemStateEventRow( onClick = onClick, onLongClick = onLongClick, extraPadding = noExtraPadding, + onPollAnswerSelected = { _, _ -> error("Polls are not supported in state events") }, modifier = Modifier.defaultTimelineContentPadding() ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt index d53e3f1e5b..e882950d6c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt @@ -31,6 +31,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent +import io.element.android.libraries.matrix.api.core.EventId @Composable fun TimelineItemEventContentView( @@ -39,6 +40,7 @@ fun TimelineItemEventContentView( extraPadding: ExtraPadding, onClick: () -> Unit, onLongClick: () -> Unit, + onPollAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit, modifier: Modifier = Modifier ) { when (content) { @@ -93,7 +95,7 @@ fun TimelineItemEventContentView( ) is TimelineItemPollContent -> TimelineItemPollView( content = content, - onAnswerSelected = {}, + onAnswerSelected = onPollAnswerSelected, modifier = modifier, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.kt index 3608593cde..ec784ad331 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.kt @@ -24,16 +24,17 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.poll.api.PollContentView import io.element.android.libraries.designsystem.preview.DayNightPreviews import io.element.android.libraries.designsystem.preview.ElementPreview -import io.element.android.libraries.matrix.api.poll.PollAnswer +import io.element.android.libraries.matrix.api.core.EventId import kotlinx.collections.immutable.toImmutableList @Composable fun TimelineItemPollView( content: TimelineItemPollContent, - onAnswerSelected: (PollAnswer) -> Unit, + onAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit, modifier: Modifier = Modifier, ) { PollContentView( + eventId = content.eventId, question = content.question, answerItems = content.answerItems.toImmutableList(), pollKind = content.pollKind, @@ -49,6 +50,6 @@ internal fun TimelineItemPollViewPreview(@PreviewParameter(TimelineItemPollConte ElementPreview { TimelineItemPollView( content = content, - onAnswerSelected = {}, + onAnswerSelected = { _, _ -> }, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFactory.kt index 1de4ee7c86..6a74fd11a5 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFactory.kt @@ -55,7 +55,7 @@ class TimelineItemContentFactory @Inject constructor( is RoomMembershipContent -> roomMembershipFactory.create(eventTimelineItem) is StateContent -> stateFactory.create(eventTimelineItem) is StickerContent -> stickerFactory.create(itemContent) - is PollContent -> pollFactory.create(itemContent) + is PollContent -> pollFactory.create(itemContent, eventTimelineItem.eventId) is UnableToDecryptContent -> utdFactory.create(itemContent) is UnknownContent -> TimelineItemUnknownContent } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactory.kt index 9c06b17056..04551f7086 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactory.kt @@ -23,6 +23,7 @@ import io.element.android.features.poll.api.PollAnswerItem import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.poll.isDisclosed import io.element.android.libraries.matrix.api.timeline.item.event.PollContent import javax.inject.Inject @@ -32,7 +33,10 @@ class TimelineItemContentPollFactory @Inject constructor( private val featureFlagService: FeatureFlagService, ) { - suspend fun create(content: PollContent): TimelineItemEventContent { + suspend fun create( + content: PollContent, + eventId: EventId? + ): TimelineItemEventContent { if (!featureFlagService.isFeatureEnabled(FeatureFlags.Polls)) return TimelineItemUnknownContent // Todo Move this computation to the matrix rust sdk @@ -67,6 +71,7 @@ class TimelineItemContentPollFactory @Inject constructor( } return TimelineItemPollContent( + eventId = eventId, question = content.question, answerItems = answerItems, pollKind = content.kind, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContent.kt index 0f94b97776..dc47c12a86 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContent.kt @@ -17,9 +17,11 @@ package io.element.android.features.messages.impl.timeline.model.event import io.element.android.features.poll.api.PollAnswerItem +import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.poll.PollKind data class TimelineItemPollContent( + val eventId: EventId?, val question: String, val answerItems: List, val pollKind: PollKind, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt index 247d450ae7..c475f6dfbd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt @@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.timeline.model.event import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.poll.api.aPollAnswerItemList +import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.poll.PollKind open class TimelineItemPollContentProvider : PreviewParameterProvider { @@ -30,6 +31,7 @@ open class TimelineItemPollContentProvider : PreviewParameterProvider context.getString(CommonStrings.common_shared_location) is TimelineItemEncryptedContent -> context.getString(CommonStrings.common_unable_to_decrypt) is TimelineItemRedactedContent -> context.getString(CommonStrings.common_message_removed) - is TimelineItemPollContent, // Todo Polls: handle summary + is TimelineItemPollContent -> event.content.question is TimelineItemUnknownContent -> context.getString(CommonStrings.common_unsupported_event) is TimelineItemImageContent -> context.getString(CommonStrings.common_image) is TimelineItemVideoContent -> context.getString(CommonStrings.common_video) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt index b3c805d32d..111b3a370d 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt @@ -32,6 +32,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent import io.element.android.features.poll.api.PollAnswerItem +import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.poll.PollAnswer import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.test.A_MESSAGE @@ -384,6 +385,7 @@ class ActionListPresenterTest { val messageEvent = aMessageEvent( isMine = true, content = TimelineItemPollContent( + eventId = EventId("\$anEventId"), question = "Some question?", answerItems = listOf( PollAnswerItem( diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt index b4bb14f672..860f8def37 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt @@ -25,7 +25,7 @@ import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.TimelinePresenter import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory import io.element.android.features.messages.impl.timeline.model.TimelineItem -import io.element.android.libraries.matrix.ui.components.aMatrixUserList +import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction @@ -36,8 +36,10 @@ import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aMessageContent import io.element.android.libraries.matrix.test.room.anEventTimelineItem import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline +import io.element.android.libraries.matrix.ui.components.aMatrixUserList import io.element.android.tests.testutils.awaitWithLatch import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Test @@ -248,6 +250,23 @@ class TimelinePresenterTest { } } + @Test + fun `present - PollAnswerSelected event calls into rust room api and analytics`() = runTest { + val room = FakeMatrixRoom() + val presenter = createTimelinePresenter(room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(TimelineEvents.PollAnswerSelected(AN_EVENT_ID, "anAnswerId")) + } + delay(1) + assertThat(room.sendPollResponseInvocations.size).isEqualTo(1) + assertThat(room.sendPollResponseInvocations.first().answers).isEqualTo(listOf("anAnswerId")) + assertThat(room.sendPollResponseInvocations.first().pollStartId).isEqualTo(AN_EVENT_ID) + // TODO Polls: Test poll vote analytic + } + private fun TestScope.createTimelinePresenter( timeline: MatrixTimeline = FakeMatrixTimeline(), timelineItemsFactory: TimelineItemsFactory = aTimelineItemsFactory() @@ -259,4 +278,15 @@ class TimelinePresenterTest { appScope = this ) } + + private fun TestScope.createTimelinePresenter( + room: MatrixRoom, + ): TimelinePresenter { + return TimelinePresenter( + timelineItemsFactory = aTimelineItemsFactory(), + room = room, + dispatchers = testCoroutineDispatchers(), + appScope = this + ) + } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/factories/event/TimelineItemContentPollFactoryTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/factories/event/TimelineItemContentPollFactoryTest.kt index a1411293e9..8cf5704bba 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/factories/event/TimelineItemContentPollFactoryTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/factories/event/TimelineItemContentPollFactoryTest.kt @@ -22,10 +22,12 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.poll.api.PollAnswerItem import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +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.poll.PollAnswer import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.timeline.item.event.PollContent +import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID_10 import io.element.android.libraries.matrix.test.A_USER_ID_2 @@ -49,14 +51,14 @@ internal class TimelineItemContentPollFactoryTest { @Test fun `Disclosed poll - not ended, no votes`() = runTest { - Truth.assertThat(factory.create(aPollContent())).isEqualTo(aTimelineItemPollContent()) + Truth.assertThat(factory.create(aPollContent(), eventId = null)).isEqualTo(aTimelineItemPollContent()) } @Test fun `Disclosed poll - not ended, some votes, including one from current user`() = runTest { val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id } Truth.assertThat( - factory.create(aPollContent(votes = votes)) + factory.create(aPollContent(votes = votes), eventId = null) ) .isEqualTo( aTimelineItemPollContent( @@ -73,7 +75,7 @@ internal class TimelineItemContentPollFactoryTest { @Test fun `Disclosed poll - ended, no votes, no winner`() = runTest { Truth.assertThat( - factory.create(aPollContent(endTime = 1UL)) + factory.create(aPollContent(endTime = 1UL), eventId = null) ).isEqualTo( aTimelineItemPollContent().let { it.copy( @@ -88,7 +90,7 @@ internal class TimelineItemContentPollFactoryTest { fun `Disclosed poll - ended, some votes, including one from current user (winner)`() = runTest { val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id } Truth.assertThat( - factory.create(aPollContent(votes = votes, endTime = 1UL)) + factory.create(aPollContent(votes = votes, endTime = 1UL), eventId = null) ) .isEqualTo( aTimelineItemPollContent( @@ -107,7 +109,7 @@ internal class TimelineItemContentPollFactoryTest { fun `Disclosed poll - ended, some votes, including one from current user (not winner) and two winning votes`() = runTest { val votes = OTHER_WINNING_VOTES.mapKeys { it.key.id } Truth.assertThat( - factory.create(aPollContent(votes = votes, endTime = 1UL)) + factory.create(aPollContent(votes = votes, endTime = 1UL), eventId = null) ) .isEqualTo( aTimelineItemPollContent( @@ -125,9 +127,9 @@ internal class TimelineItemContentPollFactoryTest { @Test fun `Undisclosed poll - not ended, no votes`() = runTest { Truth.assertThat( - factory.create(aPollContent(PollKind.Undisclosed).copy()) + factory.create(aPollContent(PollKind.Undisclosed).copy(), eventId = null) ).isEqualTo( - aTimelineItemPollContent(PollKind.Undisclosed).let { + aTimelineItemPollContent(pollKind = PollKind.Undisclosed).let { it.copy(answerItems = it.answerItems.map { answerItem -> answerItem.copy(isDisclosed = false) }) } ) @@ -137,7 +139,7 @@ internal class TimelineItemContentPollFactoryTest { fun `Undisclosed poll - not ended, some votes, including one from current user`() = runTest { val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id } Truth.assertThat( - factory.create(aPollContent(pollKind = PollKind.Undisclosed, votes = votes)) + factory.create(aPollContent(pollKind = PollKind.Undisclosed, votes = votes), eventId = null) ) .isEqualTo( aTimelineItemPollContent( @@ -155,7 +157,7 @@ internal class TimelineItemContentPollFactoryTest { @Test fun `Undisclosed poll - ended, no votes, no winner`() = runTest { Truth.assertThat( - factory.create(aPollContent(pollKind = PollKind.Undisclosed, endTime = 1UL)) + factory.create(aPollContent(pollKind = PollKind.Undisclosed, endTime = 1UL), eventId = null) ).isEqualTo( aTimelineItemPollContent().let { it.copy( @@ -173,7 +175,7 @@ internal class TimelineItemContentPollFactoryTest { fun `Undisclosed poll - ended, some votes, including one from current user (winner)`() = runTest { val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id } Truth.assertThat( - factory.create(aPollContent(pollKind = PollKind.Undisclosed, votes = votes, endTime = 1UL)) + factory.create(aPollContent(pollKind = PollKind.Undisclosed, votes = votes, endTime = 1UL), eventId = null) ) .isEqualTo( aTimelineItemPollContent( @@ -193,7 +195,7 @@ internal class TimelineItemContentPollFactoryTest { fun `Undisclosed poll - ended, some votes, including one from current user (not winner) and two winning votes`() = runTest { val votes = OTHER_WINNING_VOTES.mapKeys { it.key.id } Truth.assertThat( - factory.create(aPollContent(PollKind.Undisclosed).copy(votes = votes, endTime = 1UL)) + factory.create(aPollContent(PollKind.Undisclosed).copy(votes = votes, endTime = 1UL), eventId = null) ) .isEqualTo( aTimelineItemPollContent( @@ -209,6 +211,15 @@ internal class TimelineItemContentPollFactoryTest { ) } + @Test + fun `eventId is populated`() = runTest { + Truth.assertThat(factory.create(aPollContent(), eventId = null)) + .isEqualTo(aTimelineItemPollContent(eventId = null)) + + Truth.assertThat(factory.create(aPollContent(), eventId = AN_EVENT_ID)) + .isEqualTo(aTimelineItemPollContent(eventId = AN_EVENT_ID)) + } + private fun aPollContent( pollKind: PollKind = PollKind.Disclosed, votes: Map> = emptyMap(), @@ -223,6 +234,7 @@ internal class TimelineItemContentPollFactoryTest { ) private fun aTimelineItemPollContent( + eventId: EventId? = null, pollKind: PollKind = PollKind.Disclosed, answerItems: List = listOf( aPollAnswerItem(A_POLL_ANSWER_1), @@ -232,6 +244,7 @@ internal class TimelineItemContentPollFactoryTest { ), isEnded: Boolean = false, ) = TimelineItemPollContent( + eventId = eventId, question = A_POLL_QUESTION, answerItems = answerItems, pollKind = pollKind, diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt index 419aa21204..438c14456c 100644 --- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt @@ -35,6 +35,7 @@ import io.element.android.libraries.designsystem.preview.DayNightPreviews import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.poll.PollAnswer import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.theme.ElementTheme @@ -43,13 +44,18 @@ import kotlinx.collections.immutable.ImmutableList @Composable fun PollContentView( + eventId: EventId?, question: String, answerItems: ImmutableList, pollKind: PollKind, isPollEnded: Boolean, - onAnswerSelected: (PollAnswer) -> Unit, + onAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit, modifier: Modifier = Modifier, ) { + fun onAnswerSelected(pollAnswer: PollAnswer) { + eventId?.let { onAnswerSelected(it, pollAnswer.id) } + } + Column( modifier = modifier .selectableGroup() @@ -58,7 +64,7 @@ fun PollContentView( ) { PollTitle(title = question) - PollAnswers(answerItems = answerItems, onAnswerSelected = onAnswerSelected) + PollAnswers(answerItems = answerItems, onAnswerSelected = ::onAnswerSelected) when { isPollEnded || pollKind == PollKind.Disclosed -> DisclosedPollBottomNotice(answerItems) @@ -134,11 +140,12 @@ fun ColumnScope.UndisclosedPollBottomNotice(modifier: Modifier = Modifier) { @Composable internal fun PollContentUndisclosedPreview() = ElementPreview { PollContentView( + eventId = EventId("\$anEventId"), question = "What type of food should we have at the party?", answerItems = aPollAnswerItemList(isDisclosed = false), pollKind = PollKind.Undisclosed, isPollEnded = false, - onAnswerSelected = { }, + onAnswerSelected = { _, _ -> }, ) } @@ -146,11 +153,12 @@ internal fun PollContentUndisclosedPreview() = ElementPreview { @Composable internal fun PollContentDisclosedPreview() = ElementPreview { PollContentView( + eventId = EventId("\$anEventId"), question = "What type of food should we have at the party?", answerItems = aPollAnswerItemList(), pollKind = PollKind.Disclosed, isPollEnded = false, - onAnswerSelected = { }, + onAnswerSelected = { _, _ -> }, ) } @@ -158,10 +166,11 @@ internal fun PollContentDisclosedPreview() = ElementPreview { @Composable internal fun PollContentEndedPreview() = ElementPreview { PollContentView( + eventId = EventId("\$anEventId"), question = "What type of food should we have at the party?", answerItems = aPollAnswerItemList(isEnded = true), pollKind = PollKind.Disclosed, isPollEnded = false, - onAnswerSelected = { }, + onAnswerSelected = { _, _ -> }, ) } From b16dc457542056771f6cbd18e7dc7afbe65a57f2 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Wed, 30 Aug 2023 19:02:37 +0200 Subject: [PATCH 5/7] Fix the orientation of sent images (#1190) * Fix the orientation of sent images --------- Co-authored-by: Benoit Marty --- changelog.d/1135.bugfix | 1 + .../libraries/androidutils/bitmap/Bitmap.kt | 19 +++++++------------ .../mediaupload/AndroidMediaPreProcessor.kt | 7 +++++++ .../libraries/mediaupload/ImageCompressor.kt | 11 +++++++---- 4 files changed, 22 insertions(+), 16 deletions(-) create mode 100644 changelog.d/1135.bugfix diff --git a/changelog.d/1135.bugfix b/changelog.d/1135.bugfix new file mode 100644 index 0000000000..2b963c7732 --- /dev/null +++ b/changelog.d/1135.bugfix @@ -0,0 +1 @@ +Fix the orientation of sent images. diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/bitmap/Bitmap.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/bitmap/Bitmap.kt index 6f8aa76d03..c3b7e3110e 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/bitmap/Bitmap.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/bitmap/Bitmap.kt @@ -22,7 +22,6 @@ import android.graphics.Matrix import androidx.core.graphics.scale import androidx.exifinterface.media.ExifInterface import java.io.File -import java.io.InputStream import kotlin.math.min fun File.writeBitmap(bitmap: Bitmap, format: Bitmap.CompressFormat, quality: Int) { @@ -32,13 +31,6 @@ fun File.writeBitmap(bitmap: Bitmap, format: Bitmap.CompressFormat, quality: Int } } -/** - * Reads the EXIF metadata from the [inputStream] and rotates the current [Bitmap] to match it. - * @return The resulting [Bitmap] or `null` if no metadata was found. - */ -fun Bitmap.rotateToMetadataOrientation(inputStream: InputStream): Result = - runCatching { rotateToMetadataOrientation(this, ExifInterface(inputStream)) } - /** * Scales the current [Bitmap] to fit the ([maxWidth], [maxHeight]) bounds while keeping aspect ratio. * @throws IllegalStateException if [maxWidth] or [maxHeight] <= 0. @@ -77,8 +69,11 @@ fun BitmapFactory.Options.calculateInSampleSize(desiredWidth: Int, desiredHeight return inSampleSize } -private fun rotateToMetadataOrientation(bitmap: Bitmap, exifInterface: ExifInterface): Bitmap { - val orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) +/** + * Decodes the [inputStream] into a [Bitmap] and applies the needed rotation based on [orientation]. + * This orientation value must be one of `ExifInterface.ORIENTATION_*` constants. + */ +fun Bitmap.rotateToMetadataOrientation(orientation: Int): Bitmap { val matrix = Matrix() when (orientation) { ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270f) @@ -94,8 +89,8 @@ private fun rotateToMetadataOrientation(bitmap: Bitmap, exifInterface: ExifInter matrix.preRotate(90f) matrix.preScale(-1f, 1f) } - else -> return bitmap + else -> return this } - return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) + return Bitmap.createBitmap(this, 0, 0, width, height, matrix, true) } diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt index 8ff40fae39..9fc160252b 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt @@ -119,10 +119,17 @@ class AndroidMediaPreProcessor @Inject constructor( private suspend fun processImage(uri: Uri, mimeType: String, shouldBeCompressed: Boolean): MediaUploadInfo { suspend fun processImageWithCompression(): MediaUploadInfo { + // Read the orientation metadata from its own stream. Trying to reuse this stream for compression will fail. + val orientation = contentResolver.openInputStream(uri).use { input -> + val exifInterface = input?.let { ExifInterface(it) } + exifInterface?.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED) + } ?: ExifInterface.ORIENTATION_UNDEFINED + val compressionResult = contentResolver.openInputStream(uri).use { input -> imageCompressor.compressToTmpFile( inputStream = requireNotNull(input), resizeMode = ResizeMode.Approximate(IMAGE_SCALE_REF_SIZE, IMAGE_SCALE_REF_SIZE), + orientation = orientation, ).getOrThrow() } val thumbnailResult: ThumbnailResult = thumbnailFactory.createImageThumbnail(compressionResult.file) diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ImageCompressor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ImageCompressor.kt index ab30f67b65..a619a27bd9 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ImageCompressor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ImageCompressor.kt @@ -19,6 +19,7 @@ package io.element.android.libraries.mediaupload import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory +import androidx.exifinterface.media.ExifInterface import io.element.android.libraries.androidutils.bitmap.calculateInSampleSize import io.element.android.libraries.androidutils.bitmap.resizeToMax import io.element.android.libraries.androidutils.bitmap.rotateToMetadataOrientation @@ -37,17 +38,18 @@ class ImageCompressor @Inject constructor( /** * Decodes the [inputStream] into a [Bitmap] and applies the needed transformations (rotation, scale) based on [resizeMode], then writes it into a - * temporary file using the passed [format] and [desiredQuality]. + * temporary file using the passed [format], [orientation] and [desiredQuality]. * @return a [Result] containing the resulting [ImageCompressionResult] with the temporary [File] and some metadata. */ suspend fun compressToTmpFile( inputStream: InputStream, resizeMode: ResizeMode, format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG, + orientation: Int = ExifInterface.ORIENTATION_UNDEFINED, desiredQuality: Int = 80, ): Result = withContext(Dispatchers.IO) { runCatching { - val compressedBitmap = compressToBitmap(inputStream, resizeMode).getOrThrow() + val compressedBitmap = compressToBitmap(inputStream, resizeMode, orientation).getOrThrow() // Encode bitmap to the destination temporary file val tmpFile = context.createTmpFile(extension = "jpeg") tmpFile.outputStream().use { @@ -63,19 +65,20 @@ class ImageCompressor @Inject constructor( } /** - * Decodes the [inputStream] into a [Bitmap] and applies the needed transformations (rotation, scale) based on [resizeMode]. + * Decodes the [inputStream] into a [Bitmap] and applies the needed transformations (rotation, scale) based on [resizeMode] and [orientation]. * @return a [Result] containing the resulting [Bitmap]. */ fun compressToBitmap( inputStream: InputStream, resizeMode: ResizeMode, + orientation: Int, ): Result = runCatching { BufferedInputStream(inputStream).use { input -> val options = BitmapFactory.Options() calculateDecodingScale(input, resizeMode, options) val decodedBitmap = BitmapFactory.decodeStream(input, null, options) ?: error("Decoding Bitmap from InputStream failed") - val rotatedBitmap = decodedBitmap.rotateToMetadataOrientation(input).getOrThrow() + val rotatedBitmap = decodedBitmap.rotateToMetadataOrientation(orientation) if (resizeMode is ResizeMode.Strict) { rotatedBitmap.resizeToMax(resizeMode.maxWidth, resizeMode.maxHeight) } else { From 3a9a3f9e83699236ecae6621a142fc00edf046b3 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Thu, 31 Aug 2023 12:08:21 +0200 Subject: [PATCH 6/7] Focus on question field when opening screen. (#1194) --- .../poll/impl/create/CreatePollView.kt | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt index 3e3ec4eb16..ceb9513545 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt @@ -25,12 +25,19 @@ import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.features.poll.impl.R @@ -68,6 +75,10 @@ fun CreatePollView( onSubmitClicked = { state.eventSink(CreatePollEvents.NavBack) }, onDismiss = { state.eventSink(CreatePollEvents.HideConfirmation) } ) + val questionFocusRequester = remember { FocusRequester() } + LaunchedEffect(Unit) { + questionFocusRequester.requestFocus() + } Scaffold( modifier = modifier, topBar = { @@ -113,10 +124,13 @@ fun CreatePollView( onValueChange = { state.eventSink(CreatePollEvents.SetQuestion(it)) }, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .focusRequester(questionFocusRequester) + .fillMaxWidth(), placeholder = { Text(text = stringResource(id = R.string.screen_create_poll_question_hint)) }, + keyboardOptions = keyboardOptions, ) } ) @@ -133,6 +147,7 @@ fun CreatePollView( placeholder = { Text(text = stringResource(id = R.string.screen_create_poll_answer_hint, index + 1)) }, + keyboardOptions = keyboardOptions, ) }, trailingContent = ListItemContent.Custom { @@ -185,3 +200,8 @@ internal fun CreatePollViewPreview( state = state, ) } + +private val keyboardOptions = KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.Sentences, + imeAction = ImeAction.Next, +) From e7cf36e2f92d8a016953fff5201756afabe89db7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 31 Aug 2023 12:35:31 +0200 Subject: [PATCH 7/7] Update dagger to v2.48 (#1193) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0d52d1e0df..863d4812b8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -48,7 +48,7 @@ sqldelight = "1.5.5" telephoto = "0.6.0-SNAPSHOT" # DI -dagger = "2.47" +dagger = "2.48" anvil = "2.4.7-1-8" # Auto service