Merge branch 'release/25.06.2' into main

This commit is contained in:
ganfra 2025-06-10 20:13:07 +02:00
commit 57ff0b49a7
26 changed files with 158 additions and 54 deletions

View file

@ -1,3 +1,45 @@
## What's Changed
### ✨ Features
* Enable support for Android Auto. by @bmarty in https://github.com/element-hq/element-x-android/pull/4818
* Element Call: Add audio output selector handled by Android by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4663
### 🙌 Improvements
* Oidc: Fallback to external browser instead of using Webview by @bmarty in https://github.com/element-hq/element-x-android/pull/4808
* change (room member moderation) : update icon to match figma by @ganfra in https://github.com/element-hq/element-x-android/pull/4837
### 🐛 Bugfixes
* Fix login flow by @bmarty in https://github.com/element-hq/element-x-android/pull/4813
* fix: When sending media as files use the `octet-stream` type by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4815
* fix: Make `Client.findDM` return a `Result` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4816
* Mark room as fully read when user goes back to the room list. by @bmarty in https://github.com/element-hq/element-x-android/pull/2687
* fix (identity change) : RoomMemberIdentityStateChange in non encrypted room by @ganfra in https://github.com/element-hq/element-x-android/pull/4824
* Fix room and user avatar downloaded with a `.bin` extension. by @bmarty in https://github.com/element-hq/element-x-android/pull/4830
* Log the push resolving failure reason if available by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4835
### 🧱 Build
* Update Gradle Wrapper from 8.14.1 to 8.14.2 by @ElementBot in https://github.com/element-hq/element-x-android/pull/4831
### Dependency upgrades
* fix(deps): update dependency androidx.compose:compose-bom to v2025.04.01 by @renovate in https://github.com/element-hq/element-x-android/pull/4631
* fix(deps): update dependency androidx.compose:compose-bom to v2025.05.01 by @renovate in https://github.com/element-hq/element-x-android/pull/4814
* fix(deps): update dependency io.sentry:sentry-android to v8.13.2 by @renovate in https://github.com/element-hq/element-x-android/pull/4780
* fix(deps): update appyx to v1.7.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4672
* fix(deps): update telephoto to v0.16.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4749
* fix(deps): update coil to v3.2.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4712
* fix(deps): update dependency androidx.webkit:webkit to v1.14.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4823
* fix(deps): update dependency com.posthog:posthog-android to v3.17.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4827
* fix(deps): update dependency io.element.android:element-call-embedded to v0.12.2 by @renovate in https://github.com/element-hq/element-x-android/pull/4832
* fix(deps): update dependency com.google.firebase:firebase-bom to v33.15.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4833
* fix(deps): update dependency org.maplibre.gl:android-sdk to v11.10.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4825
* fix(deps): update lifecycle to v2.9.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4822
* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.6.6 by @renovate in https://github.com/element-hq/element-x-android/pull/4834
* fix(deps): update dependency io.element.android:opusencoder to v1.2.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4836
### Others
* Add `catchingExceptions` method to replace `runCatching` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4797
* Rename classes overriding classes from the FFI layer. by @bmarty in https://github.com/element-hq/element-x-android/pull/4817
* Fix coroutine scope by @bmarty in https://github.com/element-hq/element-x-android/pull/4820
* Add extra logs the 'send call notification' flow by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4819
* misc (matrix) : use innerClient.subscribeToRoomInfo sdk method by @ganfra in https://github.com/element-hq/element-x-android/pull/4838
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.06.0...v25.06.1
Changes in Element X v25.06.0 Changes in Element X v25.06.0
============================= =============================

View file

@ -122,6 +122,17 @@
<data android:scheme="https" /> <data android:scheme="https" />
<data android:host="matrix.to" /> <data android:host="matrix.to" />
</intent-filter> </intent-filter>
<!--
matrix: links
-->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="matrix" />
</intent-filter>
<!-- <!--
links from matrix.to website links from matrix.to website
--> -->

View file

@ -0,0 +1,2 @@
Main changes in this version: bug fixes and improvements.
Full changelog: https://github.com/element-hq/element-x-android/releases

View file

@ -15,9 +15,11 @@ import android.os.Build
import android.os.PowerManager import android.os.PowerManager
import android.webkit.JavascriptInterface import android.webkit.JavascriptInterface
import android.webkit.WebView import android.webkit.WebView
import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient import kotlinx.serialization.Transient
@ -25,6 +27,7 @@ import kotlinx.serialization.json.Json
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import kotlin.time.Duration.Companion.milliseconds
/** /**
* This class manages the audio devices for a WebView. * This class manages the audio devices for a WebView.
@ -66,23 +69,26 @@ class WebViewAudioManager(
/** /**
* This listener tracks the current communication device and updates the WebView when it changes. * This listener tracks the current communication device and updates the WebView when it changes.
*/ */
private val commsDeviceChangedListener = AudioManager.OnCommunicationDeviceChangedListener { device -> @get:RequiresApi(Build.VERSION_CODES.S)
if (device != null && device.id == expectedNewCommunicationDeviceId) { private val commsDeviceChangedListener by lazy {
expectedNewCommunicationDeviceId = null AudioManager.OnCommunicationDeviceChangedListener { device ->
Timber.d("Audio device changed, type: ${device.type}") if (device != null && device.id == expectedNewCommunicationDeviceId) {
updateSelectedAudioDeviceInWebView(device.id.toString())
} else if (device != null && device.id != expectedNewCommunicationDeviceId) {
// We were expecting a device change but it didn't happen, so we should retry
val expectedDeviceId = expectedNewCommunicationDeviceId
if (expectedDeviceId != null) {
// Remove the expected id so we only retry once
expectedNewCommunicationDeviceId = null expectedNewCommunicationDeviceId = null
audioManager.selectAudioDevice(expectedDeviceId.toString()) Timber.d("Audio device changed, type: ${device.type}")
updateSelectedAudioDeviceInWebView(device.id.toString())
} else if (device != null && device.id != expectedNewCommunicationDeviceId) {
// We were expecting a device change but it didn't happen, so we should retry
val expectedDeviceId = expectedNewCommunicationDeviceId
if (expectedDeviceId != null) {
// Remove the expected id so we only retry once
expectedNewCommunicationDeviceId = null
audioManager.selectAudioDevice(expectedDeviceId.toString())
}
} else {
Timber.d("Audio device cleared")
expectedNewCommunicationDeviceId = null
audioManager.selectAudioDevice(null)
} }
} else {
Timber.d("Audio device cleared")
expectedNewCommunicationDeviceId = null
audioManager.selectAudioDevice(null)
} }
} }
@ -217,6 +223,10 @@ class WebViewAudioManager(
}, },
onAudioPlaybackStarted = { onAudioPlaybackStarted = {
coroutineScope.launch(Dispatchers.Main) { coroutineScope.launch(Dispatchers.Main) {
// Even with the callback, it seems like starting the audio takes a bit on the webview side,
// so we add an extra delay here to make sure it's ready
delay(500.milliseconds)
// Calling this ahead of time makes the default audio device to not use the right audio stream // Calling this ahead of time makes the default audio device to not use the right audio stream
setAvailableAudioDevices() setAvailableAudioDevices()

View file

@ -83,7 +83,7 @@ import kotlin.time.Duration.Companion.seconds
@Test @Test
fun `present - with CallType RoomCall sets call as active, loads URL, runs WidgetDriver and notifies the other clients a call started`() = runTest { fun `present - with CallType RoomCall sets call as active, loads URL, runs WidgetDriver and notifies the other clients a call started`() = runTest {
val sendCallNotificationIfNeededLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) } val sendCallNotificationIfNeededLambda = lambdaRecorder<Result<Boolean>> { Result.success(true) }
val syncService = FakeSyncService(SyncState.Running) val syncService = FakeSyncService(SyncState.Running)
val fakeRoom = FakeJoinedRoom(sendCallNotificationIfNeededResult = sendCallNotificationIfNeededLambda) val fakeRoom = FakeJoinedRoom(sendCallNotificationIfNeededResult = sendCallNotificationIfNeededLambda)
val client = FakeMatrixClient(syncService = syncService).apply { val client = FakeMatrixClient(syncService = syncService).apply {

View file

@ -10,7 +10,10 @@
package io.element.android.features.messages.impl package io.element.android.features.messages.impl
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SheetValue import androidx.compose.material3.SheetValue
import androidx.compose.material3.rememberBottomSheetScaffoldState import androidx.compose.material3.rememberBottomSheetScaffoldState
@ -112,7 +115,7 @@ internal fun ExpandableBottomSheetScaffold(
} }
SubcomposeLayout( SubcomposeLayout(
modifier = modifier, modifier = modifier.windowInsetsPadding(WindowInsets.ime),
measurePolicy = { constraints: Constraints -> measurePolicy = { constraints: Constraints ->
val sheetContentSub = subcompose(Slot.SheetContent(sheetContentKey)) { sheetContent(true) }.map { val sheetContentSub = subcompose(Slot.SheetContent(sheetContentKey)) { sheetContent(true) }.map {
it.measure(Constraints(maxWidth = constraints.maxWidth)) it.measure(Constraints(maxWidth = constraints.maxWidth))
@ -123,7 +126,7 @@ internal fun ExpandableBottomSheetScaffold(
val dragHandleHeight = dragHandleSub?.height?.toDp() ?: 0.dp val dragHandleHeight = dragHandleSub?.height?.toDp() ?: 0.dp
val maxHeight = constraints.maxHeight.toDp() val maxHeight = constraints.maxHeight.toDp()
val contentHeight = sheetContentSub.height.toDp() + dragHandleHeight val contentHeight = sheetContentSub.measuredHeight.toDp() + dragHandleHeight
contentOverflows = contentHeight > maxHeight contentOverflows = contentHeight > maxHeight
@ -140,7 +143,7 @@ internal fun ExpandableBottomSheetScaffold(
measurePolicy = { measurables, constraints -> measurePolicy = { measurables, constraints ->
val constraintHeight = constraints.maxHeight val constraintHeight = constraints.maxHeight
val offset = tryOrNull { scaffoldState.bottomSheetState.requireOffset() } ?: 0f val offset = tryOrNull { scaffoldState.bottomSheetState.requireOffset() } ?: 0f
val height = Integer.max(0, constraintHeight - offset.roundToInt()) val height = Integer.max(peekHeight.roundToPx(), constraintHeight - offset.roundToInt())
val top = measurables[0].measure( val top = measurables[0].measure(
constraints.copy( constraints.copy(
minHeight = height, minHeight = height,

View file

@ -172,7 +172,7 @@ jsoup = "org.jsoup:jsoup:1.20.1"
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
molecule-runtime = "app.cash.molecule:molecule-runtime:2.1.0" molecule-runtime = "app.cash.molecule:molecule-runtime:2.1.0"
timber = "com.jakewharton.timber:timber:5.0.1" timber = "com.jakewharton.timber:timber:5.0.1"
matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.6.6" matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.6.10"
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" } matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" } matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" } sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }
@ -237,7 +237,7 @@ anvil = { id = "dev.zacsweers.anvil", version.ref = "anvil" }
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
ktlint = "org.jlleitschuh.gradle.ktlint:12.3.0" ktlint = "org.jlleitschuh.gradle.ktlint:12.3.0"
dependencygraph = "com.savvasdalkitsis.module-dependency-graph:0.12" dependencygraph = "com.savvasdalkitsis.module-dependency-graph:0.12"
dependencycheck = "org.owasp.dependencycheck:12.1.1" dependencycheck = "org.owasp.dependencycheck:12.1.2"
dependencyanalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyAnalysis" } dependencyanalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyAnalysis" }
paparazzi = "app.cash.paparazzi:1.3.5" paparazzi = "app.cash.paparazzi:1.3.5"
sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }

View file

@ -27,6 +27,11 @@ fun View.showKeyboard(andRequestFocus: Boolean = false) {
imm?.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) imm?.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)
} }
fun View.isKeyboardVisible(): Boolean {
val imm = context?.getSystemService<InputMethodManager>()
return imm?.isAcceptingText == true
}
suspend fun View.awaitWindowFocus() = suspendCancellableCoroutine { continuation -> suspend fun View.awaitWindowFocus() = suspendCancellableCoroutine { continuation ->
if (hasWindowFocus()) { if (hasWindowFocus()) {
continuation.resume(Unit) continuation.resume(Unit)

View file

@ -12,6 +12,7 @@ package io.element.android.libraries.matrix.api.permalink
* element-based domains (e.g. https://app.element.io/#/user/@chagai95:matrix.org) permalinks * element-based domains (e.g. https://app.element.io/#/user/@chagai95:matrix.org) permalinks
* or matrix.to permalinks (e.g. https://matrix.to/#/@chagai95:matrix.org) * or matrix.to permalinks (e.g. https://matrix.to/#/@chagai95:matrix.org)
* or client permalinks (e.g. <clientPermalinkBaseUrl>user/@chagai95:matrix.org) * or client permalinks (e.g. <clientPermalinkBaseUrl>user/@chagai95:matrix.org)
* or matrix: permalinks (e.g. matrix:u/chagai95:matrix.org)
*/ */
interface PermalinkParser { interface PermalinkParser {
/** /**

View file

@ -159,7 +159,7 @@ interface JoinedRoom : BaseRoom {
/** /**
* Send an Element Call started notification if needed. * Send an Element Call started notification if needed.
*/ */
suspend fun sendCallNotificationIfNeeded(): Result<Unit> suspend fun sendCallNotificationIfNeeded(): Result<Boolean>
suspend fun setSendQueueEnabled(enabled: Boolean) suspend fun setSendQueueEnabled(enabled: Boolean)

View file

@ -29,6 +29,7 @@ import javax.inject.Inject
* element-based domains (e.g. https://app.element.io/#/user/@chagai95:matrix.org) permalinks * element-based domains (e.g. https://app.element.io/#/user/@chagai95:matrix.org) permalinks
* or matrix.to permalinks (e.g. https://matrix.to/#/@chagai95:matrix.org) * or matrix.to permalinks (e.g. https://matrix.to/#/@chagai95:matrix.org)
* or client permalinks (e.g. <clientPermalinkBaseUrl>user/@chagai95:matrix.org) * or client permalinks (e.g. <clientPermalinkBaseUrl>user/@chagai95:matrix.org)
* or matrix: permalinks (e.g. matrix:u/chagai95:matrix.org)
*/ */
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultPermalinkParser @Inject constructor( class DefaultPermalinkParser @Inject constructor(
@ -40,10 +41,15 @@ class DefaultPermalinkParser @Inject constructor(
*/ */
override fun parse(uriString: String): PermalinkData { override fun parse(uriString: String): PermalinkData {
val uri = uriString.toUri() val uri = uriString.toUri()
// the client or element-based domain permalinks (e.g. https://app.element.io/#/user/@chagai95:matrix.org) don't have the val matrixToUri = if (uri.scheme == "matrix") {
// mxid in the first param (like matrix.to does - https://matrix.to/#/@chagai95:matrix.org) but rather in the second after /user/ so /user/mxid // take matrix: URI as is to [parseMatrixEntityFrom]
// so convert URI to matrix.to to simplify parsing process uri
val matrixToUri = matrixToConverter.convert(uri) ?: return PermalinkData.FallbackLink(uri) } else {
// the client or element-based domain permalinks (e.g. https://app.element.io/#/user/@chagai95:matrix.org) don't have the
// mxid in the first param (like matrix.to does - https://matrix.to/#/@chagai95:matrix.org) but rather in the second after /user/ so /user/mxid
// so convert URI to matrix.to to simplify parsing process
matrixToConverter.convert(uri) ?: return PermalinkData.FallbackLink(uri)
}
val result = runCatchingExceptions { val result = runCatchingExceptions {
parseMatrixEntityFrom(matrixToUri.toString()) parseMatrixEntityFrom(matrixToUri.toString())

View file

@ -161,14 +161,16 @@ class JoinedRustRoom(
maxEventsToLoad = 100u, maxEventsToLoad = 100u,
maxConcurrentRequests = 10u, maxConcurrentRequests = 10u,
) )
is CreateTimelineParams.MediaOnly -> TimelineFocus.Live is CreateTimelineParams.MediaOnly -> TimelineFocus.Live(hideThreadedEvents = false)
is CreateTimelineParams.Focused -> TimelineFocus.Event( is CreateTimelineParams.Focused -> TimelineFocus.Event(
eventId = createTimelineParams.focusedEventId.value, eventId = createTimelineParams.focusedEventId.value,
numContextEvents = 50u, numContextEvents = 50u,
hideThreadedEvents = false,
) )
is CreateTimelineParams.MediaOnlyFocused -> TimelineFocus.Event( is CreateTimelineParams.MediaOnlyFocused -> TimelineFocus.Event(
eventId = createTimelineParams.focusedEventId.value, eventId = createTimelineParams.focusedEventId.value,
numContextEvents = 50u, numContextEvents = 50u,
hideThreadedEvents = false,
) )
} }
@ -427,7 +429,7 @@ class JoinedRustRoom(
} }
} }
override suspend fun sendCallNotificationIfNeeded(): Result<Unit> = withContext(roomDispatcher) { override suspend fun sendCallNotificationIfNeeded(): Result<Boolean> = withContext(roomDispatcher) {
runCatchingExceptions { runCatchingExceptions {
innerRoom.sendCallNotificationIfNeeded() innerRoom.sendCallNotificationIfNeeded()
} }

View file

@ -110,7 +110,7 @@ class RustRoomFactory(
// Init the live timeline in the SDK from the Room // Init the live timeline in the SDK from the Room
val timeline = sdkRoom.timelineWithConfiguration( val timeline = sdkRoom.timelineWithConfiguration(
TimelineConfiguration( TimelineConfiguration(
focus = TimelineFocus.Live, focus = TimelineFocus.Live(hideThreadedEvents = false),
filter = eventFilters?.let(TimelineFilter::EventTypeFilter) ?: TimelineFilter.All, filter = eventFilters?.let(TimelineFilter::EventTypeFilter) ?: TimelineFilter.All,
internalIdPrefix = "live", internalIdPrefix = "live",
dateDividerMode = DateDividerMode.DAILY, dateDividerMode = DateDividerMode.DAILY,

View file

@ -55,7 +55,7 @@ class FakeJoinedRoom(
private val roomNotificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), private val roomNotificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(),
private var createTimelineResult: (CreateTimelineParams) -> Result<Timeline> = { lambdaError() }, private var createTimelineResult: (CreateTimelineParams) -> Result<Timeline> = { lambdaError() },
private val editMessageLambda: (EventId, String, String?, List<IntentionalMention>) -> Result<Unit> = { _, _, _, _ -> lambdaError() }, private val editMessageLambda: (EventId, String, String?, List<IntentionalMention>) -> Result<Unit> = { _, _, _, _ -> lambdaError() },
private val sendCallNotificationIfNeededResult: () -> Result<Unit> = { lambdaError() }, private val sendCallNotificationIfNeededResult: () -> Result<Boolean> = { lambdaError() },
private val progressCallbackValues: List<Pair<Long, Long>> = emptyList(), private val progressCallbackValues: List<Pair<Long, Long>> = emptyList(),
private val generateWidgetWebViewUrlResult: (MatrixWidgetSettings, String, String?, String?) -> Result<String> = { _, _, _, _ -> lambdaError() }, private val generateWidgetWebViewUrlResult: (MatrixWidgetSettings, String, String?, String?) -> Result<String> = { _, _, _, _ -> lambdaError() },
private val getWidgetDriverResult: (MatrixWidgetSettings) -> Result<MatrixWidgetDriver> = { lambdaError() }, private val getWidgetDriverResult: (MatrixWidgetSettings) -> Result<MatrixWidgetDriver> = { lambdaError() },
@ -207,7 +207,7 @@ class FakeJoinedRoom(
return getWidgetDriverResult(widgetSettings) return getWidgetDriverResult(widgetSettings)
} }
override suspend fun sendCallNotificationIfNeeded(): Result<Unit> = simulateLongTask { override suspend fun sendCallNotificationIfNeeded(): Result<Boolean> = simulateLongTask {
sendCallNotificationIfNeededResult() sendCallNotificationIfNeededResult()
} }

View file

@ -7,6 +7,8 @@
package io.element.android.libraries.textcomposer package io.element.android.libraries.textcomposer
import android.os.Build
import android.view.WindowInsets
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -14,6 +16,7 @@ import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import io.element.android.libraries.androidutils.ui.awaitWindowFocus import io.element.android.libraries.androidutils.ui.awaitWindowFocus
import io.element.android.libraries.androidutils.ui.isKeyboardVisible
import io.element.android.libraries.androidutils.ui.showKeyboard import io.element.android.libraries.androidutils.ui.showKeyboard
/** /**
@ -40,11 +43,17 @@ internal fun <T> SoftKeyboardEffect(
// Await window focus in case returning from a dialog // Await window focus in case returning from a dialog
view.awaitWindowFocus() view.awaitWindowFocus()
// Show the keyboard, temporarily using the root view for focus if (!view.isKeyboardVisible()) {
view.showKeyboard(andRequestFocus = true) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
view.windowInsetsController?.show(WindowInsets.Type.ime())
} else {
// Show the keyboard, temporarily using the root view for focus
view.showKeyboard(andRequestFocus = true)
}
// Refocus to the correct view // Refocus to the correct view
latestOnRequestFocus() latestOnRequestFocus()
}
} }
} }
} }

View file

@ -18,12 +18,14 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeightIn import androidx.compose.foundation.layout.requiredHeightIn
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@ -35,6 +37,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@ -432,8 +435,9 @@ private fun TextFormattingLayout(
sendButton: @Composable () -> Unit, sendButton: @Composable () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val bottomPadding = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() + 8.dp }
Column( Column(
modifier = modifier.padding(vertical = 4.dp), modifier = modifier.padding(vertical = 4.dp).padding(bottom = bottomPadding),
verticalArrangement = Arrangement.spacedBy(4.dp), verticalArrangement = Arrangement.spacedBy(4.dp),
) { ) {
if (isRoomEncrypted == false) { if (isRoomEncrypted == false) {

View file

@ -8,7 +8,6 @@
package io.element.android.libraries.textcomposer.components.markdown package io.element.android.libraries.textcomposer.components.markdown
import android.content.Context import android.content.Context
import android.view.View
import androidx.appcompat.widget.AppCompatEditText import androidx.appcompat.widget.AppCompatEditText
internal class MarkdownEditText( internal class MarkdownEditText(
@ -37,8 +36,4 @@ internal class MarkdownEditText(
onSelectionChangeListener?.invoke(selStart, selEnd) onSelectionChangeListener?.invoke(selStart, selEnd)
} }
} }
override fun focusSearch(direction: Int): View? {
return null
}
} }

View file

@ -118,6 +118,11 @@ fun MarkdownTextInput(
) )
} }
state.requestFocusAction = { this.requestFocus() } state.requestFocusAction = { this.requestFocus() }
} else {
isEnabled = false
isFocusable = false
isFocusableInTouchMode = false
isClickable = false
} }
} }
}, },

View file

@ -32,7 +32,7 @@ private const val versionYear = 25
private const val versionMonth = 6 private const val versionMonth = 6
// Note: must be in [0,99] // Note: must be in [0,99]
private const val versionReleaseNumber = 1 private const val versionReleaseNumber = 2
object Versions { object Versions {
const val VERSION_CODE = (2000 + versionYear) * 10_000 + versionMonth * 100 + versionReleaseNumber const val VERSION_CODE = (2000 + versionYear) * 10_000 + versionMonth * 100 + versionReleaseNumber

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:390f7f3c699ecd843eb79adcefc64a66e6bbb6740dcd3bccf0f90c2f2b70e7e2 oid sha256:70ab6c6c738a96fd2db7901482cf4110dfcc69a6289e1db734a753751690747a
size 54255 size 51622

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:c6465d7f2e552193d51dd1701d6b4f5726cb616e35cdcd3e219ebf31c22941b9 oid sha256:237ee8f45cfd01821287669abc4fe7125c0d2828ff60224706eef855910821f0
size 53750 size 51048

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:c86b3656e5913338c2513af8a56dc725a63d4fda9c5478f429ec684203c68115 oid sha256:17f126215961643b6b7c3b41fcc5ea970923229b76bb7c918db191cb5c0ef8b9
size 64550 size 64672

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:1b491d682990192c54ebdae58045caab1ff653d3d6c89ca5f15e93bbb45c6fc0 oid sha256:f72fdf44bd35b6d62b5e75ec1bf2206429e0861da684d370259bea684eb2a76e
size 62052 size 62158

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d2bc5c13d2a1e100bdcd0142b7f03b458ba80d0d6ded82de1ebb1b6aa055a344 oid sha256:d5226d7599af16015858523607a829d9cbbc0ffa21174f9c528e725673e2e31c
size 53893 size 53982

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:fb2210aa87440b2e87217ff53448091078bdff658a6b36fad12cb1a57caf59e2 oid sha256:31b2d698e78fa11dc6af9dbc20b07533afd16c26b8f9969756057f07ea4785d0
size 51187 size 51371

9
tools/adb/deeplink_matrix.sh Executable file
View file

@ -0,0 +1,9 @@
#! /bin/bash
# Copyright 2025 New Vector Ltd.
#
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
# Please see LICENSE files in the repository root for full details.
adb shell am start -a android.intent.action.VIEW \
-d "matrix:r/element-android:matrix.org"