diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 95f322a2e4..9ed1919e0e 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -52,6 +52,7 @@ Uncomment this markdown table below and edit the last line `|||`:
+- [ ] I am aware of the [etiquette](https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md#etiquette).
- This PR was made with the help of AI:
- [ ] Yes. In this case, please request a review by Copilot.
- [ ] No.
diff --git a/AGENTS.md b/AGENTS.md
index 454c347a9f..d206be98c6 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -44,11 +44,14 @@ PRs must meet these rules.
## Pull Request Guidelines
-- Use sentence-style commit/PR messages (no conventional commits).
-- Apply exactly **one** `PR-` label for changelog categorization.
-- PR title = changelog entry — make it descriptive; no "Fixes #…" prefixes.
-- Include screenshots or screen recordings for any UI changes.
-- Keep PRs focused; split changes over 1000 lines.
+- Sentence-style titles (no conventional commits).
+- Exactly one `pr-` label (see `.github/release.yml`).
+- Title = changelog entry — descriptive, no "Fixes #…".
+- Leave description template for the developer. Redirect them to the [contributing etiquette](CONTRIBUTING.md#etiquette).
+- Screenshots/videos for visual changes.
+- 500 additions max — split large changes.
+- Commits need a title and description; no tiny or massive commits.
+- No history rewrites.
---
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 0126d89a9c..ec131aa65f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -6,7 +6,8 @@
* [I want to help translating Element](#i-want-to-help-translating-element)
* [I want to fix a bug](#i-want-to-fix-a-bug)
* [I want to add a new feature or enhancement](#i-want-to-add-a-new-feature-or-enhancement)
-* [Developer onboarding](#developer-onboarding)
+ * [Etiquette](#etiquette)
+ * [Developer onboarding](#developer-onboarding)
* [Submitting the PRs](#submitting-the-prs)
* [Android Studio settings](#android-studio-settings)
* [Compilation](#compilation)
@@ -63,7 +64,27 @@ Once we know that you want to contribute and have confirmed that the new feature
Only once all of the above is met should you open a PR with your proposed changes.
-## Developer onboarding
+### Etiquette
+
+* As stated above all significant changes should be communicated through an issue before opening a PR
+* We are happy to receive contributions but features always require maintenance, so depending on the change we might not be willing to accept it
+* We are also fine with AI led contributions within reasonable bounds
+ * You are completely responsible for the quality of the PR
+ * If the PR doesn't show minimal effort (code does not compile, code does not work as expected, etc.) on your part it will be closed
+ * Please write the description yourself, we don't have the bandwidth to read LLM essays. The code needs to speak for itself.
+* We use git for version control and GitHub for reviews, so in order to make everybody's life easier please:
+ * Keep the existing pull request template
+ * Don't submit large PRs, especially if not previously talked about. Anything above 200 lines is large (excluding generated code e.g. tests, translations, mocks)
+ * Please don't open unfinished PRs and expect us to fill in the details
+ * If you would like our opinion/direction on unfinished code please link your branch or idea in the ticket
+ * Please limit the number of commits in a single PR. We are perfectly happy with splitting work across multiple sessions as long as they're logically independant and show promise of progress (ideally expressed through a ticket)
+ * Each and every commit should stand on its own, clearly explaining what it does and why
+* Once a PR goes into review please don't rewrite the history unless agreed so with the reviewer.
+ * Tweaks and fixes following review can be directly committed (to be interactively rebased later) or as fixups
+
+*The reviewer's response time will generally match yours. Switching contexts is very hard so please act accordingly. You are responsible for making the reviewers job enjoyable!*
+
+### Developer onboarding
For a detailed overview of the project, see [Developer Onboarding](./docs/_developer_onboarding.md).
diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/SimpleLocationTrackingEffect.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/SimpleLocationTrackingEffect.kt
new file mode 100644
index 0000000000..7a7a1551cf
--- /dev/null
+++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/SimpleLocationTrackingEffect.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2026 Element Creations Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.location.impl.common.ui
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.snapshotFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import org.maplibre.compose.location.Location
+import org.maplibre.compose.location.UserLocationState
+import kotlin.math.abs
+
+/**
+ * Drop-in replacement for the library's LocationTrackingEffect.
+ * TODO remove once https://github.com/maplibre/maplibre-compose/issues/808 is fixed
+ */
+@Composable
+internal fun SimpleLocationTrackingEffect(
+ locationState: UserLocationState,
+ enabled: Boolean = true,
+ precision: Double = 0.00001,
+ onLocationChange: suspend (Location?) -> Unit,
+) {
+ val latestOnLocationChange by rememberUpdatedState(onLocationChange)
+
+ LaunchedEffect(locationState, enabled) {
+ if (!enabled) return@LaunchedEffect
+ val locationStateFlow = snapshotFlow { locationState.location }
+ locationStateFlow
+ .distinctUntilChanged { oldLocation, newLocation ->
+ if (oldLocation != null && newLocation != null) {
+ when {
+ abs(oldLocation.position.value.latitude - newLocation.position.value.latitude) >= precision -> false
+ abs(oldLocation.position.value.longitude - newLocation.position.value.longitude) >= precision -> false
+ else -> true
+ }
+ } else {
+ false
+ }
+ }
+ .collect { location ->
+ latestOnLocationChange(location)
+ }
+ }
+}
diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/UserLocationPuck.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/UserLocationPuck.kt
index 589ed87c6f..7bad736911 100644
--- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/UserLocationPuck.kt
+++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/UserLocationPuck.kt
@@ -18,11 +18,11 @@ import org.maplibre.compose.location.DesiredAccuracy
import org.maplibre.compose.location.LocationPuck
import org.maplibre.compose.location.LocationPuckColors
import org.maplibre.compose.location.LocationPuckSizes
-import org.maplibre.compose.location.LocationTrackingEffect
import org.maplibre.compose.location.UserLocationState
import org.maplibre.compose.location.rememberAndroidLocationProvider
import org.maplibre.compose.location.rememberNullLocationProvider
import org.maplibre.compose.location.rememberUserLocationState
+import org.maplibre.spatialk.units.extensions.meters
import kotlin.time.Duration.Companion.seconds
@Composable
@@ -31,22 +31,24 @@ fun UserLocationPuck(
locationState: UserLocationState,
trackUserLocation: Boolean,
) {
- LocationTrackingEffect(
+ SimpleLocationTrackingEffect(
locationState = locationState,
enabled = trackUserLocation,
- ) {
- val finalPosition = cameraState.position.copy(
- target = currentLocation.position,
- bearing = currentLocation.bearing ?: cameraState.position.bearing,
+ ) { currentLocation ->
+ val target = currentLocation?.position?.value ?: cameraState.position.target
+ val newPosition = cameraState.position.copy(
+ target = target,
+ // Force pointing to NORTH
+ bearing = 0.0,
zoom = cameraState.position.zoom.coerceAtLeast(MapDefaults.DEFAULT_ZOOM)
)
- cameraState.animateTo(finalPosition)
+ cameraState.animateTo(newPosition)
}
val location = locationState.location
if (location != null) {
LocationPuck(
idPrefix = "user-location",
- locationState = locationState,
+ location = location,
cameraState = cameraState,
accuracyThreshold = Float.POSITIVE_INFINITY,
showBearingAccuracy = false,
@@ -74,7 +76,7 @@ fun rememberUserLocationState(hasLocationPermission: Boolean): UserLocationState
rememberAndroidLocationProvider(
updateInterval = 5.seconds,
desiredAccuracy = DesiredAccuracy.High,
- minDistanceMeters = 5f,
+ minDistance = 5.meters,
)
}
return rememberUserLocationState(locationProvider)
diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/live/service/LiveLocationSharingService.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/live/service/LiveLocationSharingService.kt
index 4451febb19..a926f1255b 100644
--- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/live/service/LiveLocationSharingService.kt
+++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/live/service/LiveLocationSharingService.kt
@@ -36,6 +36,8 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import org.maplibre.compose.location.AndroidLocationProvider
import org.maplibre.compose.location.DesiredAccuracy
+import org.maplibre.spatialk.units.extensions.inMeters
+import org.maplibre.spatialk.units.extensions.meters
import timber.log.Timber
import kotlin.time.Duration.Companion.seconds
import io.element.android.features.location.api.Location as ApiLocation
@@ -91,7 +93,7 @@ class LiveLocationSharingService : Service() {
val locationProvider = AndroidLocationProvider(
context = applicationContext,
updateInterval = UPDATE_INTERVAL_IN_SECOND.seconds,
- minDistanceMeters = minDistanceMeters.toFloat(),
+ minDistance = minDistanceMeters.meters,
desiredAccuracy = DesiredAccuracy.Balanced,
coroutineScope = coroutineScope
)
@@ -100,9 +102,9 @@ class LiveLocationSharingService : Service() {
.filterNotNull()
.map { location ->
ApiLocation(
- lat = location.position.latitude,
- lon = location.position.longitude,
- accuracy = location.accuracy.toFloat(),
+ lat = location.position.value.latitude,
+ lon = location.position.value.longitude,
+ accuracy = location.position.accuracy?.inMeters?.toFloat(),
)
}
.onEach(coordinator::dispatch)
diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenter.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenter.kt
index 9f4c8333b8..6a769f8958 100644
--- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenter.kt
+++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenter.kt
@@ -100,8 +100,8 @@ class ShareLocationPresenter(
fun checkLocationConstraints() {
// No need to check SendLiveLocationPermissions here
val locationConstraints = checkLocationConstraints(permissionsState, locationActions, SendLiveLocationPermissions.GRANTED)
- dialogState = ShareLocationState.Dialog.Constraints(locationConstraints.toDialogState())
trackUserPosition = locationConstraints is LocationConstraintsCheck.Success
+ dialogState = ShareLocationState.Dialog.Constraints(locationConstraints.toDialogState())
}
suspend fun computeLiveLocationDialogState(): ShareLocationState.Dialog {
diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt
index 52400cd49d..68dcee36ac 100644
--- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt
+++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt
@@ -225,8 +225,8 @@ private fun BottomSheetContent(
state.eventSink(
ShareLocationEvent.ShareStaticLocation(
location = Location(
- lat = userLocation.position.latitude,
- lon = userLocation.position.longitude
+ lat = userLocation.position.value.latitude,
+ lon = userLocation.position.value.longitude
),
isPinned = false
)
diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt
index 231bc01b12..5d5295871c 100644
--- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt
+++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt
@@ -155,7 +155,9 @@ fun ShowLocationView(
val position = CameraPosition(
padding = sheetPaddings,
target = Position(locationShare.location.lon, locationShare.location.lat),
- zoom = MapDefaults.DEFAULT_ZOOM
+ // Force pointing to NORTH
+ bearing = 0.0,
+ zoom = cameraState.position.zoom.coerceAtLeast(MapDefaults.DEFAULT_ZOOM),
)
coroutineScope.launch {
cameraState.animateTo(finalPosition = position)
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt
index 6754d703a3..d3737f5fae 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt
@@ -550,7 +550,7 @@ class MessagesPresenter(
val replyToDetails = loadReplyDetails(targetEvent.eventId).map(permalinkParser)
val composerMode = MessageComposerMode.Reply(
replyToDetails = replyToDetails,
- hideImage = timelineProtectionState.hideMediaContent(targetEvent.eventId),
+ hideImage = timelineProtectionState.hideMediaContent(targetEvent.eventId, targetEvent.isMine),
)
composerState.eventSink(
MessageComposerEvent.SetMode(composerMode)
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt
index 7190aa174f..9811b07c02 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt
@@ -284,7 +284,7 @@ private fun TimelineItemEventContentViewWrapper(
} else {
TimelineItemEventContentView(
content = event.content,
- hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId),
+ hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId, event.isMine),
onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
onLinkClick = onLinkClick,
onLinkLongClick = onLinkLongClick,
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemCallNotifyView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemCallNotifyView.kt
index 61273ff07a..fea4f92de5 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemCallNotifyView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemCallNotifyView.kt
@@ -32,7 +32,11 @@ import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.messages.impl.timeline.TimelineRoomInfo
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
+import io.element.android.features.messages.impl.timeline.aTimelineItemReadReceipts
import io.element.android.features.messages.impl.timeline.aTimelineRoomInfo
+import io.element.android.features.messages.impl.timeline.components.receipt.ReadReceiptViewState
+import io.element.android.features.messages.impl.timeline.components.receipt.TimelineItemReadReceiptView
+import io.element.android.features.messages.impl.timeline.components.receipt.aReadReceiptData
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.RtcNotificationState
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRtcNotificationContent
@@ -48,46 +52,63 @@ internal fun TimelineItemCallNotifyView(
timelineRoomInfo: TimelineRoomInfo,
event: TimelineItem.Event,
content: TimelineItemRtcNotificationContent,
+ renderReadReceipts: Boolean,
+ isLastOutgoingMessage: Boolean,
onLongClick: (TimelineItem.Event) -> Unit,
+ onReadReceiptsClick: (TimelineItem.Event) -> Unit,
modifier: Modifier = Modifier
) {
- Row(
- modifier = modifier
- .fillMaxWidth()
- .border(1.dp, ElementTheme.colors.borderInteractiveSecondary, RoundedCornerShape(8.dp))
- .combinedClickable(
- enabled = true,
- onClick = {},
- onLongClick = { onLongClick(event) },
- onLongClickLabel = stringResource(CommonStrings.action_open_context_menu),
+ Column(modifier = modifier) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 16.dp, end = 16.dp, top = 16.dp)
+ .border(1.dp, ElementTheme.colors.borderInteractiveSecondary, RoundedCornerShape(8.dp))
+ .combinedClickable(
+ enabled = true,
+ onClick = {},
+ onLongClick = { onLongClick(event) },
+ onLongClickLabel = stringResource(CommonStrings.action_open_context_menu),
+ )
+ .onKeyboardContextMenuAction { onLongClick(event) }
+ .padding(12.dp),
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Icon(
+ modifier = Modifier.size(20.sp.toDp()),
+ imageVector = getIcon(timelineRoomInfo, content),
+ contentDescription = null,
+ tint = ElementTheme.colors.iconSecondary,
)
- .onKeyboardContextMenuAction { onLongClick(event) }
- .padding(12.dp),
- horizontalArrangement = Arrangement.spacedBy(12.dp),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Icon(
- modifier = Modifier.size(20.sp.toDp()),
- imageVector = getIcon(timelineRoomInfo, content),
- contentDescription = null,
- tint = ElementTheme.colors.iconSecondary,
- )
- Text(
- modifier = Modifier.weight(1f),
- text = stringResource(getTextRes(timelineRoomInfo, content)),
- style = ElementTheme.typography.fontBodyMdRegular,
- color = ElementTheme.colors.textSecondary,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- )
+ Text(
+ modifier = Modifier.weight(1f),
+ text = stringResource(getTextRes(timelineRoomInfo, content)),
+ style = ElementTheme.typography.fontBodyMdRegular,
+ color = ElementTheme.colors.textSecondary,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
- Text(
- text = event.sentTime,
- style = ElementTheme.typography.fontBodyMdRegular,
- color = ElementTheme.colors.textSecondary,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
+ Text(
+ text = event.sentTime,
+ style = ElementTheme.typography.fontBodyMdRegular,
+ color = ElementTheme.colors.textSecondary,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+
+ TimelineItemReadReceiptView(
+ state = ReadReceiptViewState(
+ sendState = event.localSendState,
+ isLastOutgoingMessage = isLastOutgoingMessage,
+ receipts = event.readReceiptState.receipts,
+ ),
+ renderReadReceipts = renderReadReceipts,
+ onReadReceiptsClick = { onReadReceiptsClick(event) },
+ modifier = Modifier.padding(top = 4.dp),
)
}
}
@@ -125,7 +146,10 @@ private fun getIcon(
@PreviewsDayNight
@Composable
internal fun TimelineItemCallNotifyViewPreview() = ElementPreview {
- Column(modifier = Modifier.padding(2.dp), verticalArrangement = Arrangement.spacedBy(2.dp)) {
+ val readReceiptState = aTimelineItemReadReceipts(
+ receipts = List(3) { aReadReceiptData(it) },
+ )
+ Column(modifier = Modifier.padding(bottom = 16.dp)) {
listOf(false, true).forEach { isDm ->
listOf(CallIntent.AUDIO, CallIntent.VIDEO).forEach { callIntent ->
listOf(
@@ -136,9 +160,18 @@ internal fun TimelineItemCallNotifyViewPreview() = ElementPreview {
val content = TimelineItemRtcNotificationContent(callIntent, state)
TimelineItemCallNotifyView(
timelineRoomInfo = aTimelineRoomInfo(isDm = isDm),
- event = aTimelineItemEvent(content = content),
+ event = aTimelineItemEvent(
+ content = content,
+ readReceiptState = readReceiptState,
+ ),
content = content,
+ // Render read receipts for the first item only
+ renderReadReceipts = !isDm &&
+ callIntent == CallIntent.AUDIO &&
+ state == RtcNotificationState.Started,
+ isLastOutgoingMessage = false,
onLongClick = {},
+ onReadReceiptsClick = {},
)
}
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt
index df2b9d8691..12f7a52f99 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt
@@ -63,7 +63,7 @@ fun TimelineItemGroupedEventsRow(
{ event, contentModifier, onContentLayoutChange ->
TimelineItemEventContentView(
content = event.content,
- hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId),
+ hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId, event.isMine),
onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
onLinkClick = onLinkClick,
onLinkLongClick = onLinkLongClick,
@@ -136,7 +136,7 @@ private fun TimelineItemGroupedEventsRowContent(
{ event, contentModifier, onContentLayoutChange ->
TimelineItemEventContentView(
content = event.content,
- hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId),
+ hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId, event.isMine),
onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
onLinkClick = onLinkClick,
onLinkLongClick = onLinkLongClick,
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 a02413d534..5dc69bba8b 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
@@ -78,7 +78,7 @@ internal fun TimelineItemRow(
{ event, contentModifier, onContentLayoutChange ->
TimelineItemEventContentView(
content = event.content,
- hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId),
+ hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId, event.isMine),
onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
onContentClick = { onContentClick(event) },
onLongClick = { onLongClick(event) },
@@ -124,11 +124,13 @@ internal fun TimelineItemRow(
}
is TimelineItemRtcNotificationContent -> {
TimelineItemCallNotifyView(
- modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp),
timelineRoomInfo = timelineRoomInfo,
event = timelineItem,
content = timelineItem.content,
+ renderReadReceipts = renderReadReceipts,
+ isLastOutgoingMessage = isLastOutgoingMessage,
onLongClick = onLongClick,
+ onReadReceiptsClick = onReadReceiptClick,
)
}
else -> {
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineItem.kt
index 5a5363f0c6..fcaa28a108 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineItem.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineItem.kt
@@ -34,26 +34,32 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
*/
fun TimelineItem.mustBeProtected(): Boolean {
return when (this) {
- is TimelineItem.Event -> when (content) {
- is TimelineItemImageContent,
- is TimelineItemVideoContent,
- is TimelineItemStickerContent -> true
- is TimelineItemAudioContent,
- is TimelineItemRtcNotificationContent,
- is TimelineItemEncryptedContent,
- is TimelineItemFileContent,
- TimelineItemLegacyCallInviteContent,
- is TimelineItemLocationContent,
- is TimelineItemPollContent,
- TimelineItemRedactedContent,
- is TimelineItemProfileChangeContent,
- is TimelineItemRoomMembershipContent,
- is TimelineItemStateEventContent,
- is TimelineItemEmoteContent,
- is TimelineItemNoticeContent,
- is TimelineItemTextContent,
- TimelineItemUnknownContent,
- is TimelineItemVoiceContent -> false
+ is TimelineItem.Event -> {
+ if (isMine) {
+ false
+ } else {
+ when (content) {
+ is TimelineItemImageContent,
+ is TimelineItemVideoContent,
+ is TimelineItemStickerContent -> true
+ is TimelineItemAudioContent,
+ is TimelineItemRtcNotificationContent,
+ is TimelineItemEncryptedContent,
+ is TimelineItemFileContent,
+ TimelineItemLegacyCallInviteContent,
+ is TimelineItemLocationContent,
+ is TimelineItemPollContent,
+ TimelineItemRedactedContent,
+ is TimelineItemProfileChangeContent,
+ is TimelineItemRoomMembershipContent,
+ is TimelineItemStateEventContent,
+ is TimelineItemEmoteContent,
+ is TimelineItemNoticeContent,
+ is TimelineItemTextContent,
+ TimelineItemUnknownContent,
+ is TimelineItemVoiceContent -> false
+ }
+ }
}
is TimelineItem.Virtual -> false
is TimelineItem.GroupedEvents -> false
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionState.kt
index 169e5ba7f4..df31c205e9 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionState.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionState.kt
@@ -16,9 +16,13 @@ data class TimelineProtectionState(
val protectionState: ProtectionState,
val eventSink: (TimelineProtectionEvent) -> Unit,
) {
- fun hideMediaContent(eventId: EventId?) = when (protectionState) {
- is ProtectionState.RenderAll -> false
- is ProtectionState.RenderOnly -> eventId !in protectionState.eventIds
+ fun hideMediaContent(eventId: EventId?, isMine: Boolean = false) = if (isMine) {
+ false
+ } else {
+ when (protectionState) {
+ is ProtectionState.RenderAll -> false
+ is ProtectionState.RenderOnly -> eventId !in protectionState.eventIds
+ }
}
}
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionStateTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionStateTest.kt
index 645039b30c..c5709588f0 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionStateTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionStateTest.kt
@@ -42,4 +42,19 @@ class TimelineProtectionStateTest {
assertThat(sut.hideMediaContent(AN_EVENT_ID)).isFalse()
assertThat(sut.hideMediaContent(AN_EVENT_ID_2)).isTrue()
}
+
+ @Test
+ fun `when isMine is true, hideMediaContent always returns false regardless of state`() {
+ val sutRenderAll = aTimelineProtectionState(
+ protectionState = ProtectionState.RenderAll
+ )
+ assertThat(sutRenderAll.hideMediaContent(null, isMine = true)).isFalse()
+ assertThat(sutRenderAll.hideMediaContent(AN_EVENT_ID, isMine = true)).isFalse()
+
+ val sutRenderOnly = aTimelineProtectionState(
+ protectionState = ProtectionState.RenderOnly(persistentSetOf())
+ )
+ assertThat(sutRenderOnly.hideMediaContent(null, isMine = true)).isFalse()
+ assertThat(sutRenderOnly.hideMediaContent(AN_EVENT_ID, isMine = true)).isFalse()
+ }
}
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt
index 5652276c23..2009970448 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt
@@ -286,7 +286,7 @@ private fun SoundsPreferenceCategory(state: NotificationSettingsState) {
type = RingtoneManager.TYPE_NOTIFICATION,
current = state.messageSound.sound,
defaultUri = Settings.System.DEFAULT_NOTIFICATION_URI,
- onSoundPicked = { sound -> state.eventSink(NotificationSettingsEvents.SetMessageSound(sound)) },
+ onSoundPick = { sound -> state.eventSink(NotificationSettingsEvents.SetMessageSound(sound)) },
)
// Skip the initial 0 emission so the picker doesn't auto-open on screen entry; only
// increments fired by LaunchMessageSoundPicker should launch it.
@@ -313,7 +313,7 @@ private fun SoundsPreferenceCategory(state: NotificationSettingsState) {
type = RingtoneManager.TYPE_RINGTONE,
current = state.callRingtone.sound,
defaultUri = Settings.System.DEFAULT_RINGTONE_URI,
- onSoundPicked = { sound -> state.eventSink(NotificationSettingsEvents.SetCallRingtone(sound)) },
+ onSoundPick = { sound -> state.eventSink(NotificationSettingsEvents.SetCallRingtone(sound)) },
)
// Skip the initial 0 emission so the picker doesn't auto-open on screen entry; only
// increments fired by LaunchCallRingtonePicker should launch it.
@@ -441,7 +441,7 @@ private fun rememberSoundPickerOnClick(
type: Int,
current: NotificationSound,
defaultUri: Uri,
- onSoundPicked: (NotificationSound) -> Unit,
+ onSoundPick: (NotificationSound) -> Unit,
): () -> Unit {
// Paparazzi previews don't provide a LocalActivityResultRegistryOwner, which
// rememberLauncherForActivityResult requires. Skip the launcher in inspection mode and
@@ -453,7 +453,7 @@ private fun rememberSoundPickerOnClick(
if (result.resultCode == Activity.RESULT_OK) {
val sound = result.data?.toPickedNotificationSound(defaultUri)
if (sound != null) {
- onSoundPicked(sound)
+ onSoundPick(sound)
}
}
}
diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/reporter/BugReporter.kt b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/reporter/BugReporter.kt
index 79b05ea32e..631a51a686 100644
--- a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/reporter/BugReporter.kt
+++ b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/reporter/BugReporter.kt
@@ -20,6 +20,7 @@ interface BugReporter {
* @param problemDescription the bug description
* @param canContact true if the user opt in to be contacted directly
* @param sendPushRules true to include the push rules
+ * @param ghIssueNumber it not null, the GitHub issue number to link the bug report to.
* @param listener the listener
*/
suspend fun sendBugReport(
@@ -29,6 +30,7 @@ interface BugReporter {
problemDescription: String,
canContact: Boolean = false,
sendPushRules: Boolean = false,
+ ghIssueNumber: Int? = null,
listener: BugReporterListener
)
diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportEvents.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportEvents.kt
index 90751873dd..94897d1bad 100644
--- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportEvents.kt
+++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportEvents.kt
@@ -18,4 +18,5 @@ sealed interface BugReportEvents {
data class SetCanContact(val canContact: Boolean) : BugReportEvents
data class SetSendScreenshot(val sendScreenshot: Boolean) : BugReportEvents
data class SetSendPushRules(val sendPushRules: Boolean) : BugReportEvents
+ data class SetGhIssueNumber(val ghIssueNumber: Int?) : BugReportEvents
}
diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenter.kt
index 4985f9b30e..85731ae959 100644
--- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenter.kt
+++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenter.kt
@@ -113,6 +113,9 @@ class BugReportPresenter(
sendingProgress.floatValue = 0f
sendingAction.value = AsyncAction.Uninitialized
}
+ is BugReportEvents.SetGhIssueNumber -> updateFormState(formState) {
+ copy(ghIssueNumber = event.ghIssueNumber)
+ }
}
}
@@ -142,7 +145,8 @@ class BugReportPresenter(
problemDescription = formState.description,
canContact = formState.canContact,
sendPushRules = formState.sendPushRules,
- listener = listener
+ ghIssueNumber = formState.ghIssueNumber,
+ listener = listener,
)
}
diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportState.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportState.kt
index 65cc055ec9..8e07b3c4e8 100644
--- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportState.kt
+++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportState.kt
@@ -32,6 +32,7 @@ data class BugReportFormState(
val canContact: Boolean,
val sendScreenshot: Boolean,
val sendPushRules: Boolean,
+ val ghIssueNumber: Int?,
) : Parcelable {
companion object {
val Default = BugReportFormState(
@@ -40,6 +41,7 @@ data class BugReportFormState(
canContact = false,
sendScreenshot = false,
sendPushRules = false,
+ ghIssueNumber = null,
)
}
}
diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt
index c2c34835c9..36b407a669 100644
--- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt
+++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt
@@ -34,6 +34,7 @@ import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import coil3.request.CachePolicy
import coil3.request.ImageRequest
+import io.element.android.compound.theme.ElementTheme
import io.element.android.features.rageshake.impl.R
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.components.async.AsyncActionView
@@ -155,6 +156,48 @@ fun BugReportView(
title = stringResource(R.string.screen_bug_report_send_notification_settings_title),
subtitle = stringResource(R.string.screen_bug_report_send_notification_settings_description),
)
+ PreferenceRow {
+ var ghIssueNumberState by textFieldState(
+ stateValue = state.formState.ghIssueNumber?.toString() ?: ""
+ )
+ TextField(
+ value = ghIssueNumberState,
+ modifier = Modifier
+ .fillMaxWidth()
+ .onTabOrEnterKeyFocusNext(LocalFocusManager.current),
+ enabled = isFormEnabled,
+ label = stringResource(id = R.string.screen_bug_report_github_issue_label),
+ placeholder = "1234",
+ supportingText = stringResource(id = R.string.screen_bug_report_github_issue_description),
+ leadingIcon = {
+ Text(
+ text = "#",
+ style = ElementTheme.typography.fontBodyLgMedium,
+ color = ElementTheme.colors.textSecondary,
+ )
+ },
+ onValueChange = {
+ if (it.isEmpty()) {
+ ghIssueNumberState = ""
+ eventSink(BugReportEvents.SetGhIssueNumber(null))
+ } else {
+ val number = it.toIntOrNull()?.takeIf { ghInt -> ghInt in 1..99_999 }
+ number?.let { ghIssueNumber ->
+ ghIssueNumberState = ghIssueNumber.toString()
+ eventSink(BugReportEvents.SetGhIssueNumber(ghIssueNumber))
+ }
+ }
+ },
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Number,
+ imeAction = ImeAction.Next,
+ ),
+ keyboardActions = KeyboardActions(onNext = {
+ keyboardController?.hide()
+ }),
+ singleLine = true,
+ )
+ }
// Submit
PreferenceRow {
Button(
diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt
index 2568a6fba9..735bb1cea0 100755
--- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt
+++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt
@@ -123,6 +123,7 @@ class DefaultBugReporter(
problemDescription: String,
canContact: Boolean,
sendPushRules: Boolean,
+ ghIssueNumber: Int?,
listener: BugReporterListener,
) {
val url = bugReporterUrlProvider.provide().first()
@@ -144,6 +145,9 @@ class DefaultBugReporter(
val crashCallStack = crashDataStore.crashInfo().first()
val bugDescription = buildString {
append(problemDescription)
+ ghIssueNumber?.let {
+ append("\n\nhttps://github.com/element-hq/element-x-android/issues/$it")
+ }
if (crashCallStack.isNotEmpty() && withCrashLogs) {
append("\n\n\n\n--------------------------------- crash call stack ---------------------------------\n")
append(crashCallStack)
diff --git a/features/rageshake/impl/src/main/res/values/localazy.xml b/features/rageshake/impl/src/main/res/values/localazy.xml
index 85af2af4a7..d1dc52f3e4 100644
--- a/features/rageshake/impl/src/main/res/values/localazy.xml
+++ b/features/rageshake/impl/src/main/res/values/localazy.xml
@@ -8,6 +8,8 @@
"Describe the problem…"
"If possible, please write the description in English."
"The description is too short, please provide more details about what happened. Thanks!"
+ "You can enter the number of an associated GitHub issue, if any."
+ "GitHub issue"
"Send crash logs"
"Allow logs"
"Your logs are excessively large so cannot be included in this report, please send them to us another way."
diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt
index 148c375e25..afbcc939a6 100644
--- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt
+++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt
@@ -107,6 +107,18 @@ class BugReportPresenterTest {
}
}
+ @Test
+ fun `present - set GitHub issue number`() = runTest {
+ val presenter = createPresenter()
+ presenter.test {
+ val initialState = awaitItem()
+ initialState.eventSink.invoke(BugReportEvents.SetGhIssueNumber(1))
+ assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(ghIssueNumber = 1))
+ initialState.eventSink.invoke(BugReportEvents.SetGhIssueNumber(null))
+ assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(ghIssueNumber = null))
+ }
+ }
+
@Test
fun `present - reset all`() = runTest {
val presenter = createPresenter(
diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/FakeBugReporter.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/FakeBugReporter.kt
index 36cc185e86..659145caa9 100644
--- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/FakeBugReporter.kt
+++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/FakeBugReporter.kt
@@ -28,6 +28,7 @@ class FakeBugReporter(val mode: Mode = Mode.Success) : BugReporter {
problemDescription: String,
canContact: Boolean,
sendPushRules: Boolean,
+ ghIssueNumber: Int?,
listener: BugReporterListener,
) {
delay(100)
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index f3e79e3c45..05530df227 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -214,7 +214,7 @@ telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "
statemachine = "com.freeletics.flowredux:compose:1.2.2"
maplibre = "org.maplibre.gl:android-sdk:13.1.0"
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.2"
-maplibre_compose = "org.maplibre.compose:maplibre-compose:0.12.1"
+maplibre_compose = "org.maplibre.compose:maplibre-compose:0.13.0"
maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.2"
opusencoder = "io.element.android:opusencoder:1.2.0"
zxing_cpp = "io.github.zxing-cpp:android:3.0.2"
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Day_0_en.png
index e89697a8cc..510521faa5 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5529e89e00208e38522f5206f5b8d304bd472b27071cab4e0d3c2daf3cb64db0
-size 49741
+oid sha256:aeaeef2348e2844e300c83b257027468ff51977bcfa2d6c540f8c882d2624826
+size 44173
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Night_0_en.png
index 37037907c8..5dcea94a33 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:fa29aaa82f21912dd5147ffb6fdc457fd5880e8be4abde4f0177d0ab1a412fe2
-size 48245
+oid sha256:4a6c9b24bfc9159fc70b6431f106836031c69cf5308674c20d88c1570f15d105
+size 43144
diff --git a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_0_en.png b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_0_en.png
index b81b524b86..0599f2300a 100644
--- a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:4a10513e98757e17cc3ffb8302807896839a78d036331e79bb207686f3f9db86
-size 47842
+oid sha256:a4b243025f40d5d4a91e32d59a6cac9ec21a273aaddbfac020cb26b3dc86c906
+size 53339
diff --git a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_1_en.png b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_1_en.png
index 0700c94bb7..9e2407229c 100644
--- a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:05efb0fa45bb6440e75e9280b219cfdac83c372efe9e6d6254b4bbe516f8b998
-size 112842
+oid sha256:e98722e0c21fe6b509b2bdf4a9ce7f78a1c83b606411329d2fd97a676fc4bed3
+size 113728
diff --git a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_2_en.png b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_2_en.png
index 61e347497e..bfd0f366be 100644
--- a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_2_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_2_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:833d21633cb0f153b660112fcfd0923c588cd26b64005403a007d7782ecfe5d5
-size 45138
+oid sha256:44eed3e8a093977f51d706acbec65cf29d592dcf0af3cdc9fce85d2b8e570eb9
+size 51038
diff --git a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_3_en.png b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_3_en.png
index b81b524b86..0599f2300a 100644
--- a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_3_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_3_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:4a10513e98757e17cc3ffb8302807896839a78d036331e79bb207686f3f9db86
-size 47842
+oid sha256:a4b243025f40d5d4a91e32d59a6cac9ec21a273aaddbfac020cb26b3dc86c906
+size 53339
diff --git a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_4_en.png b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_4_en.png
index 948604dd84..1a5d877a24 100644
--- a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_4_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_4_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ebe22ec065cc6dff2133dcc573a82e003d9061ae0446c2d8c13f4e1fba1f3c19
-size 37056
+oid sha256:781287977ff2637fe6479c3653270783c4374bc10fffb91da8b45d4e4f588a98
+size 41744
diff --git a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_0_en.png b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_0_en.png
index 9b379990c3..3fea4591cd 100644
--- a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f647e92bf813ead7affcdb6ad07076fe9d739f6a4c0016b9787912f032e27476
-size 46567
+oid sha256:f690c34f97eec23f01bfdc3c2606a229bb12abb91fe86fb79c89da79b08480a0
+size 52049
diff --git a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_1_en.png b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_1_en.png
index fcdfe6ba7d..957e19a4ce 100644
--- a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:cdfbb9f70a97724a1b6687ad4f199efc2c9bcc5f29fe9475a17ee6e52e5ecf8e
-size 110829
+oid sha256:751b442e5aecd9d3938817f8a1af11b7cc1d250c1166c9e8bd3154a85268471d
+size 111765
diff --git a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_2_en.png b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_2_en.png
index 6760c96a86..390d0faf7e 100644
--- a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_2_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_2_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1cfab4886243e6c914f466f5f127ec0347b0ab30ee43fb95c7bc82d2b16325d5
-size 43727
+oid sha256:0719e803112640729fe2b59c40373343db3e9f2e8dded9258d4b6c44547f8fd1
+size 49420
diff --git a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_3_en.png b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_3_en.png
index 9b379990c3..3fea4591cd 100644
--- a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_3_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_3_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f647e92bf813ead7affcdb6ad07076fe9d739f6a4c0016b9787912f032e27476
-size 46567
+oid sha256:f690c34f97eec23f01bfdc3c2606a229bb12abb91fe86fb79c89da79b08480a0
+size 52049
diff --git a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_4_en.png b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_4_en.png
index 6b7ad4a825..4fd1ecbc0e 100644
--- a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_4_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_4_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8c24515cc18b3c5cc52f2acfcee4faa65c02d8d5c8be847d4b1ec5671a858210
-size 35201
+oid sha256:1cb10ddf4a9dee4a001276554cf32cf874062e1e5d7d0ebb673f6417641530bf
+size 39730