Merge branch 'release/25.03.4'

This commit is contained in:
Jorge Martín 2025-03-27 11:26:00 +01:00
commit 612749bbcc
12 changed files with 155 additions and 85 deletions

View file

@ -1,3 +1,34 @@
Changes in Element X v25.03.3
=============================
<!-- Release notes generated using configuration in .github/release.yml at v25.03.3 -->
## What's Changed
### ✨ Features
* Add 'unencrypted room' badges and labels by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4445
* Use embedded version of Element Call by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4470
### 🐛 Bugfixes
* Fix 'unverified session' flow displayed when creating account by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4467
### 🗣 Translations
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4461
### 🧱 Build
* Let element enterprise be able to configure id for mapTiler. by @bmarty in https://github.com/element-hq/element-x-android/pull/4446
### Dependency upgrades
* chore(deps): update rnkdsh/action-upload-diawi action to v1.5.8 by @renovate in https://github.com/element-hq/element-x-android/pull/4457
* chore(deps): update plugin licensee to v1.13.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4447
* fix(deps): update dependency org.maplibre.gl:android-sdk to v11.8.4 by @renovate in https://github.com/element-hq/element-x-android/pull/4450
* fix(deps): update dependency com.google.firebase:firebase-bom to v33.11.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4448
* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.3.24 by @renovate in https://github.com/element-hq/element-x-android/pull/4394
* fix(deps): update dependencyanalysis to v2.13.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4464
* chore(deps): update plugin sonarqube to v6.1.0.5360 by @renovate in https://github.com/element-hq/element-x-android/pull/4468
* fix(deps): update android.gradle.plugin to v8.9.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4465
### Others
* Sync Strings - tweaks to identity change messages by @andybalaam in https://github.com/element-hq/element-x-android/pull/4454
* Check link click by @bmarty in https://github.com/element-hq/element-x-android/pull/4463
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.03.2...v25.03.3
Changes in Element X v25.03.2
=============================

View file

@ -0,0 +1,2 @@
Main changes in this version: added a fix for Element Call not being able to report issues.
Full changelog: https://github.com/element-hq/element-x-android/releases

View file

@ -23,6 +23,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.media3.common.MimeTypes
import androidx.media3.common.util.UnstableApi
import dagger.assisted.Assisted
@ -84,11 +85,13 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import timber.log.Timber
import kotlin.time.Duration.Companion.seconds
@ -135,7 +138,6 @@ class MessageComposerPresenter @AssistedInject constructor(
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal var showTextFormatting: Boolean by mutableStateOf(false)
@OptIn(FlowPreview::class)
@SuppressLint("UnsafeOptInUsageError")
@Composable
override fun present(): MessageComposerState {
@ -148,12 +150,6 @@ class MessageComposerPresenter @AssistedInject constructor(
richTextEditorState.isReadyToProcessActions = true
}
val markdownTextEditorState = rememberMarkdownTextEditorState(initialText = null, initialFocus = false)
var isMentionsEnabled by remember { mutableStateOf(false) }
var isRoomAliasSuggestionsEnabled by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
isMentionsEnabled = featureFlagService.isFeatureEnabled(FeatureFlags.Mentions)
isRoomAliasSuggestionsEnabled = featureFlagService.isFeatureEnabled(FeatureFlags.RoomAliasSuggestions)
}
val cameraPermissionState = cameraPermissionPresenter.present()
@ -187,8 +183,6 @@ class MessageComposerPresenter @AssistedInject constructor(
val sendTypingNotifications by sessionPreferencesStore.isSendTypingNotificationsEnabled().collectAsState(initial = true)
val roomAliasSuggestions by roomAliasSuggestionsDataSource.getAllRoomAliasSuggestions().collectAsState(initial = emptyList())
LaunchedEffect(cameraPermissionState.permissionGranted) {
if (cameraPermissionState.permissionGranted) {
when (pendingEvent) {
@ -201,35 +195,7 @@ class MessageComposerPresenter @AssistedInject constructor(
}
val suggestions = remember { mutableStateListOf<ResolvedSuggestion>() }
LaunchedEffect(isMentionsEnabled) {
if (!isMentionsEnabled) return@LaunchedEffect
val currentUserId = room.sessionId
suspend fun canSendRoomMention(): Boolean {
val userCanSendAtRoom = room.canUserTriggerRoomNotification(currentUserId).getOrDefault(false)
return !room.isDm() && userCanSendAtRoom
}
// This will trigger a search immediately when `@` is typed
val mentionStartTrigger = suggestionSearchTrigger.filter { it?.text.isNullOrEmpty() }
// This will start a search when the user changes the text after the `@` with a debounce to prevent too much wasted work
val mentionCompletionTrigger = suggestionSearchTrigger.debounce(0.3.seconds).filter { !it?.text.isNullOrEmpty() }
merge(mentionStartTrigger, mentionCompletionTrigger)
.combine(room.membersStateFlow) { suggestion, roomMembersState ->
suggestions.clear()
val result = suggestionsProcessor.process(
suggestion = suggestion,
roomMembersState = roomMembersState,
roomAliasSuggestions = if (isRoomAliasSuggestionsEnabled) roomAliasSuggestions else emptyList(),
currentUserId = currentUserId,
canSendRoomMention = ::canSendRoomMention,
)
if (result.isNotEmpty()) {
suggestions.addAll(result)
}
}
.collect()
}
ResolveSuggestionsEffect(suggestions)
DisposableEffect(Unit) {
// Declare that the user is not typing anymore when the composer is disposed
@ -409,6 +375,45 @@ class MessageComposerPresenter @AssistedInject constructor(
)
}
@OptIn(FlowPreview::class)
@Composable
private fun ResolveSuggestionsEffect(
suggestions: SnapshotStateList<ResolvedSuggestion>,
) {
LaunchedEffect(Unit) {
val currentUserId = room.sessionId
suspend fun canSendRoomMention(): Boolean {
val userCanSendAtRoom = room.canUserTriggerRoomNotification(currentUserId).getOrDefault(false)
return !room.isDm() && userCanSendAtRoom
}
// This will trigger a search immediately when `@` is typed
val mentionStartTrigger = suggestionSearchTrigger.filter { it?.text.isNullOrEmpty() }
// This will start a search when the user changes the text after the `@` with a debounce to prevent too much wasted work
val mentionCompletionTrigger = suggestionSearchTrigger.debounce(0.3.seconds).filter { !it?.text.isNullOrEmpty() }
val mentionTriggerFlow = merge(mentionStartTrigger, mentionCompletionTrigger)
val roomAliasSuggestionsFlow = roomAliasSuggestionsDataSource
.getAllRoomAliasSuggestions()
.stateIn(this, SharingStarted.Lazily, emptyList())
combine(mentionTriggerFlow, room.membersStateFlow, roomAliasSuggestionsFlow) { suggestion, roomMembersState, roomAliasSuggestions ->
val result = suggestionsProcessor.process(
suggestion = suggestion,
roomMembersState = roomMembersState,
roomAliasSuggestions = roomAliasSuggestions,
currentUserId = currentUserId,
canSendRoomMention = ::canSendRoomMention,
)
suggestions.clear()
suggestions.addAll(result)
}
.collect()
}
}
private fun CoroutineScope.sendMessage(
markdownTextEditorState: MarkdownTextEditorState,
richTextEditorState: RichTextEditorState,

View file

@ -53,7 +53,11 @@ class SuggestionsProcessor @Inject constructor() {
}
SuggestionType.Room -> {
roomAliasSuggestions
.filter { it.roomAlias.value.contains(suggestion.text, ignoreCase = true) }
.filter { roomAliasSuggestion ->
// Filter by either room alias or room name (if available)
roomAliasSuggestion.roomAlias.value.contains(suggestion.text, ignoreCase = true) ||
roomAliasSuggestion.roomName?.contains(suggestion.text, ignoreCase = true) == true
}
.map {
ResolvedSuggestion.Alias(
roomAlias = it.roomAlias,

View file

@ -61,7 +61,6 @@ import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.A_USER_ID_3
import io.element.android.libraries.matrix.test.A_USER_ID_4
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
@ -1011,16 +1010,10 @@ class MessageComposerPresenterTest {
)
givenRoomInfo(aRoomInfo(isDirect = false))
}
val flagsService = FakeFeatureFlagService(
mapOf(
FeatureFlags.Mentions.key to true,
)
)
val presenter = createPresenter(this, room, featureFlagService = flagsService)
val presenter = createPresenter(this, room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
// A null suggestion (no suggestion was received) returns nothing
@ -1078,16 +1071,10 @@ class MessageComposerPresenterTest {
)
)
}
val flagsService = FakeFeatureFlagService(
mapOf(
FeatureFlags.Mentions.key to true,
)
)
val presenter = createPresenter(this, room, featureFlagService = flagsService)
val presenter = createPresenter(this, room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
// An empty suggestion returns the joined members that are not the current user, but not the room
@ -1293,11 +1280,11 @@ class MessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitFirstItem().also { state ->
skipItems(2)
awaitItem().also { state ->
assertThat(state.textEditorState.messageMarkdown(permalinkBuilder)).isEqualTo(A_MESSAGE)
assertThat(state.textEditorState.messageHtml()).isNull()
}
assert(loadDraftLambda)
.isCalledOnce()
.with(value(A_ROOM_ID), value(false))
@ -1327,7 +1314,8 @@ class MessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitFirstItem().also { state ->
skipItems(1)
awaitItem().also { state ->
assertThat(state.showTextFormatting).isTrue()
assertThat(state.textEditorState.messageMarkdown(permalinkBuilder)).isEqualTo(A_MESSAGE)
assertThat(state.textEditorState.messageHtml()).isEqualTo(A_MESSAGE)
@ -1360,7 +1348,8 @@ class MessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitFirstItem().also { state ->
skipItems(2)
awaitItem().also { state ->
assertThat(state.showTextFormatting).isFalse()
assertThat(state.mode).isEqualTo(anEditMode())
assertThat(state.textEditorState.messageMarkdown(permalinkBuilder)).isEqualTo(A_MESSAGE)
@ -1406,7 +1395,8 @@ class MessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitFirstItem().also { state ->
skipItems(2)
awaitItem().also { state ->
assertThat(state.showTextFormatting).isFalse()
assertThat(state.mode).isEqualTo(aReplyMode())
assertThat(state.textEditorState.messageMarkdown(permalinkBuilder)).isEqualTo(A_MESSAGE)
@ -1580,8 +1570,7 @@ class MessageComposerPresenterTest {
}
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
// Skip 2 item if Mentions feature is enabled, else 1
skipItems(if (FeatureFlags.Mentions.defaultValue(aBuildMeta())) 2 else 1)
skipItems(1)
return awaitItem()
}
}

View file

@ -133,7 +133,7 @@ class SuggestionsProcessorTest {
}
@Test
fun `processing Room suggestion with aliases will return a suggestion`() = runTest {
fun `processing Room suggestion with aliases will return a suggestion when matching on alias`() = runTest {
val aRoomSummary = aRoomSummary(canonicalAlias = A_ROOM_ALIAS)
val result = suggestionsProcessor.process(
suggestion = aRoomSuggestion("ali"),
@ -171,7 +171,56 @@ class SuggestionsProcessorTest {
RoomAliasSuggestion(
roomAlias = A_ROOM_ALIAS,
roomId = aRoomSummary.roomId,
roomName = aRoomSummary.info.name,
roomName = "Element",
roomAvatarUrl = aRoomSummary.info.avatarUrl,
)
),
currentUserId = A_USER_ID,
canSendRoomMention = { true },
)
assertThat(result).isEmpty()
}
@Test
fun `processing Room suggestion will return a suggestion when matching on room name`() = runTest {
val aRoomSummary = aRoomSummary(canonicalAlias = A_ROOM_ALIAS)
val result = suggestionsProcessor.process(
suggestion = aRoomSuggestion("lement"),
roomMembersState = MatrixRoomMembersState.Ready(persistentListOf()),
roomAliasSuggestions = listOf(
RoomAliasSuggestion(
roomAlias = A_ROOM_ALIAS,
roomId = aRoomSummary.roomId,
roomName = "Element",
roomAvatarUrl = aRoomSummary.info.avatarUrl,
)
),
currentUserId = A_USER_ID,
canSendRoomMention = { true },
)
assertThat(result).isEqualTo(
listOf(
ResolvedSuggestion.Alias(
roomAlias = A_ROOM_ALIAS,
roomId = aRoomSummary.roomId,
roomName = "Element",
roomAvatarUrl = aRoomSummary.info.avatarUrl,
)
)
)
}
@Test
fun `processing Room suggestion will not return a suggestion when room has no name`() = runTest {
val aRoomSummary = aRoomSummary(canonicalAlias = A_ROOM_ALIAS)
val result = suggestionsProcessor.process(
suggestion = aRoomSuggestion("lement"),
roomMembersState = MatrixRoomMembersState.Ready(persistentListOf()),
roomAliasSuggestions = listOf(
RoomAliasSuggestion(
roomAlias = A_ROOM_ALIAS,
roomId = aRoomSummary.roomId,
roomName = null,
roomAvatarUrl = aRoomSummary.info.avatarUrl,
)
),

View file

@ -80,7 +80,7 @@ class DeveloperSettingsPresenterTest {
presenter.test {
skipItems(2)
awaitItem().also { state ->
val feature = state.features.first()
val feature = state.features.first { !it.isEnabled }
state.eventSink(DeveloperSettingsEvents.UpdateEnabledFeature(feature, !feature.isEnabled))
}
awaitItem().also { state ->

View file

@ -30,7 +30,6 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationS
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
import io.element.android.libraries.matrix.api.verification.VerificationRequest
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@ -57,7 +56,7 @@ class IncomingVerificationPresenter @AssistedInject constructor(
@Composable
override fun present(): IncomingVerificationState {
val coroutineScope = rememberCoroutineScope { Dispatchers.IO }
val coroutineScope = rememberCoroutineScope()
val stateAndDispatch = stateMachine.rememberStateAndDispatch()

View file

@ -131,6 +131,9 @@ class IncomingVerificationPresenterTest {
isWaiting = false,
)
)
advanceTimeBy(1.seconds)
resetLambda.assertions().isCalledOnce().with(value(false))
acknowledgeVerificationRequestLambda.assertions().isCalledOnce().with(value(anIncomingSessionVerificationRequest))
acceptVerificationRequestLambda.assertions().isNeverCalled()
@ -139,7 +142,9 @@ class IncomingVerificationPresenterTest {
skipItems(1)
val initialWaitingState = awaitItem()
assertThat((initialWaitingState.step as IncomingVerificationState.Step.Initial).isWaiting).isTrue()
advanceUntilIdle()
advanceTimeBy(1.seconds)
acceptVerificationRequestLambda.assertions().isCalledOnce()
// Remote sent the data
fakeSessionVerificationService.emitVerificationFlowState(VerificationFlowState.DidAcceptVerificationRequest)

View file

@ -50,10 +50,10 @@ wysiwyg = "2.38.3"
telephoto = "0.15.1"
# Dependency analysis
dependencyAnalysis = "2.13.1"
dependencyAnalysis = "2.13.2"
# DI
dagger = "2.56"
dagger = "2.56.1"
anvil = "0.4.1"
# Auto service
@ -212,7 +212,7 @@ anvil_compiler_api = { module = "dev.zacsweers.anvil:compiler-api", version.ref
anvil_compiler_utils = { module = "dev.zacsweers.anvil:compiler-utils", version.ref = "anvil" }
# Element Call
element_call_embedded = "io.element.android:element-call-embedded:0.9.0-rc.2"
element_call_embedded = "io.element.android:element-call-embedded:0.9.0-rc.4"
# Auto services
google_autoservice = { module = "com.google.auto.service:auto-service", version.ref = "autoservice" }

View file

@ -54,20 +54,6 @@ enum class FeatureFlags(
defaultValue = { true },
isFinished = true,
),
Mentions(
key = "feature.mentions",
title = "Mentions",
description = "Type `@` to get mention suggestions and insert them",
defaultValue = { true },
isFinished = false,
),
RoomAliasSuggestions(
key = "feature.roomAliasSuggestions",
title = "Room alias suggestions",
description = "Type `#` to get room alias suggestions and insert them",
defaultValue = { false },
isFinished = false,
),
MarkAsUnread(
key = "feature.markAsUnread",
title = "Mark as unread",

View file

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