diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index ec67e56676..7bda713623 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -10,8 +10,7 @@ updates:
directory: "/"
schedule:
interval: "daily"
- ignore:
- - dependency-name: "*"
+ open-pull-requests-limit: 0
reviewers:
- "element-hq/element-x-android-reviewers"
# Updates for Gradle dependencies used in the app
@@ -19,8 +18,6 @@ updates:
directory: "/"
schedule:
interval: "daily"
- open-pull-requests-limit: 200
- ignore:
- - dependency-name: "*"
+ open-pull-requests-limit: 0
reviewers:
- "element-hq/element-x-android-reviewers"
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 1668cd2b1f..3b80134e52 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -69,7 +69,7 @@ jobs:
retention-days: 5
overwrite: true
if-no-files-found: error
- - uses: rnkdsh/action-upload-diawi@605adbad0db6c000eee26adfd8fc128d7df8f7ab # v1.5.9
+ - uses: rnkdsh/action-upload-diawi@993c2e9f871486114a171cf7fb40554716dac3c5 # v1.5.10
id: diawi
# Do not fail the whole build if Diawi upload fails
continue-on-error: true
diff --git a/CHANGES.md b/CHANGES.md
index 4b0b22eb03..1883ee0080 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,79 @@
+Changes in Element X v25.07.0
+=============================
+
+
+
+## What's Changed
+### 🙌 Improvements
+* Change : handle invalid invite error by @ganfra in https://github.com/element-hq/element-x-android/pull/4909
+* Add ability to zoom on video. by @bmarty in https://github.com/element-hq/element-x-android/pull/4916
+* Change : sync moderation and safety preferences with server by @ganfra in https://github.com/element-hq/element-x-android/pull/4962
+### 🐛 Bugfixes
+* Restore `MarkdownEditText.focusSearch` override by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4908
+* Fix duplicate usage of a `modifier` variable in `TextInputBox` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4928
+### 🗣 Translations
+* Sync Strings - new translations to Danish by @ElementBot in https://github.com/element-hq/element-x-android/pull/4913
+* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4983
+### 🧱 Build
+* a11y: Add scripts to enable and disable the talkback service by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4906
+* Update min api level to 33 for Element enterprise by @bmarty in https://github.com/element-hq/element-x-android/pull/4960
+### 🚧 In development 🚧
+* Rename module roomlist to home by @bmarty in https://github.com/element-hq/element-x-android/pull/4955
+* Home navigation bar by @bmarty in https://github.com/element-hq/element-x-android/pull/4964
+### Dependency upgrades
+* fix(deps): update dependency org.unifiedpush.android:connector to v3.0.10 by @renovate in https://github.com/element-hq/element-x-android/pull/4871
+* fix(deps): update dependency io.sentry:sentry-android to v8.14.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4892
+* fix(deps): update dependency com.google.crypto.tink:tink-android to v1.18.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4897
+* fix(deps): update wysiwyg to v2.38.4 by @renovate in https://github.com/element-hq/element-x-android/pull/4907
+* fix(deps): update dependency org.robolectric:robolectric to v4.15 by @renovate in https://github.com/element-hq/element-x-android/pull/4901
+* fix(deps): update dependency androidx.sqlite:sqlite-ktx to v2.5.2 by @renovate in https://github.com/element-hq/element-x-android/pull/4898
+* fix(deps): update dependency io.mockk:mockk to v1.14.4 by @renovate in https://github.com/element-hq/element-x-android/pull/4912
+* fix(deps): update dependency org.robolectric:robolectric to v4.15.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4911
+* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.6.23 by @renovate in https://github.com/element-hq/element-x-android/pull/4917
+* fix(deps): update dependencyanalysis to v2.19.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4932
+* fix(deps): update dependency org.jsoup:jsoup to v1.21.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4914
+* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.6.25 by @renovate in https://github.com/element-hq/element-x-android/pull/4936
+* fix(deps): update dependency io.sentry:sentry-android to v8.15.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4938
+* fix(deps): update dependency org.maplibre.gl:android-sdk to v11.11.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4939
+* fix(deps): update dependency com.google.firebase:firebase-bom to v33.16.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4945
+* fix(deps): update dependency io.sentry:sentry-android to v8.16.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4941
+* Update sdk to version 25.7.1 by @bmarty in https://github.com/element-hq/element-x-android/pull/4966
+* Update haze to v1.6.6 by @renovate in https://github.com/element-hq/element-x-android/pull/4968
+* Update dependency com.google.gms:google-services to v4.4.3 by @renovate in https://github.com/element-hq/element-x-android/pull/4946
+* Update android.gradle.plugin to v8.11.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4931
+* Update dependency io.element.android:element-call-embedded to v0.13.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4969
+* Update dependency org.matrix.rustcomponents:sdk-android to v25.7.2 by @renovate in https://github.com/element-hq/element-x-android/pull/4967
+* Upgrade compose bom to 2025.06.01 by @bmarty in https://github.com/element-hq/element-x-android/pull/4970
+* Update kotlin by @renovate in https://github.com/element-hq/element-x-android/pull/4918
+* Update dependency io.element.android:element-call-embedded to v0.13.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4977
+* Update dependency org.matrix.rustcomponents:sdk-android to v25.7.3 by @ganfra in https://github.com/element-hq/element-x-android/pull/4976
+### Others
+* a11y: Make isTalkbackActive() live. by @bmarty in https://github.com/element-hq/element-x-android/pull/4903
+* a11y: improve accessibility on grouped state events header. by @bmarty in https://github.com/element-hq/element-x-android/pull/4902
+* Room debug info by @bmarty in https://github.com/element-hq/element-x-android/pull/4904
+* [a11y] Improve accessibility of message composer by @bmarty in https://github.com/element-hq/element-x-android/pull/4900
+* refactor: Migrate SQLCipher Android to new API by @ShadowRZ in https://github.com/element-hq/element-x-android/pull/4874
+* Iterate on avatar to be able to render Space avatar. by @bmarty in https://github.com/element-hq/element-x-android/pull/4921
+* Simplify syncing the room list when receiving a push by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4915
+* Add unit test on ChooseAccountProviderState so that the coverage is above 90% by @bmarty in https://github.com/element-hq/element-x-android/pull/4924
+* Iterate on avatar to be able to render Space avatar Part2 by @bmarty in https://github.com/element-hq/element-x-android/pull/4923
+* Introduce SessionEnterpriseService. by @bmarty in https://github.com/element-hq/element-x-android/pull/4925
+* Simplify message composer layout by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4884
+* Display error dialog if Element Call can't be joined by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4919
+* misc : simplify timeline diff logic by @ganfra in https://github.com/element-hq/element-x-android/pull/4930
+* Navigation bar component by @bmarty in https://github.com/element-hq/element-x-android/pull/4940
+* a11y: improve content description of the close buttons by @bmarty in https://github.com/element-hq/element-x-android/pull/4943
+* Element Call: remove top app bar and add it inside the webview instead by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4927
+* Replace the Report a problem button with the app's version on the on boading screen. by @bmarty in https://github.com/element-hq/element-x-android/pull/4944
+* Split RoomListPresenter and introduce HomePresenter by @bmarty in https://github.com/element-hq/element-x-android/pull/4958
+* Add "View avatar" content description to all clickable Avatar that will open the avatar preview. by @bmarty in https://github.com/element-hq/element-x-android/pull/4948
+* [a11y] Ensure that the focus is not lost when the send button state change by @bmarty in https://github.com/element-hq/element-x-android/pull/4975
+* [a11y] add missing heading() qualifier on screen titles and other headers by @bmarty in https://github.com/element-hq/element-x-android/pull/4980
+* misc (tracing) : add new TraceLogPack.Notification by @ganfra in https://github.com/element-hq/element-x-android/pull/4981
+
+
+**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.06.3...v25.07.0
+
Changes in Element X v25.06.3
=============================
diff --git a/README.md b/README.md
index 20ac1c0536..95e8b219f5 100644
--- a/README.md
+++ b/README.md
@@ -8,11 +8,9 @@
# Element X Android
-Element X Android is a [Matrix](https://matrix.org/) Android Client provided by [element.io](https://element.io/).
+Element X Android is the next-generation [Matrix](https://matrix.org/) client provided by [Element](https://element.io/).
-The application is a total rewrite of [Element-Android](https://github.com/element-hq/element-android) using the [Matrix Rust SDK](https://github.com/matrix-org/matrix-rust-sdk) underneath and targeting devices running Android 7+. The UI layer is written using [Jetpack Compose](https://developer.android.com/jetpack/compose), and the navigation is managed using [Appyx](https://github.com/bumble-tech/appyx).
-
-Learn more about why we are building Element X in our blog post: [https://element.io/blog/element-x-experience-the-future-of-element/](https://element.io/blog/element-x-experience-the-future-of-element/).
+Compared to the previous-generation [Element Classic](https://github.com/element-hq/element-android), the application is a total rewrite, using the [Matrix Rust SDK](https://github.com/matrix-org/matrix-rust-sdk) underneath and targeting devices running Android 7+. The UI layer is written using [Jetpack Compose](https://developer.android.com/jetpack/compose), and the navigation is managed using [Appyx](https://github.com/bumble-tech/appyx).
[
](https://play.google.com/store/apps/details?id=io.element.android.x)[
](https://f-droid.org/packages/io.element.android.x)
@@ -72,7 +70,7 @@ We're doing this as a way to share code between platforms and while we've seen p
## Status
-This project is in an early rollout and migration phase.
+This project is actively developed and supported. New users are recommended to use Element X instead of the previous-generation app.
## Minimum SDK version
diff --git a/fastlane/metadata/android/en-US/changelogs/202507010.txt b/fastlane/metadata/android/en-US/changelogs/202507010.txt
new file mode 100644
index 0000000000..42a042fb06
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/202507010.txt
@@ -0,0 +1,2 @@
+Main changes in this version: improvements and bug fixes.
+Full changelog: https://github.com/element-hq/element-x-android/releases
\ No newline at end of file
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeNavigationBarItem.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeNavigationBarItem.kt
index 5254648f24..328661c834 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeNavigationBarItem.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeNavigationBarItem.kt
@@ -16,18 +16,18 @@ enum class HomeNavigationBarItem(
val labelRes: Int,
) {
Chats(
- labelRes = R.string.screen_roomlist_main_space_title
+ labelRes = R.string.screen_home_tab_chats
),
Spaces(
- // TODO Create a new entry in Localazy
- labelRes = R.string.screen_roomlist_main_space_title
+ labelRes = R.string.screen_home_tab_spaces
);
@Composable
- fun icon() = when (this) {
- Chats -> CompoundIcons.ChatSolid()
- // TODO Spaces -> CompoundIcons.Workspace()
- Spaces -> CompoundIcons.Code()
+ fun icon(
+ isSelected: Boolean,
+ ) = when (this) {
+ Chats -> if (isSelected) CompoundIcons.ChatSolid() else CompoundIcons.Chat()
+ Spaces -> if (isSelected) CompoundIcons.WorkspaceSolid() else CompoundIcons.Workspace()
}
companion object {
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt
index fb5b9c9ae3..d08aba7d97 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt
@@ -12,6 +12,7 @@ package io.element.android.features.home.impl
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.consumeWindowInsets
@@ -56,7 +57,9 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.NavigationBar
+import io.element.android.libraries.designsystem.theme.components.NavigationBarIcon
import io.element.android.libraries.designsystem.theme.components.NavigationBarItem
+import io.element.android.libraries.designsystem.theme.components.NavigationBarText
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
@@ -179,6 +182,15 @@ private fun HomeScaffold(
displayFilters = roomListState.displayFilters && state.currentHomeNavigationBarItem == HomeNavigationBarItem.Chats,
filtersState = roomListState.filtersState,
canReportBug = state.canReportBug,
+ modifier = if (state.isSpaceFeatureEnabled) {
+ Modifier.hazeEffect(
+ state = hazeState,
+ style = HazeMaterials.thick(),
+ )
+ } else {
+ Modifier
+ .background(ElementTheme.colors.bgCanvasDefault)
+ }
)
},
bottomBar = {
@@ -188,23 +200,25 @@ private fun HomeScaffold(
modifier = Modifier
.hazeEffect(
state = hazeState,
- style = HazeMaterials.regular(),
+ style = HazeMaterials.thick(),
)
) {
HomeNavigationBarItem.entries.forEach { item ->
+ val isSelected = state.currentHomeNavigationBarItem == item
NavigationBarItem(
- selected = state.currentHomeNavigationBarItem == item,
+ selected = isSelected,
onClick = {
state.eventSink(HomeEvents.SelectHomeNavigationBarItem(item))
},
icon = {
- Icon(
- imageVector = item.icon(),
- contentDescription = null
+ NavigationBarIcon(
+ imageVector = item.icon(isSelected),
)
},
label = {
- Text(stringResource(item.labelRes))
+ NavigationBarText(
+ text = stringResource(item.labelRes),
+ )
}
)
}
@@ -223,15 +237,18 @@ private fun HomeScaffold(
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
onRoomClick = ::onRoomClick,
onCreateRoomClick = onCreateRoomClick,
- // FAB height is 56dp, bottom padding is 16dp, we add 8dp as extra margin -> 56+16+8 = 80,
- // and include provided bottom padding
- contentBottomPadding = 80.dp + padding.calculateBottomPadding(),
+ contentPadding = PaddingValues(
+ // FAB height is 56dp, bottom padding is 16dp, we add 8dp as extra margin -> 56+16+8 = 80,
+ // and include provided bottom padding
+ bottom = 80.dp + padding.calculateBottomPadding(),
+ top = padding.calculateTopPadding()
+ ),
modifier = Modifier
.padding(
- top = padding.calculateTopPadding(),
- bottom = 0.dp,
- start = padding.calculateStartPadding(LocalLayoutDirection.current),
- end = padding.calculateEndPadding(LocalLayoutDirection.current),
+ PaddingValues(
+ start = padding.calculateStartPadding(LocalLayoutDirection.current),
+ end = padding.calculateEndPadding(LocalLayoutDirection.current),
+ )
)
.consumeWindowInsets(padding)
.hazeSource(state = hazeState)
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt
index 993fb0b85c..2845a79b8e 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt
@@ -31,7 +31,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter
-import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
@@ -67,44 +66,53 @@ fun RoomListContentView(
onConfirmRecoveryKeyClick: () -> Unit,
onRoomClick: (RoomListRoomSummary) -> Unit,
onCreateRoomClick: () -> Unit,
- contentBottomPadding: Dp,
+ contentPadding: PaddingValues,
modifier: Modifier = Modifier,
) {
- Box(modifier = modifier) {
- when (contentState) {
- is RoomListContentState.Skeleton -> {
- SkeletonView(
- count = contentState.count,
- )
- }
- is RoomListContentState.Empty -> {
- EmptyView(
- state = contentState,
- eventSink = eventSink,
- onSetUpRecoveryClick = onSetUpRecoveryClick,
- onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
- onCreateRoomClick = onCreateRoomClick,
- )
- }
- is RoomListContentState.Rooms -> {
- RoomsView(
- state = contentState,
- hideInvitesAvatars = hideInvitesAvatars,
- filtersState = filtersState,
- eventSink = eventSink,
- onSetUpRecoveryClick = onSetUpRecoveryClick,
- onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
- onRoomClick = onRoomClick,
- contentBottomPadding = contentBottomPadding,
- )
- }
+ when (contentState) {
+ is RoomListContentState.Skeleton -> {
+ SkeletonView(
+ modifier = modifier,
+ count = contentState.count,
+ contentPadding = contentPadding,
+ )
+ }
+ is RoomListContentState.Empty -> {
+ EmptyView(
+ modifier = modifier.padding(contentPadding),
+ state = contentState,
+ eventSink = eventSink,
+ onSetUpRecoveryClick = onSetUpRecoveryClick,
+ onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
+ onCreateRoomClick = onCreateRoomClick,
+ )
+ }
+ is RoomListContentState.Rooms -> {
+ RoomsView(
+ modifier = modifier,
+ state = contentState,
+ hideInvitesAvatars = hideInvitesAvatars,
+ filtersState = filtersState,
+ eventSink = eventSink,
+ onSetUpRecoveryClick = onSetUpRecoveryClick,
+ onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
+ onRoomClick = onRoomClick,
+ contentPadding = contentPadding,
+ )
}
}
}
@Composable
-private fun SkeletonView(count: Int, modifier: Modifier = Modifier) {
- LazyColumn(modifier = modifier) {
+private fun SkeletonView(
+ count: Int,
+ contentPadding: PaddingValues,
+ modifier: Modifier = Modifier,
+) {
+ LazyColumn(
+ modifier = modifier,
+ contentPadding = contentPadding,
+ ) {
repeat(count) { index ->
item {
RoomSummaryPlaceholderRow()
@@ -167,7 +175,7 @@ private fun RoomsView(
onSetUpRecoveryClick: () -> Unit,
onConfirmRecoveryKeyClick: () -> Unit,
onRoomClick: (RoomListRoomSummary) -> Unit,
- contentBottomPadding: Dp,
+ contentPadding: PaddingValues,
modifier: Modifier = Modifier,
) {
if (state.summaries.isEmpty() && filtersState.hasAnyFilterSelected) {
@@ -183,7 +191,7 @@ private fun RoomsView(
onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
onRoomClick = onRoomClick,
- contentBottomPadding = contentBottomPadding,
+ contentPadding = contentPadding,
modifier = modifier.fillMaxSize(),
)
}
@@ -197,7 +205,7 @@ private fun RoomsViewList(
onSetUpRecoveryClick: () -> Unit,
onConfirmRecoveryKeyClick: () -> Unit,
onRoomClick: (RoomListRoomSummary) -> Unit,
- contentBottomPadding: Dp,
+ contentPadding: PaddingValues,
modifier: Modifier = Modifier,
) {
val lazyListState = rememberLazyListState()
@@ -216,7 +224,7 @@ private fun RoomsViewList(
LazyColumn(
state = lazyListState,
modifier = modifier,
- contentPadding = PaddingValues(bottom = contentBottomPadding)
+ contentPadding = contentPadding,
) {
when (state.securityBannerState) {
SecurityBannerState.SetUpRecovery -> {
@@ -329,6 +337,6 @@ internal fun RoomListContentViewPreview(@PreviewParameter(RoomListContentStatePr
onConfirmRecoveryKeyClick = {},
onRoomClick = {},
onCreateRoomClick = {},
- contentBottomPadding = 0.dp,
+ contentPadding = PaddingValues(0.dp),
)
}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListTopBar.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListTopBar.kt
index 8071dead3d..f1f06afe6d 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListTopBar.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListTopBar.kt
@@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
@@ -22,7 +21,6 @@ import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@@ -30,14 +28,9 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.unit.DpOffset
-import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import io.element.android.appconfig.RoomListConfig
import io.element.android.compound.theme.ElementTheme
@@ -51,12 +44,10 @@ import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.AvatarType
-import io.element.android.libraries.designsystem.components.avatarBloom
+import io.element.android.libraries.designsystem.modifiers.backgroundVerticalGradient
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.text.applyScaleDown
-import io.element.android.libraries.designsystem.text.roundToPx
-import io.element.android.libraries.designsystem.text.toDp
import io.element.android.libraries.designsystem.text.toSp
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
import io.element.android.libraries.designsystem.theme.components.DropdownMenu
@@ -73,8 +64,6 @@ import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings
-private val avatarBloomSize = 430.dp
-
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RoomListTopBar(
@@ -126,25 +115,13 @@ private fun DefaultRoomListTopBar(
canReportBug: Boolean,
modifier: Modifier = Modifier,
) {
- // We need this to manually clip the top app bar in preview mode
- val previewAppBarHeight = if (LocalInspectionMode.current) {
- 112.dp.roundToPx()
- } else {
- null
- }
val collapsedFraction = scrollBehavior.state.collapsedFraction
- var appBarHeight by remember {
- mutableIntStateOf(previewAppBarHeight ?: 0)
- }
-
val avatarData by remember(matrixUser) {
derivedStateOf {
matrixUser.getAvatarData(size = AvatarSize.CurrentUserTopBar)
}
}
- val statusBarPadding = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() }
-
Box(modifier = modifier) {
val collapsedTitleTextStyle = ElementTheme.typography.aliasScreenTitle
val expandedTitleTextStyle = ElementTheme.typography.fontHeadingLgBold.copy(
@@ -160,40 +137,13 @@ private fun DefaultRoomListTopBar(
titleLarge = collapsedTitleTextStyle
),
) {
- Column(
- modifier = Modifier
- .onSizeChanged {
- appBarHeight = it.height
- }
- .avatarBloom(
- avatarData = avatarData,
- background = if (ElementTheme.isLightTheme) {
- // Workaround to display a very subtle bloom for avatars with very soft colors
- Color(0xFFF9F9F9)
- } else {
- ElementTheme.colors.bgCanvasDefault
- },
- blurSize = DpSize(avatarBloomSize, avatarBloomSize),
- offset = DpOffset(24.dp, 24.dp + statusBarPadding),
- clipToSize = if (appBarHeight > 0) {
- DpSize(
- avatarBloomSize,
- appBarHeight.toDp()
- )
- } else {
- DpSize.Unspecified
- },
- bottomSoftEdgeColor = ElementTheme.colors.bgCanvasDefault,
- bottomSoftEdgeAlpha = if (displayFilters) {
- 1f
- } else {
- 1f - collapsedFraction
- },
- alpha = if (areSearchResultsDisplayed) 0f else 1f,
- )
- .statusBarsPadding(),
- ) {
+ Column {
MediumTopAppBar(
+ modifier = Modifier
+ .backgroundVerticalGradient(
+ isVisible = !areSearchResultsDisplayed,
+ )
+ .statusBarsPadding(),
colors = TopAppBarDefaults.mediumTopAppBarColors(
containerColor = Color.Transparent,
scrolledContainerColor = Color.Transparent,
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt
index e5a85a7442..b37e9ea223 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt
@@ -219,7 +219,7 @@ private fun NameAndTimestampRow(
style = ElementTheme.typography.fontBodyLgMedium,
text = name ?: stringResource(id = CommonStrings.common_no_room_name),
fontStyle = FontStyle.Italic.takeIf { name == null },
- color = ElementTheme.roomListRoomName(),
+ color = ElementTheme.colors.roomListRoomName,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
@@ -230,7 +230,7 @@ private fun NameAndTimestampRow(
color = if (isHighlighted) {
ElementTheme.colors.unreadIndicator
} else {
- ElementTheme.roomListRoomMessageDate()
+ ElementTheme.colors.roomListRoomMessageDate
},
)
}
@@ -253,7 +253,7 @@ private fun InviteSubtitle(
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = ElementTheme.typography.fontBodyMdRegular,
- color = ElementTheme.roomListRoomMessage(),
+ color = ElementTheme.colors.roomListRoomMessage,
modifier = modifier,
)
}
@@ -277,7 +277,7 @@ private fun MessagePreviewAndIndicatorRow(
Text(
modifier = Modifier.weight(1f),
text = annotatedMessagePreview,
- color = ElementTheme.roomListRoomMessage(),
+ color = ElementTheme.colors.roomListRoomMessage,
style = ElementTheme.typography.fontBodyMdRegular,
minLines = 2,
maxLines = 2,
@@ -325,7 +325,7 @@ private fun InviteNameAndIndicatorRow(
style = ElementTheme.typography.fontBodyLgMedium,
text = name ?: stringResource(id = CommonStrings.common_no_room_name),
fontStyle = FontStyle.Italic.takeIf { name == null },
- color = ElementTheme.roomListRoomName(),
+ color = ElementTheme.colors.roomListRoomName,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListDataSource.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListDataSource.kt
index dcda64ca9c..3ed7c383ba 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListDataSource.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListDataSource.kt
@@ -119,7 +119,7 @@ class RoomListDataSource @Inject constructor(
private suspend fun rebuildAllRoomSummaries() {
lock.withLock {
- roomListService.allRooms.summaries.replayCache.firstOrNull()?.let { roomSummaries ->
+ roomListService.allRooms.filteredSummaries.replayCache.firstOrNull()?.let { roomSummaries ->
buildAndEmitAllRooms(roomSummaries, useCache = false)
}
}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenu.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenu.kt
index 27883e88d5..2a6cfee502 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenu.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenu.kt
@@ -176,7 +176,6 @@ private fun RoomListModalBottomSheetContent(
leadingContent = ListItemContent.Icon(
iconSource = IconSource.Vector(
CompoundIcons.ChatProblem(),
- contentDescription = stringResource(CommonStrings.action_report_room),
)
),
style = ListItemStyle.Destructive,
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateProvider.kt
index 0e4600c16a..b28ff0cce9 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateProvider.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateProvider.kt
@@ -42,6 +42,7 @@ open class RoomListStateProvider : PreviewParameterProvider {
aRoomListState(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")),
aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SetUpRecovery)),
aRoomListState(contentState = aRoomsContentState(batteryOptimizationState = aBatteryOptimizationState(shouldDisplayBanner = true))),
+ aRoomListState(contentState = anEmptyContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation)),
)
}
diff --git a/features/home/impl/src/main/res/values/localazy.xml b/features/home/impl/src/main/res/values/localazy.xml
index 58afb06591..f2d2e9c441 100644
--- a/features/home/impl/src/main/res/values/localazy.xml
+++ b/features/home/impl/src/main/res/values/localazy.xml
@@ -12,6 +12,8 @@
"Your key storage is out of sync"
"To ensure you never miss an important call, please change your settings to allow full-screen notifications when your phone is locked."
"Enhance your call experience"
+ "Chats"
+ "Spaces"
"Are you sure you want to decline the invitation to join %1$s?"
"Decline invite"
"Are you sure you want to decline this private chat with %1$s?"
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt
index 644cbf2310..d8af3c1736 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt
@@ -18,8 +18,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.semantics.hideFromAccessibility
-import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
@@ -44,15 +42,15 @@ fun TimelineEventTimestampView(
modifier: Modifier = Modifier,
) {
val formattedTime = event.sentTime
- val hasError = event.localSendState is LocalEventSendState.Failed
+ val hasError = event.failedToSend
val hasEncryptionCritical = event.messageShield?.isCritical.orFalse()
val isMessageEdited = event.content.isEdited()
val isMessageRedacted = event.content.isRedacted()
val tint = if (hasError || hasEncryptionCritical && !isMessageRedacted) ElementTheme.colors.textCriticalPrimary else ElementTheme.colors.textSecondary
Row(
modifier = Modifier
- .padding(PaddingValues(start = TimelineEventTimestampViewDefaults.spacing))
- .then(modifier),
+ .padding(PaddingValues(start = TimelineEventTimestampViewDefaults.spacing))
+ .then(modifier),
verticalAlignment = Alignment.CenterVertically,
) {
if (isMessageEdited) {
@@ -76,11 +74,13 @@ fun TimelineEventTimestampView(
contentDescription = stringResource(id = CommonStrings.common_sending_failed),
tint = tint,
modifier = Modifier
- .size(15.dp, 18.dp)
- .clickable(isVerifiedUserSendFailure) {
- eventSink(TimelineEvents.ComputeVerifiedUserSendFailure(event))
- }
- .semantics { hideFromAccessibility() }
+ .size(15.dp, 18.dp)
+ .clickable(
+ enabled = isVerifiedUserSendFailure,
+ onClickLabel = stringResource(CommonStrings.action_open_context_menu),
+ ) {
+ eventSink(TimelineEvents.ComputeVerifiedUserSendFailure(event))
+ }
)
}
@@ -89,13 +89,14 @@ fun TimelineEventTimestampView(
Spacer(modifier = Modifier.width(2.dp))
Icon(
imageVector = shield.toIcon(),
- contentDescription = shield.toText(),
+ contentDescription = stringResource(id = CommonStrings.a11y_encryption_details),
modifier = Modifier
.size(15.dp)
- .clickable {
+ .clickable(
+ onClickLabel = stringResource(CommonStrings.a11y_view_details),
+ ) {
eventSink(TimelineEvents.ShowShieldDialog(shield))
- }
- .semantics { hideFromAccessibility() },
+ },
tint = shield.toIconColor(),
)
Spacer(modifier = Modifier.width(4.dp))
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt
index ffbcfee305..59ec63d842 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt
@@ -37,11 +37,11 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionEvent
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
+import io.element.android.libraries.designsystem.modifiers.subtleColorStops
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.text.toPx
import io.element.android.libraries.designsystem.theme.LocalBuildMeta
-import io.element.android.libraries.designsystem.theme.highlightedMessageBackgroundColor
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.ui.strings.CommonStrings
@@ -140,7 +140,10 @@ internal fun TimelineItemRow(
timelineItem.safeSenderName
}
// For Polls, allow the answers to be traversed by Talkback
- isTraversalGroup = timelineItem.content is TimelineItemPollContent
+ isTraversalGroup = timelineItem.content is TimelineItemPollContent ||
+ timelineItem.failedToSend ||
+ timelineItem.messageShield != null
+ // TODO Also set to true when the event has link(s)
}
// Custom clickable that applies over the whole item for accessibility
.then(
@@ -206,23 +209,20 @@ internal fun TimelineItemRow(
@Suppress("ModifierComposable")
@Composable
private fun Modifier.focusedEvent(
- focusedEventOffset: Dp
+ focusedEventOffset: Dp,
+ isEnterpriseBuild: Boolean = LocalBuildMeta.current.isEnterpriseBuild,
): Modifier {
- val highlightedLineColor = ElementTheme.colors.textActionAccent
- val gradientFirstColor = if (LocalBuildMeta.current.isEnterpriseBuild) {
- ElementTheme.colors.textActionAccent.copy(alpha = 0.125f)
+ val highlightedLineColor = if (isEnterpriseBuild) {
+ ElementTheme.colors.textActionAccent
} else {
- ElementTheme.colors.highlightedMessageBackgroundColor
+ ElementTheme.colors.borderAccentSubtle
}
- val gradientColors = listOf(
- gradientFirstColor,
- ElementTheme.colors.bgCanvasDefault,
- )
+ val gradientColors = subtleColorStops(isEnterpriseBuild)
val verticalOffset = focusedEventOffset.toPx()
val verticalRatio = 0.7f
return drawWithCache {
val brush = Brush.verticalGradient(
- colors = gradientColors,
+ colorStops = gradientColors,
endY = size.height * verticalRatio,
)
onDrawBehind {
@@ -251,3 +251,18 @@ internal fun FocusedEventPreview() = ElementPreview {
.focusedEvent(0.dp),
)
}
+
+@PreviewsDayNight
+@Composable
+internal fun FocusedEventEnterprisePreview() = ElementPreview {
+ Box(
+ modifier = Modifier
+ .padding(16.dp)
+ .fillMaxWidth()
+ .height(160.dp)
+ .focusedEvent(
+ focusedEventOffset = 0.dp,
+ isEnterpriseBuild = true,
+ ),
+ )
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/layout/ContentAvoidingLayout.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/layout/ContentAvoidingLayout.kt
index d50c6d72ce..36f3204f89 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/layout/ContentAvoidingLayout.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/layout/ContentAvoidingLayout.kt
@@ -112,12 +112,11 @@ fun ContentAvoidingLayout(
* @param nonOverlappingContentWidth The width of the part of the content that can't overlap with the timestamp.
* @param nonOverlappingContentHeight The height of the part of the content that can't overlap with the timestamp.
*/
-@Suppress("DataClassShouldBeImmutable")
data class ContentAvoidingLayoutData(
- var contentWidth: Int = 0,
- var contentHeight: Int = 0,
- var nonOverlappingContentWidth: Int = contentWidth,
- var nonOverlappingContentHeight: Int = contentHeight,
+ val contentWidth: Int = 0,
+ val contentHeight: Int = 0,
+ val nonOverlappingContentWidth: Int = contentWidth,
+ val nonOverlappingContentHeight: Int = contentHeight,
)
/**
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt
index c4a2351990..73969a2860 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt
@@ -116,7 +116,7 @@ class TimelineViewTest {
eventSink = eventsRecorder,
),
)
- val contentDescription = rule.activity.getString(CommonStrings.event_shield_reason_unverified_identity)
+ val contentDescription = rule.activity.getString(CommonStrings.a11y_encryption_details)
rule.onNodeWithContentDescription(contentDescription).performClick()
eventsRecorder.assertList(
listOf(
diff --git a/features/poll/api/src/main/res/values/localazy.xml b/features/poll/api/src/main/res/values/localazy.xml
index 2d1142194c..ebba470b6a 100644
--- a/features/poll/api/src/main/res/values/localazy.xml
+++ b/features/poll/api/src/main/res/values/localazy.xml
@@ -4,5 +4,6 @@
- "%1$d percent of total votes"
- "%1$d percents of total votes"
+ "Will remove previous selection"
"This is the winning answer"
diff --git a/features/rageshake/impl/src/main/java/io/element/android/features/rageshake/impl/reporter/BugReporterMultipartBody.java b/features/rageshake/impl/src/main/java/io/element/android/features/rageshake/impl/reporter/BugReporterMultipartBody.java
deleted file mode 100755
index da198e431a..0000000000
--- a/features/rageshake/impl/src/main/java/io/element/android/features/rageshake/impl/reporter/BugReporterMultipartBody.java
+++ /dev/null
@@ -1,290 +0,0 @@
-/*
- * Copyright 2023, 2024 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.
- */
-
-package io.element.android.features.rageshake.impl.reporter;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.UUID;
-
-import okhttp3.Headers;
-import okhttp3.MediaType;
-import okhttp3.RequestBody;
-import okhttp3.internal.Util;
-import okio.Buffer;
-import okio.BufferedSink;
-import okio.ByteString;
-
-// simplified version of MultipartBody (OkHttp 3.6.0)
-public class BugReporterMultipartBody extends RequestBody {
-
- /**
- * Listener
- */
- public interface WriteListener {
- /**
- * Upload listener
- *
- * @param totalWritten total written bytes
- * @param contentLength content length
- */
- void onWrite(long totalWritten, long contentLength);
- }
-
- private static final MediaType FORM = MediaType.parse("multipart/form-data");
-
- private static final byte[] COLONSPACE = {':', ' '};
- private static final byte[] CRLF = {'\r', '\n'};
- private static final byte[] DASHDASH = {'-', '-'};
-
- private final ByteString mBoundary;
- private final MediaType mContentType;
- private final List mParts;
- private long mContentLength = -1L;
-
- // listener
- private WriteListener mWriteListener;
-
- //
- private List mContentLengthSize = null;
-
- private BugReporterMultipartBody(ByteString boundary, List parts) {
- mBoundary = boundary;
- mContentType = MediaType.parse(FORM + "; boundary=" + boundary.utf8());
- mParts = Util.toImmutableList(parts);
- }
-
- @Override
- public MediaType contentType() {
- return mContentType;
- }
-
- @Override
- public long contentLength() throws IOException {
- long result = mContentLength;
- if (result != -1L) return result;
- return mContentLength = writeOrCountBytes(null, true);
- }
-
- @Override
- public void writeTo(BufferedSink sink) throws IOException {
- writeOrCountBytes(sink, false);
- }
-
- /**
- * Set the listener
- *
- * @param listener the
- */
- public void setWriteListener(WriteListener listener) {
- mWriteListener = listener;
- }
-
- /**
- * Warn the listener that some bytes have been written
- *
- * @param totalWrittenBytes the total written bytes
- */
- private void onWrite(long totalWrittenBytes) {
- if ((null != mWriteListener) && (mContentLength > 0)) {
- mWriteListener.onWrite(totalWrittenBytes, mContentLength);
- }
- }
-
- /**
- * Either writes this request to {@code sink} or measures its content length. We have one method
- * do double-duty to make sure the counting and content are consistent, particularly when it comes
- * to awkward operations like measuring the encoded length of header strings, or the
- * length-in-digits of an encoded integer.
- */
- private long writeOrCountBytes(BufferedSink sink, boolean countBytes) throws IOException {
- long byteCount = 0L;
-
- Buffer byteCountBuffer = null;
- if (countBytes) {
- sink = byteCountBuffer = new Buffer();
- mContentLengthSize = new ArrayList<>();
- }
-
- for (int p = 0, partCount = mParts.size(); p < partCount; p++) {
- Part part = mParts.get(p);
- Headers headers = part.headers;
- RequestBody body = part.body;
-
- sink.write(DASHDASH);
- sink.write(mBoundary);
- sink.write(CRLF);
-
- if (headers != null) {
- for (int h = 0, headerCount = headers.size(); h < headerCount; h++) {
- sink.writeUtf8(headers.name(h))
- .write(COLONSPACE)
- .writeUtf8(headers.value(h))
- .write(CRLF);
- }
- }
-
- MediaType contentType = body.contentType();
- if (contentType != null) {
- sink.writeUtf8("Content-Type: ")
- .writeUtf8(contentType.toString())
- .write(CRLF);
- }
-
- int contentLength = (int) body.contentLength();
- if (contentLength != -1) {
- sink.writeUtf8("Content-Length: ")
- .writeUtf8(contentLength + "")
- .write(CRLF);
- } else if (countBytes) {
- // We can't measure the body's size without the sizes of its components.
- byteCountBuffer.clear();
- return -1L;
- }
-
- sink.write(CRLF);
-
- if (countBytes) {
- byteCount += contentLength;
- mContentLengthSize.add(byteCount);
- } else {
- body.writeTo(sink);
-
- // warn the listener of upload progress
- // sink.buffer().size() does not give the right value
- // assume that some data are popped
- if ((null != mContentLengthSize) && (p < mContentLengthSize.size())) {
- onWrite(mContentLengthSize.get(p));
- }
- }
- sink.write(CRLF);
- }
-
- sink.write(DASHDASH);
- sink.write(mBoundary);
- sink.write(DASHDASH);
- sink.write(CRLF);
-
- if (countBytes) {
- byteCount += byteCountBuffer.size();
- byteCountBuffer.clear();
- }
-
- return byteCount;
- }
-
- private static void appendQuotedString(StringBuilder target, String key) {
- target.append('"');
- for (int i = 0, len = key.length(); i < len; i++) {
- char ch = key.charAt(i);
- switch (ch) {
- case '\n':
- target.append("%0A");
- break;
- case '\r':
- target.append("%0D");
- break;
- case '"':
- target.append("%22");
- break;
- default:
- target.append(ch);
- break;
- }
- }
- target.append('"');
- }
-
- public static final class Part {
- public static Part create(Headers headers, RequestBody body) {
- if (body == null) {
- throw new NullPointerException("body == null");
- }
- if (headers != null && headers.get("Content-Type") != null) {
- throw new IllegalArgumentException("Unexpected header: Content-Type");
- }
- if (headers != null && headers.get("Content-Length") != null) {
- throw new IllegalArgumentException("Unexpected header: Content-Length");
- }
- return new Part(headers, body);
- }
-
- public static Part createFormData(String name, String value) {
- return createFormData(name, null, RequestBody.create(value, null));
- }
-
- public static Part createFormData(String name, String filename, RequestBody body) {
- if (name == null) {
- throw new NullPointerException("name == null");
- }
- StringBuilder disposition = new StringBuilder("form-data; name=");
- appendQuotedString(disposition, name);
-
- if (filename != null) {
- disposition.append("; filename=");
- appendQuotedString(disposition, filename);
- }
-
- return create(Headers.of("Content-Disposition", disposition.toString()), body);
- }
-
- final Headers headers;
- final RequestBody body;
-
- private Part(Headers headers, RequestBody body) {
- this.headers = headers;
- this.body = body;
- }
- }
-
- public static final class Builder {
- private final ByteString boundary;
- private final List parts = new ArrayList<>();
-
- public Builder() {
- this(UUID.randomUUID().toString());
- }
-
- public Builder(String boundary) {
- this.boundary = ByteString.encodeUtf8(boundary);
- }
-
- /**
- * Add a form data part to the body.
- */
- public Builder addFormDataPart(String name, String value) {
- return addPart(Part.createFormData(name, value));
- }
-
- /**
- * Add a form data part to the body.
- */
- public Builder addFormDataPart(String name, String filename, RequestBody body) {
- return addPart(Part.createFormData(name, filename, body));
- }
-
- /**
- * Add a part to the body.
- */
- public Builder addPart(Part part) {
- if (part == null) throw new NullPointerException("part == null");
- parts.add(part);
- return this;
- }
-
- /**
- * Assemble the specified parts into a request body.
- */
- public BugReporterMultipartBody build() {
- if (parts.isEmpty()) {
- throw new IllegalStateException("Multipart body must have at least one part.");
- }
- return new BugReporterMultipartBody(boundary, parts);
- }
- }
-}
diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/BugReporterMultipartBody.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/BugReporterMultipartBody.kt
new file mode 100755
index 0000000000..0fe0bfdc86
--- /dev/null
+++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/BugReporterMultipartBody.kt
@@ -0,0 +1,419 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ * Copyright 2023, 2024 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.
+ */
+@file:Suppress(
+ "unused",
+ "KDocUnresolvedReference",
+ "SpellCheckingInspection",
+)
+
+package io.element.android.features.rageshake.impl.reporter
+
+import kotlinx.collections.immutable.toImmutableList
+import okhttp3.Headers
+import okhttp3.MediaType
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.RequestBody
+import okio.Buffer
+import okio.BufferedSink
+import okio.ByteString
+import okio.ByteString.Companion.encodeUtf8
+import java.io.IOException
+import java.util.UUID
+
+/**
+ * Copy of [okhttp3.MultipartBody] with addition of a listener to track progress (Last imported from OkHttp 5.0.0).
+ * Patches are surrounded by ELEMENT-START and ELEMENT-END
+ *
+ * An [RFC 2387][rfc_2387]-compliant request body.
+ *
+ * [rfc_2387]: http://www.ietf.org/rfc/rfc2387.txt
+ */
+@Suppress("NAME_SHADOWING")
+class BugReporterMultipartBody internal constructor(
+ private val boundaryByteString: ByteString,
+ @get:JvmName("type") val type: MediaType,
+ @get:JvmName("parts") val parts: List,
+) : RequestBody() {
+ // ELEMENT-START
+ private var listener: BugReporterMultipartBodyListener? = null
+
+ private fun onWrite(totalWrittenBytes: Long) {
+ listener
+ ?.takeIf { contentLength > 0 }
+ ?.onWrite(totalWrittenBytes, contentLength)
+ }
+
+ private val contentLengthSize = mutableListOf()
+
+ fun setWriteListener(listener: BugReporterMultipartBodyListener?) {
+ this.listener = listener
+ }
+ // ELEMENT-END
+
+ private val contentType: MediaType = "$type; boundary=$boundary".toMediaType()
+ private var contentLength = -1L
+
+ @get:JvmName("boundary")
+ val boundary: String
+ get() = boundaryByteString.utf8()
+
+ /** The number of parts in this multipart body. */
+ @get:JvmName("size")
+ val size: Int
+ get() = parts.size
+
+ fun part(index: Int): Part = parts[index]
+
+ override fun isOneShot(): Boolean = parts.any { it.body.isOneShot() }
+
+ /** A combination of [type] and [boundaryByteString]. */
+ override fun contentType(): MediaType = contentType
+
+ @JvmName("-deprecated_type")
+ @Deprecated(
+ message = "moved to val",
+ replaceWith = ReplaceWith(expression = "type"),
+ level = DeprecationLevel.ERROR,
+ )
+ fun type(): MediaType = type
+
+ @JvmName("-deprecated_boundary")
+ @Deprecated(
+ message = "moved to val",
+ replaceWith = ReplaceWith(expression = "boundary"),
+ level = DeprecationLevel.ERROR,
+ )
+ fun boundary(): String = boundary
+
+ @JvmName("-deprecated_size")
+ @Deprecated(
+ message = "moved to val",
+ replaceWith = ReplaceWith(expression = "size"),
+ level = DeprecationLevel.ERROR,
+ )
+ fun size(): Int = size
+
+ @JvmName("-deprecated_parts")
+ @Deprecated(
+ message = "moved to val",
+ replaceWith = ReplaceWith(expression = "parts"),
+ level = DeprecationLevel.ERROR,
+ )
+ fun parts(): List = parts
+
+ @Throws(IOException::class)
+ override fun contentLength(): Long {
+ var result = contentLength
+ if (result == -1L) {
+ result = writeOrCountBytes(null, true)
+ contentLength = result
+ }
+ return result
+ }
+
+ @Throws(IOException::class)
+ override fun writeTo(sink: BufferedSink) {
+ writeOrCountBytes(sink, false)
+ }
+
+ /**
+ * Either writes this request to [sink] or measures its content length. We have one method do
+ * double-duty to make sure the counting and content are consistent, particularly when it comes
+ * to awkward operations like measuring the encoded length of header strings, or the
+ * length-in-digits of an encoded integer.
+ */
+ @Throws(IOException::class)
+ private fun writeOrCountBytes(
+ sink: BufferedSink?,
+ countBytes: Boolean,
+ ): Long {
+ var sink = sink
+ var byteCount = 0L
+
+ var byteCountBuffer: Buffer? = null
+ if (countBytes) {
+ byteCountBuffer = Buffer()
+ sink = byteCountBuffer
+ // ELEMENT-START
+ contentLengthSize.clear()
+ // ELEMENT-END
+ }
+
+ for (p in 0 until parts.size) {
+ val part = parts[p]
+ val headers = part.headers
+ val body = part.body
+
+ sink!!.write(DASHDASH)
+ sink.write(boundaryByteString)
+ sink.write(CRLF)
+
+ if (headers != null) {
+ for (h in 0 until headers.size) {
+ sink
+ .writeUtf8(headers.name(h))
+ .write(COLONSPACE)
+ .writeUtf8(headers.value(h))
+ .write(CRLF)
+ }
+ }
+
+ val contentType = body.contentType()
+ if (contentType != null) {
+ sink
+ .writeUtf8("Content-Type: ")
+ .writeUtf8(contentType.toString())
+ .write(CRLF)
+ }
+
+ // We can't measure the body's size without the sizes of its components.
+ val contentLength = body.contentLength()
+ if (contentLength == -1L && countBytes) {
+ byteCountBuffer!!.clear()
+ return -1L
+ }
+
+ sink.write(CRLF)
+
+ if (countBytes) {
+ byteCount += contentLength
+ // ELEMENT-START
+ contentLengthSize.add(byteCount)
+ // ELEMENT-END
+ } else {
+ body.writeTo(sink)
+ // ELEMENT-START
+ // warn the listener of upload progress
+ // sink.buffer().size() does not give the right value
+ // assume that some data are popped
+ contentLengthSize.getOrNull(p)?.let { writtenByte ->
+ onWrite(writtenByte)
+ }
+ // ELEMENT-END
+ }
+
+ sink.write(CRLF)
+ }
+
+ sink!!.write(DASHDASH)
+ sink.write(boundaryByteString)
+ sink.write(DASHDASH)
+ sink.write(CRLF)
+
+ if (countBytes) {
+ byteCount += byteCountBuffer!!.size
+ byteCountBuffer.clear()
+ }
+
+ return byteCount
+ }
+
+ class Part private constructor(
+ @get:JvmName("headers") val headers: Headers?,
+ @get:JvmName("body") val body: RequestBody,
+ ) {
+ @JvmName("-deprecated_headers")
+ @Deprecated(
+ message = "moved to val",
+ replaceWith = ReplaceWith(expression = "headers"),
+ level = DeprecationLevel.ERROR,
+ )
+ fun headers(): Headers? = headers
+
+ @JvmName("-deprecated_body")
+ @Deprecated(
+ message = "moved to val",
+ replaceWith = ReplaceWith(expression = "body"),
+ level = DeprecationLevel.ERROR,
+ )
+ fun body(): RequestBody = body
+
+ companion object {
+ @JvmStatic
+ fun create(body: RequestBody): Part = create(null, body)
+
+ @JvmStatic
+ fun create(
+ headers: Headers?,
+ body: RequestBody,
+ ): Part {
+ require(headers?.get("Content-Type") == null) { "Unexpected header: Content-Type" }
+ require(headers?.get("Content-Length") == null) { "Unexpected header: Content-Length" }
+ return Part(headers, body)
+ }
+
+ @JvmStatic
+ fun createFormData(
+ name: String,
+ value: String,
+ ): Part = createFormData(name, null, value.toRequestBody())
+
+ @JvmStatic
+ fun createFormData(
+ name: String,
+ filename: String?,
+ body: RequestBody,
+ ): Part {
+ val disposition =
+ buildString {
+ append("form-data; name=")
+ appendQuotedString(name)
+
+ if (filename != null) {
+ append("; filename=")
+ appendQuotedString(filename)
+ }
+ }
+
+ val headers =
+ Headers
+ .Builder()
+ .addUnsafeNonAscii("Content-Disposition", disposition)
+ .build()
+
+ return create(headers, body)
+ }
+ }
+ }
+
+ class Builder
+ @JvmOverloads
+ constructor(
+ boundary: String = UUID.randomUUID().toString(),
+ ) {
+ private val boundary: ByteString = boundary.encodeUtf8()
+
+ // ELEMENT-START
+ // Element: use FORM as default type
+ private var type = FORM
+ // ELEMENT-END
+
+ private val parts = mutableListOf()
+
+ /**
+ * Set the MIME type. Expected values for `type` are [MIXED] (the default), [ALTERNATIVE],
+ * [DIGEST], [PARALLEL] and [FORM].
+ */
+ fun setType(type: MediaType) =
+ apply {
+ require(type.type == "multipart") { "multipart != $type" }
+ this.type = type
+ }
+
+ /** Add a part to the body. */
+ fun addPart(body: RequestBody) =
+ apply {
+ addPart(Part.create(body))
+ }
+
+ /** Add a part to the body. */
+ fun addPart(
+ headers: Headers?,
+ body: RequestBody,
+ ) = apply {
+ addPart(Part.create(headers, body))
+ }
+
+ /** Add a form data part to the body. */
+ fun addFormDataPart(
+ name: String,
+ value: String,
+ ) = apply {
+ addPart(Part.createFormData(name, value))
+ }
+
+ /** Add a form data part to the body. */
+ fun addFormDataPart(
+ name: String,
+ filename: String?,
+ body: RequestBody,
+ ) = apply {
+ addPart(Part.createFormData(name, filename, body))
+ }
+
+ /** Add a part to the body. */
+ fun addPart(part: Part) =
+ apply {
+ parts += part
+ }
+
+ /** Assemble the specified parts into a request body. */
+ fun build(): BugReporterMultipartBody {
+ check(parts.isNotEmpty()) { "Multipart body must have at least one part." }
+ return BugReporterMultipartBody(boundary, type, parts.toImmutableList())
+ }
+ }
+
+ companion object {
+ /**
+ * The "mixed" subtype of "multipart" is intended for use when the body parts are independent
+ * and need to be bundled in a particular order. Any "multipart" subtypes that an implementation
+ * does not recognize must be treated as being of subtype "mixed".
+ */
+ @JvmField
+ val MIXED = "multipart/mixed".toMediaType()
+
+ /**
+ * The "multipart/alternative" type is syntactically identical to "multipart/mixed", but the
+ * semantics are different. In particular, each of the body parts is an "alternative" version of
+ * the same information.
+ */
+ @JvmField
+ val ALTERNATIVE = "multipart/alternative".toMediaType()
+
+ /**
+ * This type is syntactically identical to "multipart/mixed", but the semantics are different.
+ * In particular, in a digest, the default `Content-Type` value for a body part is changed from
+ * "text/plain" to "message/rfc822".
+ */
+ @JvmField
+ val DIGEST = "multipart/digest".toMediaType()
+
+ /**
+ * This type is syntactically identical to "multipart/mixed", but the semantics are different.
+ * In particular, in a parallel entity, the order of body parts is not significant.
+ */
+ @JvmField
+ val PARALLEL = "multipart/parallel".toMediaType()
+
+ /**
+ * The media-type multipart/form-data follows the rules of all multipart MIME data streams as
+ * outlined in RFC 2046. In forms, there are a series of fields to be supplied by the user who
+ * fills out the form. Each field has a name. Within a given form, the names are unique.
+ */
+ @JvmField
+ val FORM = "multipart/form-data".toMediaType()
+
+ private val COLONSPACE = byteArrayOf(':'.code.toByte(), ' '.code.toByte())
+ private val CRLF = byteArrayOf('\r'.code.toByte(), '\n'.code.toByte())
+ private val DASHDASH = byteArrayOf('-'.code.toByte(), '-'.code.toByte())
+
+ /**
+ * Appends a quoted-string to a StringBuilder.
+ *
+ * RFC 2388 is rather vague about how one should escape special characters in form-data
+ * parameters, and as it turns out Firefox and Chrome actually do rather different things, and
+ * both say in their comments that they're not really sure what the right approach is. We go
+ * with Chrome's behavior (which also experimentally seems to match what IE does), but if you
+ * actually want to have a good chance of things working, please avoid double-quotes, newlines,
+ * percent signs, and the like in your field names.
+ */
+ internal fun StringBuilder.appendQuotedString(key: String) {
+ append('"')
+ for (i in 0 until key.length) {
+ when (val ch = key[i]) {
+ '\n' -> append("%0A")
+ '\r' -> append("%0D")
+ '"' -> append("%22")
+ else -> append(ch)
+ }
+ }
+ append('"')
+ }
+ }
+}
diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/BugReporterMultipartBodyListener.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/BugReporterMultipartBodyListener.kt
new file mode 100644
index 0000000000..f6fe406666
--- /dev/null
+++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/BugReporterMultipartBodyListener.kt
@@ -0,0 +1,18 @@
+/*
+ * 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.
+ */
+
+package io.element.android.features.rageshake.impl.reporter
+
+fun interface BugReporterMultipartBodyListener {
+ /**
+ * Upload listener.
+ *
+ * @param totalWritten total written bytes
+ * @param contentLength content length
+ */
+ fun onWrite(totalWritten: Long, contentLength: Long)
+}
diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesViewTest.kt
index 4bb0253e6a..6f2179d4d4 100644
--- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesViewTest.kt
+++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesViewTest.kt
@@ -225,7 +225,10 @@ class ChangeRolesViewTest {
)
// Unselect the user from the row list
val contentDescription = rule.activity.getString(CommonStrings.action_remove)
- rule.onNodeWithContentDescription(contentDescription).performClick()
+ rule.onNodeWithContentDescription(
+ label = contentDescription,
+ useUnmergedTree = true,
+ ).performClick()
eventsRecorder.assertList(
listOf(
ChangeRolesEvent.QueryChanged(""),
@@ -248,7 +251,7 @@ class ChangeRolesViewTest {
rule.setChangeRolesContent(
state = state,
)
- // Select the user from the row list
+ // Select the user from the user list
rule.onNodeWithText("Carol").performClick()
eventsRecorder.assertList(
listOf(
@@ -271,8 +274,11 @@ class ChangeRolesViewTest {
rule.setChangeRolesContent(
state = state,
)
- // Select the user from the rom list
- rule.onAllNodesWithText("Bob")[1].performClick()
+ // Unselect the user from the user list
+ rule.onAllNodesWithText(
+ text = "Bob",
+ useUnmergedTree = true,
+ )[1].performClick()
eventsRecorder.assertList(
listOf(
ChangeRolesEvent.QueryChanged(""),
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index fa90db9e3f..d6f8d90c9d 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -3,7 +3,7 @@
[versions]
# Project
-android_gradle_plugin = "8.11.0"
+android_gradle_plugin = "8.11.1"
kotlin = "2.2.0"
kotlinpoet = "2.2.0"
ksp = "2.2.0-2.0.2"
@@ -33,7 +33,7 @@ accompanist = "0.37.3"
test_core = "1.6.1"
# Jetbrain
-datetime = "0.7.0"
+datetime = "0.7.1"
serialization_json = "1.9.0"
#other
@@ -42,9 +42,9 @@ coil = "3.2.0"
showkase = "1.0.4"
appyx = "1.7.1"
sqldelight = "2.1.0"
-wysiwyg = "2.38.4"
+wysiwyg = "2.38.5"
telephoto = "0.16.0"
-haze = "1.6.6"
+haze = "1.6.8"
# Dependency analysis
dependencyAnalysis = "2.19.0"
@@ -100,6 +100,9 @@ androidx_lifecycle_process = { module = "androidx.lifecycle:lifecycle-process",
androidx_splash = "androidx.core:core-splashscreen:1.0.1"
androidx_media3_exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
androidx_media3_ui = { module = "androidx.media3:media3-ui", version.ref = "media3" }
+androidx_media3_transformer = { module = "androidx.media3:media3-transformer", version.ref = "media3" }
+androidx_media3_effect = { module = "androidx.media3:media3-effect", version.ref = "media3" }
+androidx_media3_common = { module = "androidx.media3:media3-common", version.ref = "media3" }
androidx_biometric = "androidx.biometric:biometric-ktx:1.2.0-alpha05"
androidx_activity_activity = { module = "androidx.activity:activity", version.ref = "activity" }
@@ -131,7 +134,7 @@ accompanist_permission = { module = "com.google.accompanist:accompanist-permissi
squareup_seismic = "com.squareup:seismic:1.0.3"
# network
-network_okhttp_bom = "com.squareup.okhttp3:okhttp-bom:4.12.0"
+network_okhttp_bom = "com.squareup.okhttp3:okhttp-bom:5.1.0"
network_okhttp_logging = { module = "com.squareup.okhttp3:logging-interceptor" }
network_okhttp_okhttp = { module = "com.squareup.okhttp3:okhttp" }
network_okhttp = { module = "com.squareup.okhttp3:okhttp" }
@@ -163,7 +166,7 @@ coil_network_okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version
coil_compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
coil_gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil" }
coil_test = { module = "io.coil-kt.coil3:coil-test", version.ref = "coil" }
-compound = { module = "io.element.android:compound-android", version = "25.4.4" }
+compound = { module = "io.element.android:compound-android", version = "25.7.4" }
datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" }
serialization_json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization_json" }
kotlinx_collections_immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0"
@@ -173,7 +176,7 @@ jsoup = "org.jsoup:jsoup:1.21.1"
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
molecule-runtime = "app.cash.molecule:molecule-runtime:2.1.0"
timber = "com.jakewharton.timber:timber:5.0.1"
-matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.7.3"
+matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.7.15"
matrix_richtexteditor = { module = "io.element.android:wysiwyg", 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" }
@@ -182,12 +185,11 @@ sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions",
sqlcipher = "net.zetetic:sqlcipher-android:4.9.0"
sqlite = "androidx.sqlite:sqlite-ktx:2.5.2"
unifiedpush = "org.unifiedpush.android:connector:3.0.10"
-otaliastudios_transcoder = "com.otaliastudios:transcoder:0.11.2"
vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0"
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" }
statemachine = "com.freeletics.flowredux:compose:1.2.2"
-maplibre = "org.maplibre.gl:android-sdk:11.11.0"
+maplibre = "org.maplibre.gl:android-sdk:11.12.1"
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.2"
maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.2"
opusencoder = "io.element.android:opusencoder:1.2.0"
@@ -196,8 +198,8 @@ haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" }
haze_materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" }
# Analytics
-posthog = "com.posthog:posthog-android:3.19.0"
-sentry = "io.sentry:sentry-android:8.16.0"
+posthog = "com.posthog:posthog-android:3.19.2"
+sentry = "io.sentry:sentry-android:8.17.0"
# main branch can be tested replacing the version with main-SNAPSHOT
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.28.0"
@@ -238,7 +240,7 @@ kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
anvil = { id = "dev.zacsweers.anvil", version.ref = "anvil" }
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
-ktlint = "org.jlleitschuh.gradle.ktlint:12.3.0"
+ktlint = "org.jlleitschuh.gradle.ktlint:13.0.0"
dependencygraph = "com.savvasdalkitsis.module-dependency-graph:0.12"
dependencycheck = "org.owasp.dependencycheck:12.1.3"
dependencyanalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyAnalysis" }
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 3735f265b9..78cb6e16a4 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionSha256Sum=7197a12f450794931532469d4ff21a59ea2c1cd59a3ec3f89c035c3c420a6999
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip
+distributionSha256Sum=bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/accessibility/ContextExt.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/accessibility/ContextExt.kt
deleted file mode 100644
index 02250297b3..0000000000
--- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/accessibility/ContextExt.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright 2023, 2024 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.
- */
-
-package io.element.android.libraries.androidutils.accessibility
-
-import android.content.Context
-import android.view.accessibility.AccessibilityManager
-import androidx.core.content.getSystemService
-
-/**
- * Whether a screen reader is enabled.
- *
- * Avoid changing UI or app behavior based on the state of accessibility.
- * See [AccessibilityManager.isTouchExplorationEnabled] for more details.
- *
- * @return true if the screen reader is enabled.
- */
-fun Context.isScreenReaderEnabled(): Boolean {
- val accessibilityManager = getSystemService()
- ?: return false
-
- return accessibilityManager.let {
- it.isEnabled && it.isTouchExplorationEnabled
- }
-}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/MatrixBadgeAtom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/MatrixBadgeAtom.kt
index 51507064eb..227e2f27cc 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/MatrixBadgeAtom.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/MatrixBadgeAtom.kt
@@ -14,10 +14,6 @@ import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.components.Badge
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
-import io.element.android.libraries.designsystem.theme.badgeNegativeBackgroundColor
-import io.element.android.libraries.designsystem.theme.badgeNegativeContentColor
-import io.element.android.libraries.designsystem.theme.badgeNeutralBackgroundColor
-import io.element.android.libraries.designsystem.theme.badgeNeutralContentColor
object MatrixBadgeAtom {
data class MatrixBadgeData(
@@ -39,21 +35,21 @@ object MatrixBadgeAtom {
) {
val backgroundColor = when (data.type) {
Type.Positive -> ElementTheme.colors.bgBadgeAccent
- Type.Neutral -> ElementTheme.colors.badgeNeutralBackgroundColor
- Type.Negative -> ElementTheme.colors.badgeNegativeBackgroundColor
+ Type.Neutral -> ElementTheme.colors.bgBadgeDefault
+ Type.Negative -> ElementTheme.colors.bgCriticalSubtle
Type.Info -> ElementTheme.colors.bgBadgeInfo
}
val textColor = when (data.type) {
Type.Positive -> ElementTheme.colors.textBadgeAccent
- Type.Neutral -> ElementTheme.colors.badgeNeutralContentColor
- Type.Negative -> ElementTheme.colors.badgeNegativeContentColor
+ Type.Neutral -> ElementTheme.colors.textPrimary
+ Type.Negative -> ElementTheme.colors.textCriticalPrimary
Type.Info -> ElementTheme.colors.textBadgeInfo
}
val iconColor = when (data.type) {
- Type.Positive -> ElementTheme.colors.textBadgeAccent
- Type.Neutral -> ElementTheme.colors.iconSecondary
+ Type.Positive -> ElementTheme.colors.iconAccentPrimary
+ Type.Neutral -> ElementTheme.colors.iconPrimary
Type.Negative -> ElementTheme.colors.iconCriticalPrimary
- Type.Info -> ElementTheme.colors.textBadgeInfo
+ Type.Info -> ElementTheme.colors.iconInfoPrimary
}
Badge(
text = data.text,
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/background/OnboardingBackground.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/background/OnboardingBackground.kt
index b4830e0904..73bc7ca841 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/background/OnboardingBackground.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/background/OnboardingBackground.kt
@@ -23,9 +23,9 @@ import androidx.compose.ui.graphics.LinearGradientShader
import androidx.compose.ui.graphics.ShaderBrush
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
-import io.element.android.libraries.designsystem.components.drawWithLayer
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+import io.element.android.libraries.designsystem.utils.drawWithLayer
/**
* Gradient background for FTUE (onboarding) screens.
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/colors/Gradient.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/colors/Gradient.kt
new file mode 100644
index 0000000000..342377d0b8
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/colors/Gradient.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+
+package io.element.android.libraries.designsystem.colors
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.ui.graphics.Color
+import io.element.android.compound.theme.ElementTheme
+
+@Composable
+@ReadOnlyComposable
+fun gradientActionColors(): List = listOf(
+ ElementTheme.colors.gradientActionStop1,
+ ElementTheme.colors.gradientActionStop2,
+ ElementTheme.colors.gradientActionStop3,
+ ElementTheme.colors.gradientActionStop4,
+)
+
+@Composable
+@ReadOnlyComposable
+fun gradientSubtleColors(): List = listOf(
+ ElementTheme.colors.gradientSubtleStop1,
+ ElementTheme.colors.gradientSubtleStop2,
+ ElementTheme.colors.gradientSubtleStop3,
+ ElementTheme.colors.gradientSubtleStop4,
+ ElementTheme.colors.gradientSubtleStop5,
+ ElementTheme.colors.gradientSubtleStop6,
+)
+
+@Composable
+@ReadOnlyComposable
+fun gradientInfoColors(): List = listOf(
+ ElementTheme.colors.gradientInfoStop1,
+ ElementTheme.colors.gradientInfoStop2,
+ ElementTheme.colors.gradientInfoStop3,
+ ElementTheme.colors.gradientInfoStop4,
+ ElementTheme.colors.gradientInfoStop5,
+ ElementTheme.colors.gradientInfoStop6,
+)
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/BigCheckmark.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/BigCheckmark.kt
deleted file mode 100644
index b51b824501..0000000000
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/BigCheckmark.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright 2024 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.
- */
-
-package io.element.android.libraries.designsystem.components
-
-import androidx.compose.foundation.BorderStroke
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import io.element.android.compound.theme.ElementTheme
-import io.element.android.compound.tokens.generated.CompoundIcons
-import io.element.android.libraries.designsystem.preview.ElementPreview
-import io.element.android.libraries.designsystem.preview.PreviewsDayNight
-import io.element.android.libraries.designsystem.theme.bigCheckmarkBorderColor
-import io.element.android.libraries.designsystem.theme.components.Icon
-import io.element.android.libraries.designsystem.theme.components.Surface
-import io.element.android.libraries.ui.strings.CommonStrings
-
-/**
- * Compound component that displays a big checkmark centered in a rounded square.
- *
- * @param modifier the modifier to apply to this layout
- */
-@Composable
-fun BigCheckmark(
- modifier: Modifier = Modifier,
-) {
- Surface(
- modifier = modifier.size(120.dp),
- shape = RoundedCornerShape(14.dp),
- color = ElementTheme.colors.bgCanvasDefault,
- border = BorderStroke(1.dp, ElementTheme.colors.bigCheckmarkBorderColor),
- shadowElevation = 4.dp,
- ) {
- Box(contentAlignment = Alignment.Center) {
- Icon(
- modifier = Modifier.size(72.dp),
- tint = ElementTheme.colors.iconSuccessPrimary,
- imageVector = CompoundIcons.CheckCircleSolid(),
- contentDescription = stringResource(CommonStrings.common_success)
- )
- }
- }
-}
-
-@PreviewsDayNight
-@Composable
-internal fun BigCheckmarkPreview() {
- ElementPreview {
- Box(
- modifier = Modifier.padding(10.dp),
- contentAlignment = Alignment.Center,
- ) {
- BigCheckmark()
- }
- }
-}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Bloom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Bloom.kt
deleted file mode 100644
index fd4b380fdc..0000000000
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Bloom.kt
+++ /dev/null
@@ -1,578 +0,0 @@
-/*
- * Copyright 2023, 2024 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.
- */
-
-package io.element.android.libraries.designsystem.components
-
-import android.graphics.Bitmap
-import android.graphics.Typeface
-import android.os.Build
-import android.text.TextPaint
-import androidx.annotation.FloatRange
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.consumeWindowInsets
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.TopAppBarDefaults
-import androidx.compose.material3.rememberTopAppBarState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.draw.drawWithCache
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.geometry.center
-import androidx.compose.ui.graphics.BlendMode
-import androidx.compose.ui.graphics.ClipOp
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.ImageBitmap
-import androidx.compose.ui.graphics.LinearGradientShader
-import androidx.compose.ui.graphics.Paint
-import androidx.compose.ui.graphics.Path
-import androidx.compose.ui.graphics.RadialGradientShader
-import androidx.compose.ui.graphics.ShaderBrush
-import androidx.compose.ui.graphics.asAndroidBitmap
-import androidx.compose.ui.graphics.asImageBitmap
-import androidx.compose.ui.graphics.drawscope.DrawScope
-import androidx.compose.ui.graphics.drawscope.clipPath
-import androidx.compose.ui.graphics.nativeCanvas
-import androidx.compose.ui.graphics.painter.BitmapPainter
-import androidx.compose.ui.graphics.toArgb
-import androidx.compose.ui.input.nestedscroll.nestedScroll
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalFontFamilyResolver
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.font.FontStyle
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.rememberTextMeasurer
-import androidx.compose.ui.tooling.preview.PreviewParameter
-import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.DpOffset
-import androidx.compose.ui.unit.DpSize
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.isSpecified
-import androidx.compose.ui.unit.toOffset
-import androidx.compose.ui.unit.toSize
-import coil3.SingletonImageLoader
-import coil3.request.ImageRequest
-import coil3.request.allowHardware
-import coil3.toBitmap
-import com.airbnb.android.showkase.annotation.ShowkaseComposable
-import com.vanniktech.blurhash.BlurHash
-import io.element.android.compound.theme.ElementTheme
-import io.element.android.compound.tokens.generated.CompoundIcons
-import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
-import io.element.android.libraries.designsystem.components.avatar.Avatar
-import io.element.android.libraries.designsystem.components.avatar.AvatarData
-import io.element.android.libraries.designsystem.components.avatar.AvatarSize
-import io.element.android.libraries.designsystem.components.avatar.AvatarType
-import io.element.android.libraries.designsystem.preview.ElementPreview
-import io.element.android.libraries.designsystem.preview.PreviewGroup
-import io.element.android.libraries.designsystem.preview.PreviewsDayNight
-import io.element.android.libraries.designsystem.text.toDp
-import io.element.android.libraries.designsystem.theme.components.Icon
-import io.element.android.libraries.designsystem.theme.components.MediumTopAppBar
-import io.element.android.libraries.designsystem.theme.components.Scaffold
-import io.element.android.libraries.designsystem.theme.components.Text
-import io.element.android.libraries.designsystem.utils.CommonDrawables
-import kotlinx.collections.immutable.ImmutableList
-import kotlinx.collections.immutable.persistentListOf
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import kotlin.math.max
-import kotlin.math.roundToInt
-
-/**
- * Default bloom configuration values.
- */
-object BloomDefaults {
- /**
- * Number of components to use with BlurHash to generate the blur effect.
- * Larger values mean more detailed blurs.
- */
- const val HASH_COMPONENTS = 4
- const val ENCODE_SIZE_PX = 20
- const val DECODE_SIZE_PX = 5
-
- /** Default bloom layers. */
- @Composable
- fun defaultLayers() = persistentListOf(
- // Bottom layer
- if (ElementTheme.isLightTheme) {
- BloomLayer(0.2f, BlendMode.Hardlight)
- } else {
- BloomLayer(0.5f, BlendMode.Exclusion)
- },
- // Top layer
- BloomLayer(if (ElementTheme.isLightTheme) 0.8f else 0.2f, BlendMode.Color),
- )
-}
-
-/**
- * Bloom layer configuration.
- * @param alpha The alpha value to apply to the layer.
- * @param blendMode The blend mode to apply to the layer.
- */
-data class BloomLayer(
- val alpha: Float,
- val blendMode: BlendMode,
-)
-
-/**
- * Bloom effect modifier. Applies a bloom effect to the component.
- * @param hash The BlurHash to use as the bloom source.
- * @param background The background color to use for the bloom effect. Since we use blend modes it must be non-transparent.
- * @param blurSize The size of the bloom effect. If not specified the bloom effect will be the size of the component.
- * @param offset The offset to use for the bloom effect. If not specified the bloom effect will be centered on the component.
- * @param clipToSize The size to use for clipping the bloom effect. If not specified the bloom effect will not be clipped.
- * @param layerConfiguration The configuration for the bloom layers. If not specified the default layers configuration will be used.
- * @param bottomSoftEdgeColor The color to use for the bottom soft edge. If not specified the [background] color will be used.
- * @param bottomSoftEdgeHeight The height of the bottom soft edge. If not specified the bottom soft edge will not be drawn.
- * @param bottomSoftEdgeAlpha The alpha value to apply to the bottom soft edge.
- * @param alpha The alpha value to apply to the bloom effect.
- */
-@SuppressWarnings("ModifierComposed")
-fun Modifier.bloom(
- hash: String?,
- background: Color,
- blurSize: DpSize = DpSize.Unspecified,
- offset: DpOffset = DpOffset.Unspecified,
- clipToSize: DpSize = DpSize.Unspecified,
- layerConfiguration: ImmutableList? = null,
- bottomSoftEdgeColor: Color = background,
- bottomSoftEdgeHeight: Dp = 40.dp,
- @FloatRange(from = 0.0, to = 1.0)
- bottomSoftEdgeAlpha: Float = 1.0f,
- @FloatRange(from = 0.0, to = 1.0)
- alpha: Float = 1f,
-) = composed {
- val defaultLayers = BloomDefaults.defaultLayers()
- val layers = layerConfiguration ?: defaultLayers
- // Bloom only works on API 29+
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return@composed this
- if (hash == null) return@composed this
-
- val hashedBitmap = remember(hash) {
- BlurHash.decode(
- blurHash = hash,
- width = BloomDefaults.DECODE_SIZE_PX,
- height = BloomDefaults.DECODE_SIZE_PX,
- )?.asImageBitmap()
- } ?: return@composed this
- val density = LocalDensity.current
- val pixelSize = remember(blurSize, density) { blurSize.toIntSize(density) }
- val clipToPixelSize = remember(clipToSize, density) { clipToSize.toIntSize(density) }
- val bottomSoftEdgeHeightPixels = remember(bottomSoftEdgeHeight, density) { with(density) { bottomSoftEdgeHeight.roundToPx() } }
- val isRTL = LocalLayoutDirection.current == LayoutDirection.Rtl
- drawWithCache {
- val dstSize = if (pixelSize != IntSize.Zero) {
- pixelSize
- } else {
- IntSize(size.width.toInt(), size.height.toInt())
- }
- // Calculate where to place the center of the bloom effect
- val centerOffset = if (offset.isSpecified) {
- if (isRTL) {
- IntOffset(
- size.width.roundToInt() - offset.x.roundToPx(),
- size.height.roundToInt() - offset.y.roundToPx(),
- )
- } else {
- IntOffset(
- offset.x.roundToPx(),
- offset.y.roundToPx(),
- )
- }
- } else {
- IntOffset(
- size.center.x.toInt(),
- size.center.y.toInt(),
- )
- }
- // Calculate the offset to draw the different layers and apply clipping
- // This offset is applied to place the top left corner of the bloom effect
- val layersOffset = if (offset.isSpecified) {
- // Offsets the layers so the center of the bloom effect is at the provided offset value
- IntOffset(
- centerOffset.x - dstSize.width / 2,
- centerOffset.y - dstSize.height / 2,
- )
- } else {
- // Places the layers at the center of the component
- IntOffset.Zero
- }
- val radius = max(dstSize.width, dstSize.height).toFloat() / 2
- val circularGradientShader = RadialGradientShader(
- centerOffset.toOffset(),
- radius,
- listOf(Color.Red, Color.Transparent),
- listOf(0f, 1f)
- )
- val circularGradientBrush = ShaderBrush(circularGradientShader)
- val bottomEdgeGradient = LinearGradientShader(
- from = IntOffset(0, clipToPixelSize.height - bottomSoftEdgeHeightPixels).toOffset(),
- to = IntOffset(0, clipToPixelSize.height).toOffset(),
- listOf(Color.Transparent, bottomSoftEdgeColor),
- listOf(0f, 1f)
- )
- val bottomEdgeGradientBrush = ShaderBrush(bottomEdgeGradient)
- onDrawBehind {
- if (dstSize != IntSize.Zero) {
- val circleClipPath = Path().apply {
- addOval(Rect(centerOffset.toOffset(), radius - 1))
- }
- // Clip the external radius of bloom gradient too, otherwise we have a 1px border
- clipPath(circleClipPath, clipOp = ClipOp.Intersect) {
- // Draw the bloom layers
- drawWithLayer {
- // Clip rect to the provided size if needed
- if (clipToPixelSize != IntSize.Zero) {
- drawContext.canvas.clipRect(Rect(Offset.Zero, clipToPixelSize.toSize()), ClipOp.Intersect)
- }
- // Draw background color for blending
- drawRect(background, size = pixelSize.toSize())
- // Draw layers
- for (layer in layers) {
- drawImage(
- hashedBitmap,
- srcSize = IntSize(BloomDefaults.HASH_COMPONENTS, BloomDefaults.HASH_COMPONENTS),
- dstSize = dstSize,
- dstOffset = layersOffset,
- alpha = layer.alpha * alpha,
- blendMode = layer.blendMode,
- )
- }
- // Mask the layers erasing the outer radius using the gradient brush
- drawCircle(
- circularGradientBrush,
- radius,
- centerOffset.toOffset(),
- blendMode = BlendMode.DstIn
- )
- }
- }
- // Draw the bottom soft edge
- drawRect(
- bottomEdgeGradientBrush,
- topLeft = IntOffset(0, clipToPixelSize.height - bottomSoftEdgeHeight.roundToPx()).toOffset(),
- size = IntSize(pixelSize.width, bottomSoftEdgeHeight.roundToPx()).toSize(),
- alpha = bottomSoftEdgeAlpha
- )
- }
- }
- }
-}
-
-/**
- * Bloom effect modifier for avatars. Applies a bloom effect to the component.
- * @param avatarData The avatar data to use as the bloom source.
- * If the avatar data has a URL it will be used as the bloom source, otherwise the initials will be used.
- * @param background The background color to use for the bloom effect. Since we use blend modes it must be non-transparent.
- * @param blurSize The size of the bloom effect. If not specified the bloom effect will be the size of the component.
- * @param offset The offset to use for the bloom effect. If not specified the bloom effect will be centered on the component.
- * @param clipToSize The size to use for clipping the bloom effect. If not specified the bloom effect will not be clipped.
- * @param bottomSoftEdgeColor The color to use for the bottom soft edge. If not specified the [background] color will be used.
- * @param bottomSoftEdgeHeight The height of the bottom soft edge. If not specified the bottom soft edge will not be drawn.
- * @param bottomSoftEdgeAlpha The alpha value to apply to the bottom soft edge.
- * @param alpha The alpha value to apply to the bloom effect.
- */
-@SuppressWarnings("ModifierComposed")
-fun Modifier.avatarBloom(
- avatarData: AvatarData,
- background: Color,
- blurSize: DpSize = DpSize.Unspecified,
- offset: DpOffset = DpOffset.Unspecified,
- clipToSize: DpSize = DpSize.Unspecified,
- bottomSoftEdgeColor: Color = background,
- bottomSoftEdgeHeight: Dp = 40.dp,
- @FloatRange(from = 0.0, to = 1.0)
- bottomSoftEdgeAlpha: Float = 1.0f,
- @FloatRange(from = 0.0, to = 1.0)
- alpha: Float = 1f,
-) = composed {
- // Bloom only works on API 29+
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return@composed this
-
- // Request the avatar contents to use as the bloom source
- val context = LocalContext.current
- if (avatarData.url != null) {
- val painterRequest = remember(avatarData) {
- ImageRequest.Builder(context)
- .data(avatarData)
- // Allow cache and default dispatchers
- .defaults(ImageRequest.Defaults())
- // Needed to be able to read pixels from the Bitmap for the hash
- .allowHardware(false)
- // Reduce size so it loads faster for large avatars
- .size(BloomDefaults.ENCODE_SIZE_PX, BloomDefaults.ENCODE_SIZE_PX)
- .build()
- }
-
- // By making it saveable, we'll 'cache' the previous bloom effect until a new one is loaded
- var blurHash by rememberSaveable(avatarData) { mutableStateOf(null) }
- LaunchedEffect(avatarData) {
- withContext(Dispatchers.IO) {
- val bitmap = SingletonImageLoader.get(context)
- .execute(painterRequest)
- .image
- ?.toBitmap()
- ?: return@withContext
- blurHash = BlurHash.encode(
- bitmap = bitmap,
- componentX = BloomDefaults.HASH_COMPONENTS,
- componentY = BloomDefaults.HASH_COMPONENTS,
- )
- }
- }
-
- bloom(
- hash = blurHash,
- background = background,
- blurSize = blurSize,
- offset = offset,
- clipToSize = clipToSize,
- bottomSoftEdgeColor = bottomSoftEdgeColor,
- bottomSoftEdgeHeight = bottomSoftEdgeHeight,
- bottomSoftEdgeAlpha = bottomSoftEdgeAlpha,
- alpha = alpha,
- )
- } else {
- // There is no URL so we'll generate an avatar with the initials and use that as the bloom source
- val avatarColors = AvatarColorsProvider.provide(avatarData.id)
- val initialsBitmap = initialsBitmap(
- width = BloomDefaults.ENCODE_SIZE_PX.toDp(),
- height = BloomDefaults.ENCODE_SIZE_PX.toDp(),
- text = avatarData.initialLetter,
- textColor = avatarColors.foreground,
- backgroundColor = avatarColors.background,
- )
- val hash = remember(avatarData, avatarColors) {
- BlurHash.encode(
- bitmap = initialsBitmap.asAndroidBitmap(),
- componentX = BloomDefaults.HASH_COMPONENTS,
- componentY = BloomDefaults.HASH_COMPONENTS,
- )
- }
- bloom(
- hash = hash,
- background = background,
- blurSize = blurSize,
- offset = offset,
- clipToSize = clipToSize,
- bottomSoftEdgeColor = bottomSoftEdgeColor,
- bottomSoftEdgeHeight = bottomSoftEdgeHeight,
- bottomSoftEdgeAlpha = bottomSoftEdgeAlpha,
- alpha = alpha,
- )
- }
-}
-
-// Used to create a Bitmap version of the initials avatar
-@Composable
-private fun initialsBitmap(
- text: String,
- backgroundColor: Color,
- textColor: Color,
- width: Dp = 32.dp,
- height: Dp = 32.dp,
-): ImageBitmap = with(LocalDensity.current) {
- val backgroundPaint = remember(backgroundColor) {
- Paint().also { it.color = backgroundColor }
- }
- val resolver: FontFamily.Resolver = LocalFontFamilyResolver.current
- val fontSize = remember { height.toSp() / 2 }
- val typeface: Typeface = remember(resolver) {
- resolver.resolve(
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Bold,
- fontStyle = FontStyle.Normal,
- )
- }.value as Typeface
- val textPaint = remember(textColor, typeface) {
- TextPaint().apply {
- color = textColor.toArgb()
- textSize = fontSize.toPx()
- this.typeface = typeface
- }
- }
- val textMeasurer = rememberTextMeasurer()
- val result = remember(text) { textMeasurer.measure(text, TextStyle.Default.copy(fontSize = fontSize)) }
- val centerPx = remember(width, height) { IntOffset(width.roundToPx() / 2, height.roundToPx() / 2) }
- remember(text, width, height, backgroundColor, textColor) {
- val bitmap = Bitmap.createBitmap(width.roundToPx(), height.roundToPx(), Bitmap.Config.ARGB_8888).asImageBitmap()
- androidx.compose.ui.graphics.Canvas(bitmap).also { canvas ->
- canvas.drawCircle(centerPx.toOffset(), width.toPx() / 2, backgroundPaint)
- canvas.nativeCanvas.drawText(text, centerPx.x.toFloat() - result.size.width / 2, centerPx.y * 2f - result.size.height / 2 - 4, textPaint)
- }
- bitmap
- }
-}
-
-// Translates DP sizes into pixel sizes, taking into account unspecified values
-private fun DpSize.toIntSize(density: Density) = with(density) {
- if (isSpecified) {
- IntSize(width.roundToPx(), height.roundToPx())
- } else {
- IntSize.Zero
- }
-}
-
-/**
- * Helper to draw to a canvas using layers. This allows us to apply clipping to those layers only.
- */
-fun DrawScope.drawWithLayer(block: DrawScope.() -> Unit) {
- with(drawContext.canvas.nativeCanvas) {
- val checkPoint = saveLayer(null, null)
- block()
- restoreToCount(checkPoint)
- }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@PreviewsDayNight
-@ShowkaseComposable(group = PreviewGroup.Bloom)
-@Composable
-internal fun BloomPreview() {
- val blurhash = "eePn{tI?xExEja}ooKWWodjtNJoKR,j@a|sBWpS3WDbGazoKWWWWj@"
- var topAppBarHeight by remember { mutableIntStateOf(-1) }
- val topAppBarState = rememberTopAppBarState()
- val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(topAppBarState)
- ElementPreview(
- drawableFallbackForImages = CommonDrawables.sample_avatar,
- ) {
- Scaffold(
- modifier = Modifier
- .fillMaxSize()
- .nestedScroll(scrollBehavior.nestedScrollConnection),
- topBar = {
- Box {
- MediumTopAppBar(
- modifier = Modifier
- .onSizeChanged { size ->
- topAppBarHeight = size.height
- }
- .bloom(
- hash = blurhash,
- background = ElementTheme.colors.bgCanvasDefault,
- blurSize = DpSize(430.dp, 430.dp),
- offset = DpOffset(24.dp, 24.dp),
- clipToSize = if (topAppBarHeight > 0) DpSize(430.dp, topAppBarHeight.toDp()) else DpSize.Zero,
- ),
- colors = TopAppBarDefaults.largeTopAppBarColors(
- containerColor = Color.Transparent,
- scrolledContainerColor = Color.Black.copy(alpha = 0.05f),
- ),
- navigationIcon = {
- Avatar(
- avatarData = AvatarData(
- id = "sample-avatar",
- name = "sample",
- url = "aURL",
- size = AvatarSize.CurrentUserTopBar,
- ),
- avatarType = AvatarType.User,
- )
- },
- actions = {
- IconButton(onClick = {}) {
- Icon(
- imageVector = CompoundIcons.ShareAndroid(),
- contentDescription = null,
- )
- }
- },
- title = {
- Text("Title")
- },
- scrollBehavior = scrollBehavior,
- )
- }
- },
- ) { paddingValues ->
- Column(
- modifier = Modifier
- .padding(paddingValues)
- .consumeWindowInsets(paddingValues)
- .fillMaxSize()
- .verticalScroll(rememberScrollState()),
- ) {
- repeat(20) {
- Text("Content", modifier = Modifier.padding(vertical = 20.dp))
- }
- }
- }
- }
-}
-
-class InitialsColorIntProvider : PreviewParameterProvider {
- override val values: Sequence
- get() = sequenceOf(0, 1, 2, 3, 4, 5, 6, 7)
-}
-
-@PreviewsDayNight
-@Composable
-@ShowkaseComposable(group = PreviewGroup.Bloom)
-internal fun BloomInitialsPreview(@PreviewParameter(InitialsColorIntProvider::class) color: Int) {
- ElementPreview {
- val avatarColors = AvatarColorsProvider.provide("$color")
- val bitmap = initialsBitmap(text = "F", backgroundColor = avatarColors.background, textColor = avatarColors.foreground)
- val hash = BlurHash.encode(
- bitmap = bitmap.asAndroidBitmap(),
- componentX = BloomDefaults.HASH_COMPONENTS,
- componentY = BloomDefaults.HASH_COMPONENTS,
- )
- Box(
- modifier = Modifier
- .size(256.dp)
- .bloom(
- hash = hash,
- background = if (ElementTheme.isLightTheme) {
- // Workaround to display a very subtle bloom for avatars with very soft colors
- Color(0xFFF9F9F9)
- } else {
- ElementTheme.colors.bgCanvasDefault
- },
- bottomSoftEdgeColor = ElementTheme.colors.bgCanvasDefault,
- blurSize = DpSize(256.dp, 256.dp),
- ),
- contentAlignment = Alignment.Center
- ) {
- Image(
- modifier = Modifier
- .size(32.dp)
- .clip(CircleShape),
- painter = BitmapPainter(bitmap),
- contentDescription = null
- )
- }
- }
-}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/MainActionButton.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/MainActionButton.kt
index 537c1f639f..56a0a756c9 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/MainActionButton.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/MainActionButton.kt
@@ -42,7 +42,6 @@ fun MainActionButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
- contentDescription: String = title,
) {
val ripple = ripple(bounded = false)
val interactionSource = remember { MutableInteractionSource() }
@@ -58,8 +57,8 @@ fun MainActionButton(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
+ contentDescription = null,
imageVector = imageVector,
- contentDescription = contentDescription,
tint = if (enabled) LocalContentColor.current else ElementTheme.colors.iconDisabled,
)
Spacer(modifier = Modifier.height(14.dp))
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/SuperButton.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/SuperButton.kt
index a53955afc2..90dd64d59d 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/SuperButton.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/SuperButton.kt
@@ -36,10 +36,8 @@ import androidx.compose.ui.graphics.ShaderBrush
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp
-import io.element.android.compound.annotations.CoreColorToken
import io.element.android.compound.theme.ElementTheme
-import io.element.android.compound.tokens.generated.internal.DarkColorTokens
-import io.element.android.compound.tokens.generated.internal.LightColorTokens
+import io.element.android.libraries.designsystem.colors.gradientActionColors
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.LocalBuildMeta
@@ -47,7 +45,6 @@ import io.element.android.libraries.designsystem.theme.components.ButtonSize
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.designsystem.theme.components.lowHorizontalPaddingValue
-@OptIn(CoreColorToken::class)
@Composable
fun SuperButton(
onClick: () -> Unit,
@@ -66,34 +63,21 @@ fun SuperButton(
ButtonSize.Small -> PaddingValues(horizontal = 16.dp, vertical = 5.dp)
}
}
- val isLightTheme = ElementTheme.isLightTheme
val colors = if (LocalBuildMeta.current.isEnterpriseBuild) {
listOf(
ElementTheme.colors.textActionAccent,
ElementTheme.colors.textActionAccent,
)
} else {
- remember(isLightTheme) {
- if (isLightTheme) {
- listOf(
- LightColorTokens.colorBlue900,
- LightColorTokens.colorGreen1100,
- )
- } else {
- listOf(
- DarkColorTokens.colorBlue900,
- DarkColorTokens.colorGreen1100,
- )
- }
- }
+ gradientActionColors()
}
val shaderBrush = remember(colors) {
object : ShaderBrush() {
override fun createShader(size: Size): Shader {
return LinearGradientShader(
- from = Offset(0f, size.height),
- to = Offset(size.width, 0f),
+ from = Offset(0f, 0f),
+ to = Offset(0f, size.height),
colors = colors,
)
}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/Gradient.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/Gradient.kt
new file mode 100644
index 0000000000..5d6f91e4b4
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/Gradient.kt
@@ -0,0 +1,97 @@
+/*
+ * 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.
+ */
+
+package io.element.android.libraries.designsystem.modifiers
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.libraries.designsystem.colors.gradientSubtleColors
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+import io.element.android.libraries.designsystem.theme.LocalBuildMeta
+
+/**
+ * Ref: https://www.figma.com/design/kcnHxunG1LDWXsJhaNuiHz/ER-145--Workspaces-V1?node-id=1141-24692
+ */
+@Stable
+@Composable
+fun Modifier.backgroundVerticalGradient(
+ isVisible: Boolean = true,
+ isEnterpriseBuild: Boolean = LocalBuildMeta.current.isEnterpriseBuild,
+): Modifier {
+ if (!isVisible) return this
+ return background(
+ brush = Brush.verticalGradient(
+ colorStops = subtleColorStops(isEnterpriseBuild),
+ ),
+ )
+}
+
+@Composable
+fun subtleColorStops(
+ isEnterpriseBuild: Boolean = LocalBuildMeta.current.isEnterpriseBuild,
+): Array> {
+ return buildList {
+ if (isEnterpriseBuild) {
+ // For enterprise builds, ensure that we are theming the gradient
+ add(0f to ElementTheme.colors.textActionAccent.copy(alpha = 0.5f))
+ add(0.75f to ElementTheme.colors.bgCanvasDefault)
+ add(1f to Color.Transparent)
+ } else {
+ val colors = gradientSubtleColors()
+ colors.forEachIndexed { index, color ->
+ add(index.toFloat() / (colors.size - 1) to color)
+ }
+ }
+ }.toTypedArray()
+}
+
+@PreviewsDayNight
+@Composable
+internal fun BackgroundVerticalGradientPreview() = ElementPreview {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(height = 100.dp)
+ .backgroundVerticalGradient()
+ )
+}
+
+@PreviewsDayNight
+@Composable
+internal fun BackgroundVerticalGradientEnterprisePreview() = ElementPreview {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(height = 100.dp)
+ .backgroundVerticalGradient(
+ isEnterpriseBuild = true,
+ )
+ )
+}
+
+@PreviewsDayNight
+@Composable
+internal fun BackgroundVerticalGradientDisabledPreview() = ElementPreview {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(height = 100.dp)
+ .backgroundVerticalGradient(
+ isVisible = false,
+ )
+ )
+}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/PreviewGroup.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/PreviewGroup.kt
index 79853f5983..aa2b4dd761 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/PreviewGroup.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/PreviewGroup.kt
@@ -11,7 +11,6 @@ package io.element.android.libraries.designsystem.preview
object PreviewGroup {
const val AppBars = "App Bars"
const val Avatars = "Avatars"
- const val Bloom = "Bloom"
const val BottomSheets = "Bottom Sheets"
const val Buttons = "Buttons"
const val DateTimePickers = "DateTime pickers"
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt
index 5576a56519..8f6224eca9 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt
@@ -22,14 +22,14 @@ import kotlinx.collections.immutable.persistentMapOf
/**
* Room list.
*/
-@Composable
-fun ElementTheme.roomListRoomName() = colors.textPrimary
+val SemanticColors.roomListRoomName
+ get() = textPrimary
-@Composable
-fun ElementTheme.roomListRoomMessage() = colors.textSecondary
+val SemanticColors.roomListRoomMessage
+ get() = textSecondary
-@Composable
-fun ElementTheme.roomListRoomMessageDate() = colors.textSecondary
+val SemanticColors.roomListRoomMessageDate
+ get() = textSecondary
val SemanticColors.unreadIndicator
get() = iconAccentTertiary
@@ -52,11 +52,6 @@ val SemanticColors.messageFromOtherBackground
val SemanticColors.progressIndicatorTrackColor
get() = if (isLight) LightColorTokens.colorAlphaGray500 else DarkColorTokens.colorAlphaGray500
-// This color is not present in Semantic color, so put hard-coded value for now
-@OptIn(CoreColorToken::class)
-val SemanticColors.iconSuccessPrimaryBackground
- get() = if (isLight) LightColorTokens.colorGreen300 else DarkColorTokens.colorGreen300
-
// This color is not present in Semantic color, so put hard-coded value for now
@OptIn(CoreColorToken::class)
val SemanticColors.bgSubtleTertiary
@@ -71,57 +66,6 @@ val SemanticColors.temporaryColorBgSpecial
val SemanticColors.pinDigitBg
get() = if (isLight) LightColorTokens.colorGray300 else DarkColorTokens.colorGray400
-@OptIn(CoreColorToken::class)
-val SemanticColors.currentUserMentionPillText
- get() = if (isLight) LightColorTokens.colorGreen1100 else DarkColorTokens.colorGreen1100
-
-val SemanticColors.currentUserMentionPillBackground
- get() = if (isLight) {
- // We want LightDesignTokens.colorGreenAlpha400
- Color(0x3b07b661)
- } else {
- // We want DarkDesignTokens.colorGreenAlpha500
- Color(0xff003d29)
- }
-
-val SemanticColors.mentionPillText
- get() = textPrimary
-
-val SemanticColors.mentionPillBackground
- get() = if (isLight) {
- // We want LightDesignTokens.colorGray400
- Color(0x1f052e61)
- } else {
- // We want DarkDesignTokens.colorGray500
- Color(0x26f4f7fa)
- }
-
-@OptIn(CoreColorToken::class)
-val SemanticColors.bigCheckmarkBorderColor
- get() = if (isLight) LightColorTokens.colorGray400 else DarkColorTokens.colorGray400
-
-@OptIn(CoreColorToken::class)
-val SemanticColors.highlightedMessageBackgroundColor
- get() = if (isLight) LightColorTokens.colorGreen300 else DarkColorTokens.colorGreen300
-
-// Badge colors
-
-@OptIn(CoreColorToken::class)
-val SemanticColors.badgeNeutralBackgroundColor
- get() = if (isLight) LightColorTokens.colorAlphaGray300 else DarkColorTokens.colorAlphaGray300
-
-@OptIn(CoreColorToken::class)
-val SemanticColors.badgeNeutralContentColor
- get() = if (isLight) LightColorTokens.colorGray1100 else DarkColorTokens.colorGray1100
-
-@OptIn(CoreColorToken::class)
-val SemanticColors.badgeNegativeBackgroundColor
- get() = if (isLight) LightColorTokens.colorAlphaRed300 else DarkColorTokens.colorAlphaRed300
-
-@OptIn(CoreColorToken::class)
-val SemanticColors.badgeNegativeContentColor
- get() = if (isLight) LightColorTokens.colorRed1100 else DarkColorTokens.colorRed1100
-
@OptIn(CoreColorToken::class)
val SemanticColors.pinnedMessageBannerIndicator
get() = if (isLight) LightColorTokens.colorAlphaGray600 else DarkColorTokens.colorAlphaGray600
@@ -137,18 +81,15 @@ internal fun ColorAliasesPreview() = ElementPreview {
backgroundColor = Color.Black,
foregroundColor = Color.White,
colors = persistentMapOf(
- "roomListRoomName" to ElementTheme.roomListRoomName(),
- "roomListRoomMessage" to ElementTheme.roomListRoomMessage(),
- "roomListRoomMessageDate" to ElementTheme.roomListRoomMessageDate(),
+ "roomListRoomName" to ElementTheme.colors.roomListRoomName,
+ "roomListRoomMessage" to ElementTheme.colors.roomListRoomMessage,
+ "roomListRoomMessageDate" to ElementTheme.colors.roomListRoomMessageDate,
"unreadIndicator" to ElementTheme.colors.unreadIndicator,
"placeholderBackground" to ElementTheme.colors.placeholderBackground,
"messageFromMeBackground" to ElementTheme.colors.messageFromMeBackground,
"messageFromOtherBackground" to ElementTheme.colors.messageFromOtherBackground,
"progressIndicatorTrackColor" to ElementTheme.colors.progressIndicatorTrackColor,
"temporaryColorBgSpecial" to ElementTheme.colors.temporaryColorBgSpecial,
- "iconSuccessPrimaryBackground" to ElementTheme.colors.iconSuccessPrimaryBackground,
- "bigCheckmarkBorderColor" to ElementTheme.colors.bigCheckmarkBorderColor,
- "highlightedMessageBackgroundColor" to ElementTheme.colors.highlightedMessageBackgroundColor,
)
)
}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/DrawScope.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/DrawScope.kt
new file mode 100644
index 0000000000..4db7082720
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/DrawScope.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+package io.element.android.libraries.designsystem.utils
+
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.graphics.nativeCanvas
+
+/**
+ * Helper to draw to a canvas using layers. This allows us to apply clipping to those layers only.
+ */
+fun DrawScope.drawWithLayer(block: DrawScope.() -> Unit) {
+ with(drawContext.canvas.nativeCanvas) {
+ val checkPoint = saveLayer(null, null)
+ block()
+ restoreToCount(checkPoint)
+ }
+}
diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt
index 81c33318c1..e74304b735 100644
--- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt
+++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt
@@ -175,4 +175,13 @@ enum class FeatureFlags(
// False so it's displayed in the developer options screen
isFinished = false,
),
+ SharePos(
+ key = "feature.share_pos",
+ title = "Share pos in sliding sync",
+ description = "Keep the sliding sync pos to make initial syncs faster. Requires an app restart to take effect." +
+ "\n\nWARNING: this may cause issues with syncs.",
+ defaultValue = { false },
+ // False so it's displayed in the developer options screen
+ isFinished = false,
+ ),
}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/exception/NotificationResolverException.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/exception/NotificationResolverException.kt
new file mode 100644
index 0000000000..ab251f19d8
--- /dev/null
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/exception/NotificationResolverException.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+
+package io.element.android.libraries.matrix.api.exception
+
+/**
+ * Exceptions that can occur while resolving the events associated to push notifications.
+ */
+sealed class NotificationResolverException : Exception() {
+ /**
+ * The event was not found by the notification service.
+ */
+ data object EventNotFound : NotificationResolverException()
+
+ /**
+ * The event was found but it was filtered out by the notification service.
+ */
+ data object EventFilteredOut : NotificationResolverException()
+
+ /**
+ * An unexpected error occurred while trying to resolve the event.
+ */
+ data class UnknownError(override val message: String) : NotificationResolverException()
+}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt
index f90b70171e..338193ed44 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt
@@ -68,7 +68,6 @@ sealed interface NotificationContent {
) : MessageLike
data object RoomEncrypted : MessageLike
- data object UnableToResolve : MessageLike
data class RoomMessage(
val senderId: UserId,
val messageType: MessageType
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationService.kt
index 1e1c8b7fb7..ddec326cef 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationService.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationService.kt
@@ -10,6 +10,19 @@ package io.element.android.libraries.matrix.api.notification
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
+/**
+ * Represents the resolution state of an attempt to retrieve notification data for a set of event ids.
+ * The outer [Result] indicates the success or failure of the setup to retrieve notifications.
+ * The inner [Result] for each [EventId] in the map indicates whether the notification data was successfully retrieved or if there was an error.
+ */
+typealias GetNotificationDataResult = Result