Merge branch 'release/26.06.1'

This commit is contained in:
Jorge Martín
2026-06-04 16:13:51 +02:00
11 changed files with 135 additions and 19 deletions
@@ -0,0 +1,2 @@
Main changes in this version: fixed an issue with devices not supporting Vulkan, added image editing before sending and an option for custom notification sounds, improved start times, several other bug fixes and improvements.
Full changelog: https://github.com/element-hq/element-x-android/releases
@@ -75,10 +75,10 @@ internal fun RoomSummaryRow(
room: RoomListRoomSummary, room: RoomListRoomSummary,
hideInviteAvatars: Boolean, hideInviteAvatars: Boolean,
isInviteSeen: Boolean, isInviteSeen: Boolean,
showUnreadCount: Boolean = false,
onClick: (RoomListRoomSummary) -> Unit, onClick: (RoomListRoomSummary) -> Unit,
eventSink: (RoomListEvent) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
showUnreadCount: Boolean = false,
eventSink: (RoomListEvent) -> Unit,
) { ) {
Box(modifier = modifier) { Box(modifier = modifier) {
when (room.displayType) { when (room.displayType) {
@@ -0,0 +1,30 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.location.api
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun RenderingMapsNotSupportedDialog(onSubmit: () -> Unit) {
ErrorDialog(
title = stringResource(CommonStrings.vulkan_not_supported_dialog_title_android),
content = stringResource(CommonStrings.vulkan_not_supported_dialog_content_android),
onSubmit = onSubmit,
)
}
@PreviewsDayNight
@Composable
internal fun RenderingMapsNotSupportedDialogPreview() = ElementPreview {
RenderingMapsNotSupportedDialog(onSubmit = {})
}
@@ -5,13 +5,21 @@
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. ~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
~ Please see LICENSE files in the repository root for full details. ~ Please see LICENSE files in the repository root for full details.
--> -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" >
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<!-- Declare Vulkan as an optional feature to prevent installation issues on devices that do not support it. -->
<uses-feature
android:name="android.hardware.vulkan.version"
android:required="false"
tools:replace="android:required" />
<application> <application>
<service <service
android:name=".live.service.LiveLocationSharingService" android:name=".live.service.LiveLocationSharingService"
@@ -11,6 +11,9 @@ package io.element.android.features.messages.impl
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.lifecycle.subscribe
@@ -29,6 +32,7 @@ import io.element.android.features.call.api.ElementCallEntryPoint
import io.element.android.features.forward.api.ForwardEntryPoint import io.element.android.features.forward.api.ForwardEntryPoint
import io.element.android.features.knockrequests.api.list.KnockRequestsListEntryPoint import io.element.android.features.knockrequests.api.list.KnockRequestsListEntryPoint
import io.element.android.features.location.api.LocationService import io.element.android.features.location.api.LocationService
import io.element.android.features.location.api.RenderingMapsNotSupportedDialog
import io.element.android.features.location.api.ShareLocationEntryPoint import io.element.android.features.location.api.ShareLocationEntryPoint
import io.element.android.features.location.api.ShowLocationEntryPoint import io.element.android.features.location.api.ShowLocationEntryPoint
import io.element.android.features.location.api.ShowLocationMode import io.element.android.features.location.api.ShowLocationMode
@@ -53,6 +57,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.duration import io.element.android.features.messages.impl.timeline.model.event.duration
import io.element.android.features.poll.api.create.CreatePollEntryPoint import io.element.android.features.poll.api.create.CreatePollEntryPoint
import io.element.android.features.poll.api.create.CreatePollMode import io.element.android.features.poll.api.create.CreatePollMode
import io.element.android.libraries.androidutils.system.DeviceHasVulkanSupport
import io.element.android.libraries.architecture.BackstackWithOverlayBox import io.element.android.libraries.architecture.BackstackWithOverlayBox
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.callback import io.element.android.libraries.architecture.callback
@@ -121,6 +126,7 @@ class MessagesFlowNode(
private val knockRequestsListEntryPoint: KnockRequestsListEntryPoint, private val knockRequestsListEntryPoint: KnockRequestsListEntryPoint,
private val dateFormatter: DateFormatter, private val dateFormatter: DateFormatter,
private val coroutineDispatchers: CoroutineDispatchers, private val coroutineDispatchers: CoroutineDispatchers,
private val hasVulkanSupport: DeviceHasVulkanSupport,
) : BaseFlowNode<MessagesFlowNode.NavTarget>( ) : BaseFlowNode<MessagesFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
initialElement = plugins.filterIsInstance<MessagesEntryPoint.Params>().first().initialTarget.toNavTarget(), initialElement = plugins.filterIsInstance<MessagesEntryPoint.Params>().first().initialTarget.toNavTarget(),
@@ -188,6 +194,8 @@ class MessagesFlowNode(
private val callback: MessagesEntryPoint.Callback = callback() private val callback: MessagesEntryPoint.Callback = callback()
private var displayVulkanNotSupportedError by mutableStateOf(false)
override fun onBuilt() { override fun onBuilt() {
super.onBuilt() super.onBuilt()
lifecycle.subscribe( lifecycle.subscribe(
@@ -267,7 +275,11 @@ class MessagesFlowNode(
} }
override fun navigateToSendLocation() { override fun navigateToSendLocation() {
backstack.push(NavTarget.SendLocation(Timeline.Mode.Live)) if (hasVulkanSupport()) {
backstack.push(NavTarget.SendLocation(Timeline.Mode.Live))
} else {
displayVulkanNotSupportedError = true
}
} }
override fun navigateToCreatePoll() { override fun navigateToCreatePoll() {
@@ -279,7 +291,11 @@ class MessagesFlowNode(
} }
override fun navigateToCurrentLiveLocation() { override fun navigateToCurrentLiveLocation() {
backstack.push(NavTarget.LocationViewer(ShowLocationMode.Live(senderId = sessionId))) if (hasVulkanSupport()) {
backstack.push(NavTarget.LocationViewer(ShowLocationMode.Live(senderId = sessionId)))
} else {
displayVulkanNotSupportedError = true
}
} }
override fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean) { override fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean) {
@@ -506,7 +522,11 @@ class MessagesFlowNode(
} }
override fun navigateToSendLocation() { override fun navigateToSendLocation() {
backstack.push(NavTarget.SendLocation(Timeline.Mode.Thread(navTarget.threadRootId))) if (hasVulkanSupport()) {
backstack.push(NavTarget.SendLocation(Timeline.Mode.Thread(navTarget.threadRootId)))
} else {
displayVulkanNotSupportedError = true
}
} }
override fun navigateToCreatePoll() { override fun navigateToCreatePoll() {
@@ -518,7 +538,11 @@ class MessagesFlowNode(
} }
override fun navigateToCurrentLiveLocation() { override fun navigateToCurrentLiveLocation() {
backstack.push(NavTarget.LocationViewer(ShowLocationMode.Live(senderId = sessionId))) if (hasVulkanSupport()) {
backstack.push(NavTarget.LocationViewer(ShowLocationMode.Live(senderId = sessionId)))
} else {
displayVulkanNotSupportedError = true
}
} }
override fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean) { override fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean) {
@@ -607,18 +631,23 @@ class MessagesFlowNode(
) )
} }
is TimelineItemLocationContent -> { is TimelineItemLocationContent -> {
val mode = when (event.content.mode) { if (hasVulkanSupport()) {
is TimelineItemLocationContent.Mode.Live -> ShowLocationMode.Live(event.senderId) val mode = when (event.content.mode) {
is TimelineItemLocationContent.Mode.Static -> ShowLocationMode.Static( is TimelineItemLocationContent.Mode.Live -> ShowLocationMode.Live(event.senderId)
location = event.content.mode.location, is TimelineItemLocationContent.Mode.Static -> ShowLocationMode.Static(
senderName = event.safeSenderName, location = event.content.mode.location,
senderId = event.senderId, senderName = event.safeSenderName,
senderAvatarUrl = event.senderAvatar.url, senderId = event.senderId,
timestamp = event.sentTimeMillis, senderAvatarUrl = event.senderAvatar.url,
assetType = event.content.assetType, timestamp = event.sentTimeMillis,
) assetType = event.content.assetType,
)
}
NavTarget.LocationViewer(mode = mode).takeIf { locationService.isServiceAvailable() }
} else {
displayVulkanNotSupportedError = true
null
} }
NavTarget.LocationViewer(mode = mode).takeIf { locationService.isServiceAvailable() }
} }
else -> null else -> null
} }
@@ -691,6 +720,11 @@ class MessagesFlowNode(
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
mentionSpanTheme.updateStyles() mentionSpanTheme.updateStyles()
if (displayVulkanNotSupportedError) {
RenderingMapsNotSupportedDialog { displayVulkanNotSupportedError = false }
}
CompositionLocalProvider( CompositionLocalProvider(
LocalMentionSpanUpdater provides mentionSpanUpdater LocalMentionSpanUpdater provides mentionSpanUpdater
) { ) {
@@ -23,6 +23,7 @@ import io.element.android.features.messages.api.MessagesEntryPoint
import io.element.android.features.messages.impl.pinned.banner.createPinnedEventsTimelineProvider import io.element.android.features.messages.impl.pinned.banner.createPinnedEventsTimelineProvider
import io.element.android.features.messages.impl.timeline.createTimelineController import io.element.android.features.messages.impl.timeline.createTimelineController
import io.element.android.features.poll.test.create.FakeCreatePollEntryPoint import io.element.android.features.poll.test.create.FakeCreatePollEntryPoint
import io.element.android.libraries.androidutils.system.DeviceHasVulkanSupport
import io.element.android.libraries.dateformatter.test.FakeDateFormatter import io.element.android.libraries.dateformatter.test.FakeDateFormatter
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
@@ -42,6 +43,7 @@ import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.node.TestParentNode import io.element.android.tests.testutils.node.TestParentNode
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import io.mockk.mockk
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@@ -85,6 +87,7 @@ class DefaultMessagesEntryPointTest {
knockRequestsListEntryPoint = FakeKnockRequestsListEntryPoint(), knockRequestsListEntryPoint = FakeKnockRequestsListEntryPoint(),
dateFormatter = FakeDateFormatter(), dateFormatter = FakeDateFormatter(),
coroutineDispatchers = testCoroutineDispatchers(), coroutineDispatchers = testCoroutineDispatchers(),
hasVulkanSupport = DeviceHasVulkanSupport(mockk(relaxed = true))
) )
} }
val callback = object : MessagesEntryPoint.Callback { val callback = object : MessagesEntryPoint.Callback {
@@ -0,0 +1,31 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.androidutils.system
import android.content.Context
import dev.zacsweers.metro.Inject
import io.element.android.libraries.di.annotations.ApplicationContext
private const val VULKAN_VERSION_1_0 = 0x400003
/**
* Checks if the device supports Vulkan 1.0.
*
* This is needed for the location screens that contain maps using MapLibre UI components.
*
* Needed until https://github.com/maplibre/maplibre-native/issues/3079 is resolved and we can automatically choose between OpenGL and Vulkan renderers,
* or no devices support OpenGL anymore.
*/
@Inject
class DeviceHasVulkanSupport(
@ApplicationContext private val context: Context,
) {
operator fun invoke(): Boolean {
return context.packageManager.hasSystemFeature("android.hardware.vulkan.version", VULKAN_VERSION_1_0)
}
}
@@ -546,4 +546,6 @@ Are you sure you want to continue?"</string>
<string name="timeline_decryption_failure_historical_event_user_not_joined">"You don\'t have access to this message"</string> <string name="timeline_decryption_failure_historical_event_user_not_joined">"You don\'t have access to this message"</string>
<string name="timeline_decryption_failure_unable_to_decrypt">"Unable to decrypt message"</string> <string name="timeline_decryption_failure_unable_to_decrypt">"Unable to decrypt message"</string>
<string name="timeline_decryption_failure_withheld_unverified">"This message was blocked either because you did not verify your device or because the sender needs to verify your digital identity."</string> <string name="timeline_decryption_failure_withheld_unverified">"This message was blocked either because you did not verify your device or because the sender needs to verify your digital identity."</string>
<string name="vulkan_not_supported_dialog_content_android">"Your device is too old, a device with Android 8 or newer is required."</string>
<string name="vulkan_not_supported_dialog_title_android">"Rendering maps is not supported"</string>
</resources> </resources>
+1 -1
View File
@@ -45,7 +45,7 @@ private const val versionMonth = 6
* Release number in the month. Value must be in [0,99]. * Release number in the month. Value must be in [0,99].
* Do not update this value. it is updated by the release script. * Do not update this value. it is updated by the release script.
*/ */
private const val versionReleaseNumber = 0 private const val versionReleaseNumber = 1
object Versions { object Versions {
/** /**