mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-03 06:55:41 +00:00
Merge branch 'develop' into renovate/tspascoal-get-user-teams-membership-4.x
This commit is contained in:
@@ -52,6 +52,7 @@ Uncomment this markdown table below and edit the last line `|||`:
|
|||||||
|
|
||||||
<!-- Depending on the Pull Request content, it can be acceptable if some of the following checkboxes stay unchecked. -->
|
<!-- Depending on the Pull Request content, it can be acceptable if some of the following checkboxes stay unchecked. -->
|
||||||
|
|
||||||
|
- [ ] 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:
|
- This PR was made with the help of AI:
|
||||||
- [ ] Yes. In this case, please request a review by Copilot.
|
- [ ] Yes. In this case, please request a review by Copilot.
|
||||||
- [ ] No.
|
- [ ] No.
|
||||||
|
|||||||
@@ -44,11 +44,14 @@ PRs must meet these rules.
|
|||||||
|
|
||||||
## Pull Request Guidelines
|
## Pull Request Guidelines
|
||||||
|
|
||||||
- Use sentence-style commit/PR messages (no conventional commits).
|
- Sentence-style titles (no conventional commits).
|
||||||
- Apply exactly **one** `PR-` label for changelog categorization.
|
- Exactly one `pr-` label (see `.github/release.yml`).
|
||||||
- PR title = changelog entry — make it descriptive; no "Fixes #…" prefixes.
|
- Title = changelog entry — descriptive, no "Fixes #…".
|
||||||
- Include screenshots or screen recordings for any UI changes.
|
- Leave description template for the developer. Redirect them to the [contributing etiquette](CONTRIBUTING.md#etiquette).
|
||||||
- Keep PRs focused; split changes over 1000 lines.
|
- 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+22
-1
@@ -6,6 +6,7 @@
|
|||||||
* [I want to help translating Element](#i-want-to-help-translating-element)
|
* [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 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)
|
* [I want to add a new feature or enhancement](#i-want-to-add-a-new-feature-or-enhancement)
|
||||||
|
* [Etiquette](#etiquette)
|
||||||
* [Developer onboarding](#developer-onboarding)
|
* [Developer onboarding](#developer-onboarding)
|
||||||
* [Submitting the PRs](#submitting-the-prs)
|
* [Submitting the PRs](#submitting-the-prs)
|
||||||
* [Android Studio settings](#android-studio-settings)
|
* [Android Studio settings](#android-studio-settings)
|
||||||
@@ -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.
|
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).
|
For a detailed overview of the project, see [Developer Onboarding](./docs/_developer_onboarding.md).
|
||||||
|
|
||||||
|
|||||||
+52
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
-9
@@ -18,11 +18,11 @@ import org.maplibre.compose.location.DesiredAccuracy
|
|||||||
import org.maplibre.compose.location.LocationPuck
|
import org.maplibre.compose.location.LocationPuck
|
||||||
import org.maplibre.compose.location.LocationPuckColors
|
import org.maplibre.compose.location.LocationPuckColors
|
||||||
import org.maplibre.compose.location.LocationPuckSizes
|
import org.maplibre.compose.location.LocationPuckSizes
|
||||||
import org.maplibre.compose.location.LocationTrackingEffect
|
|
||||||
import org.maplibre.compose.location.UserLocationState
|
import org.maplibre.compose.location.UserLocationState
|
||||||
import org.maplibre.compose.location.rememberAndroidLocationProvider
|
import org.maplibre.compose.location.rememberAndroidLocationProvider
|
||||||
import org.maplibre.compose.location.rememberNullLocationProvider
|
import org.maplibre.compose.location.rememberNullLocationProvider
|
||||||
import org.maplibre.compose.location.rememberUserLocationState
|
import org.maplibre.compose.location.rememberUserLocationState
|
||||||
|
import org.maplibre.spatialk.units.extensions.meters
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -31,22 +31,24 @@ fun UserLocationPuck(
|
|||||||
locationState: UserLocationState,
|
locationState: UserLocationState,
|
||||||
trackUserLocation: Boolean,
|
trackUserLocation: Boolean,
|
||||||
) {
|
) {
|
||||||
LocationTrackingEffect(
|
SimpleLocationTrackingEffect(
|
||||||
locationState = locationState,
|
locationState = locationState,
|
||||||
enabled = trackUserLocation,
|
enabled = trackUserLocation,
|
||||||
) {
|
) { currentLocation ->
|
||||||
val finalPosition = cameraState.position.copy(
|
val target = currentLocation?.position?.value ?: cameraState.position.target
|
||||||
target = currentLocation.position,
|
val newPosition = cameraState.position.copy(
|
||||||
bearing = currentLocation.bearing ?: cameraState.position.bearing,
|
target = target,
|
||||||
|
// Force pointing to NORTH
|
||||||
|
bearing = 0.0,
|
||||||
zoom = cameraState.position.zoom.coerceAtLeast(MapDefaults.DEFAULT_ZOOM)
|
zoom = cameraState.position.zoom.coerceAtLeast(MapDefaults.DEFAULT_ZOOM)
|
||||||
)
|
)
|
||||||
cameraState.animateTo(finalPosition)
|
cameraState.animateTo(newPosition)
|
||||||
}
|
}
|
||||||
val location = locationState.location
|
val location = locationState.location
|
||||||
if (location != null) {
|
if (location != null) {
|
||||||
LocationPuck(
|
LocationPuck(
|
||||||
idPrefix = "user-location",
|
idPrefix = "user-location",
|
||||||
locationState = locationState,
|
location = location,
|
||||||
cameraState = cameraState,
|
cameraState = cameraState,
|
||||||
accuracyThreshold = Float.POSITIVE_INFINITY,
|
accuracyThreshold = Float.POSITIVE_INFINITY,
|
||||||
showBearingAccuracy = false,
|
showBearingAccuracy = false,
|
||||||
@@ -74,7 +76,7 @@ fun rememberUserLocationState(hasLocationPermission: Boolean): UserLocationState
|
|||||||
rememberAndroidLocationProvider(
|
rememberAndroidLocationProvider(
|
||||||
updateInterval = 5.seconds,
|
updateInterval = 5.seconds,
|
||||||
desiredAccuracy = DesiredAccuracy.High,
|
desiredAccuracy = DesiredAccuracy.High,
|
||||||
minDistanceMeters = 5f,
|
minDistance = 5.meters,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return rememberUserLocationState(locationProvider)
|
return rememberUserLocationState(locationProvider)
|
||||||
|
|||||||
+6
-4
@@ -36,6 +36,8 @@ import kotlinx.coroutines.flow.map
|
|||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import org.maplibre.compose.location.AndroidLocationProvider
|
import org.maplibre.compose.location.AndroidLocationProvider
|
||||||
import org.maplibre.compose.location.DesiredAccuracy
|
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 timber.log.Timber
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
import io.element.android.features.location.api.Location as ApiLocation
|
import io.element.android.features.location.api.Location as ApiLocation
|
||||||
@@ -91,7 +93,7 @@ class LiveLocationSharingService : Service() {
|
|||||||
val locationProvider = AndroidLocationProvider(
|
val locationProvider = AndroidLocationProvider(
|
||||||
context = applicationContext,
|
context = applicationContext,
|
||||||
updateInterval = UPDATE_INTERVAL_IN_SECOND.seconds,
|
updateInterval = UPDATE_INTERVAL_IN_SECOND.seconds,
|
||||||
minDistanceMeters = minDistanceMeters.toFloat(),
|
minDistance = minDistanceMeters.meters,
|
||||||
desiredAccuracy = DesiredAccuracy.Balanced,
|
desiredAccuracy = DesiredAccuracy.Balanced,
|
||||||
coroutineScope = coroutineScope
|
coroutineScope = coroutineScope
|
||||||
)
|
)
|
||||||
@@ -100,9 +102,9 @@ class LiveLocationSharingService : Service() {
|
|||||||
.filterNotNull()
|
.filterNotNull()
|
||||||
.map { location ->
|
.map { location ->
|
||||||
ApiLocation(
|
ApiLocation(
|
||||||
lat = location.position.latitude,
|
lat = location.position.value.latitude,
|
||||||
lon = location.position.longitude,
|
lon = location.position.value.longitude,
|
||||||
accuracy = location.accuracy.toFloat(),
|
accuracy = location.position.accuracy?.inMeters?.toFloat(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.onEach(coordinator::dispatch)
|
.onEach(coordinator::dispatch)
|
||||||
|
|||||||
+1
-1
@@ -100,8 +100,8 @@ class ShareLocationPresenter(
|
|||||||
fun checkLocationConstraints() {
|
fun checkLocationConstraints() {
|
||||||
// No need to check SendLiveLocationPermissions here
|
// No need to check SendLiveLocationPermissions here
|
||||||
val locationConstraints = checkLocationConstraints(permissionsState, locationActions, SendLiveLocationPermissions.GRANTED)
|
val locationConstraints = checkLocationConstraints(permissionsState, locationActions, SendLiveLocationPermissions.GRANTED)
|
||||||
dialogState = ShareLocationState.Dialog.Constraints(locationConstraints.toDialogState())
|
|
||||||
trackUserPosition = locationConstraints is LocationConstraintsCheck.Success
|
trackUserPosition = locationConstraints is LocationConstraintsCheck.Success
|
||||||
|
dialogState = ShareLocationState.Dialog.Constraints(locationConstraints.toDialogState())
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun computeLiveLocationDialogState(): ShareLocationState.Dialog {
|
suspend fun computeLiveLocationDialogState(): ShareLocationState.Dialog {
|
||||||
|
|||||||
+2
-2
@@ -225,8 +225,8 @@ private fun BottomSheetContent(
|
|||||||
state.eventSink(
|
state.eventSink(
|
||||||
ShareLocationEvent.ShareStaticLocation(
|
ShareLocationEvent.ShareStaticLocation(
|
||||||
location = Location(
|
location = Location(
|
||||||
lat = userLocation.position.latitude,
|
lat = userLocation.position.value.latitude,
|
||||||
lon = userLocation.position.longitude
|
lon = userLocation.position.value.longitude
|
||||||
),
|
),
|
||||||
isPinned = false
|
isPinned = false
|
||||||
)
|
)
|
||||||
|
|||||||
+3
-1
@@ -155,7 +155,9 @@ fun ShowLocationView(
|
|||||||
val position = CameraPosition(
|
val position = CameraPosition(
|
||||||
padding = sheetPaddings,
|
padding = sheetPaddings,
|
||||||
target = Position(locationShare.location.lon, locationShare.location.lat),
|
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 {
|
coroutineScope.launch {
|
||||||
cameraState.animateTo(finalPosition = position)
|
cameraState.animateTo(finalPosition = position)
|
||||||
|
|||||||
+1
-1
@@ -550,7 +550,7 @@ class MessagesPresenter(
|
|||||||
val replyToDetails = loadReplyDetails(targetEvent.eventId).map(permalinkParser)
|
val replyToDetails = loadReplyDetails(targetEvent.eventId).map(permalinkParser)
|
||||||
val composerMode = MessageComposerMode.Reply(
|
val composerMode = MessageComposerMode.Reply(
|
||||||
replyToDetails = replyToDetails,
|
replyToDetails = replyToDetails,
|
||||||
hideImage = timelineProtectionState.hideMediaContent(targetEvent.eventId),
|
hideImage = timelineProtectionState.hideMediaContent(targetEvent.eventId, targetEvent.isMine),
|
||||||
)
|
)
|
||||||
composerState.eventSink(
|
composerState.eventSink(
|
||||||
MessageComposerEvent.SetMode(composerMode)
|
MessageComposerEvent.SetMode(composerMode)
|
||||||
|
|||||||
+1
-1
@@ -284,7 +284,7 @@ private fun TimelineItemEventContentViewWrapper(
|
|||||||
} else {
|
} else {
|
||||||
TimelineItemEventContentView(
|
TimelineItemEventContentView(
|
||||||
content = event.content,
|
content = event.content,
|
||||||
hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId),
|
hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId, event.isMine),
|
||||||
onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
|
onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
|
||||||
onLinkClick = onLinkClick,
|
onLinkClick = onLinkClick,
|
||||||
onLinkLongClick = onLinkLongClick,
|
onLinkLongClick = onLinkLongClick,
|
||||||
|
|||||||
+36
-3
@@ -32,7 +32,11 @@ import io.element.android.compound.theme.ElementTheme
|
|||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
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.TimelineRoomInfo
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
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.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.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.RtcNotificationState
|
import io.element.android.features.messages.impl.timeline.model.event.RtcNotificationState
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRtcNotificationContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRtcNotificationContent
|
||||||
@@ -48,12 +52,17 @@ internal fun TimelineItemCallNotifyView(
|
|||||||
timelineRoomInfo: TimelineRoomInfo,
|
timelineRoomInfo: TimelineRoomInfo,
|
||||||
event: TimelineItem.Event,
|
event: TimelineItem.Event,
|
||||||
content: TimelineItemRtcNotificationContent,
|
content: TimelineItemRtcNotificationContent,
|
||||||
|
renderReadReceipts: Boolean,
|
||||||
|
isLastOutgoingMessage: Boolean,
|
||||||
onLongClick: (TimelineItem.Event) -> Unit,
|
onLongClick: (TimelineItem.Event) -> Unit,
|
||||||
|
onReadReceiptsClick: (TimelineItem.Event) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
Column(modifier = modifier) {
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.padding(start = 16.dp, end = 16.dp, top = 16.dp)
|
||||||
.border(1.dp, ElementTheme.colors.borderInteractiveSecondary, RoundedCornerShape(8.dp))
|
.border(1.dp, ElementTheme.colors.borderInteractiveSecondary, RoundedCornerShape(8.dp))
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
enabled = true,
|
enabled = true,
|
||||||
@@ -90,6 +99,18 @@ internal fun TimelineItemCallNotifyView(
|
|||||||
overflow = TextOverflow.Ellipsis,
|
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),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@StringRes
|
@StringRes
|
||||||
@@ -125,7 +146,10 @@ private fun getIcon(
|
|||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TimelineItemCallNotifyViewPreview() = ElementPreview {
|
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(false, true).forEach { isDm ->
|
||||||
listOf(CallIntent.AUDIO, CallIntent.VIDEO).forEach { callIntent ->
|
listOf(CallIntent.AUDIO, CallIntent.VIDEO).forEach { callIntent ->
|
||||||
listOf(
|
listOf(
|
||||||
@@ -136,9 +160,18 @@ internal fun TimelineItemCallNotifyViewPreview() = ElementPreview {
|
|||||||
val content = TimelineItemRtcNotificationContent(callIntent, state)
|
val content = TimelineItemRtcNotificationContent(callIntent, state)
|
||||||
TimelineItemCallNotifyView(
|
TimelineItemCallNotifyView(
|
||||||
timelineRoomInfo = aTimelineRoomInfo(isDm = isDm),
|
timelineRoomInfo = aTimelineRoomInfo(isDm = isDm),
|
||||||
event = aTimelineItemEvent(content = content),
|
event = aTimelineItemEvent(
|
||||||
content = content,
|
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 = {},
|
onLongClick = {},
|
||||||
|
onReadReceiptsClick = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -63,7 +63,7 @@ fun TimelineItemGroupedEventsRow(
|
|||||||
{ event, contentModifier, onContentLayoutChange ->
|
{ event, contentModifier, onContentLayoutChange ->
|
||||||
TimelineItemEventContentView(
|
TimelineItemEventContentView(
|
||||||
content = event.content,
|
content = event.content,
|
||||||
hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId),
|
hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId, event.isMine),
|
||||||
onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
|
onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
|
||||||
onLinkClick = onLinkClick,
|
onLinkClick = onLinkClick,
|
||||||
onLinkLongClick = onLinkLongClick,
|
onLinkLongClick = onLinkLongClick,
|
||||||
@@ -136,7 +136,7 @@ private fun TimelineItemGroupedEventsRowContent(
|
|||||||
{ event, contentModifier, onContentLayoutChange ->
|
{ event, contentModifier, onContentLayoutChange ->
|
||||||
TimelineItemEventContentView(
|
TimelineItemEventContentView(
|
||||||
content = event.content,
|
content = event.content,
|
||||||
hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId),
|
hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId, event.isMine),
|
||||||
onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
|
onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
|
||||||
onLinkClick = onLinkClick,
|
onLinkClick = onLinkClick,
|
||||||
onLinkLongClick = onLinkLongClick,
|
onLinkLongClick = onLinkLongClick,
|
||||||
|
|||||||
+4
-2
@@ -78,7 +78,7 @@ internal fun TimelineItemRow(
|
|||||||
{ event, contentModifier, onContentLayoutChange ->
|
{ event, contentModifier, onContentLayoutChange ->
|
||||||
TimelineItemEventContentView(
|
TimelineItemEventContentView(
|
||||||
content = event.content,
|
content = event.content,
|
||||||
hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId),
|
hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId, event.isMine),
|
||||||
onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
|
onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
|
||||||
onContentClick = { onContentClick(event) },
|
onContentClick = { onContentClick(event) },
|
||||||
onLongClick = { onLongClick(event) },
|
onLongClick = { onLongClick(event) },
|
||||||
@@ -124,11 +124,13 @@ internal fun TimelineItemRow(
|
|||||||
}
|
}
|
||||||
is TimelineItemRtcNotificationContent -> {
|
is TimelineItemRtcNotificationContent -> {
|
||||||
TimelineItemCallNotifyView(
|
TimelineItemCallNotifyView(
|
||||||
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp),
|
|
||||||
timelineRoomInfo = timelineRoomInfo,
|
timelineRoomInfo = timelineRoomInfo,
|
||||||
event = timelineItem,
|
event = timelineItem,
|
||||||
content = timelineItem.content,
|
content = timelineItem.content,
|
||||||
|
renderReadReceipts = renderReadReceipts,
|
||||||
|
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
|
onReadReceiptsClick = onReadReceiptClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
|
|||||||
+7
-1
@@ -34,7 +34,11 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
|||||||
*/
|
*/
|
||||||
fun TimelineItem.mustBeProtected(): Boolean {
|
fun TimelineItem.mustBeProtected(): Boolean {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
is TimelineItem.Event -> when (content) {
|
is TimelineItem.Event -> {
|
||||||
|
if (isMine) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
when (content) {
|
||||||
is TimelineItemImageContent,
|
is TimelineItemImageContent,
|
||||||
is TimelineItemVideoContent,
|
is TimelineItemVideoContent,
|
||||||
is TimelineItemStickerContent -> true
|
is TimelineItemStickerContent -> true
|
||||||
@@ -55,6 +59,8 @@ fun TimelineItem.mustBeProtected(): Boolean {
|
|||||||
TimelineItemUnknownContent,
|
TimelineItemUnknownContent,
|
||||||
is TimelineItemVoiceContent -> false
|
is TimelineItemVoiceContent -> false
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
is TimelineItem.Virtual -> false
|
is TimelineItem.Virtual -> false
|
||||||
is TimelineItem.GroupedEvents -> false
|
is TimelineItem.GroupedEvents -> false
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -16,11 +16,15 @@ data class TimelineProtectionState(
|
|||||||
val protectionState: ProtectionState,
|
val protectionState: ProtectionState,
|
||||||
val eventSink: (TimelineProtectionEvent) -> Unit,
|
val eventSink: (TimelineProtectionEvent) -> Unit,
|
||||||
) {
|
) {
|
||||||
fun hideMediaContent(eventId: EventId?) = when (protectionState) {
|
fun hideMediaContent(eventId: EventId?, isMine: Boolean = false) = if (isMine) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
when (protectionState) {
|
||||||
is ProtectionState.RenderAll -> false
|
is ProtectionState.RenderAll -> false
|
||||||
is ProtectionState.RenderOnly -> eventId !in protectionState.eventIds
|
is ProtectionState.RenderOnly -> eventId !in protectionState.eventIds
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
sealed interface ProtectionState {
|
sealed interface ProtectionState {
|
||||||
|
|||||||
+15
@@ -42,4 +42,19 @@ class TimelineProtectionStateTest {
|
|||||||
assertThat(sut.hideMediaContent(AN_EVENT_ID)).isFalse()
|
assertThat(sut.hideMediaContent(AN_EVENT_ID)).isFalse()
|
||||||
assertThat(sut.hideMediaContent(AN_EVENT_ID_2)).isTrue()
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -286,7 +286,7 @@ private fun SoundsPreferenceCategory(state: NotificationSettingsState) {
|
|||||||
type = RingtoneManager.TYPE_NOTIFICATION,
|
type = RingtoneManager.TYPE_NOTIFICATION,
|
||||||
current = state.messageSound.sound,
|
current = state.messageSound.sound,
|
||||||
defaultUri = Settings.System.DEFAULT_NOTIFICATION_URI,
|
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
|
// Skip the initial 0 emission so the picker doesn't auto-open on screen entry; only
|
||||||
// increments fired by LaunchMessageSoundPicker should launch it.
|
// increments fired by LaunchMessageSoundPicker should launch it.
|
||||||
@@ -313,7 +313,7 @@ private fun SoundsPreferenceCategory(state: NotificationSettingsState) {
|
|||||||
type = RingtoneManager.TYPE_RINGTONE,
|
type = RingtoneManager.TYPE_RINGTONE,
|
||||||
current = state.callRingtone.sound,
|
current = state.callRingtone.sound,
|
||||||
defaultUri = Settings.System.DEFAULT_RINGTONE_URI,
|
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
|
// Skip the initial 0 emission so the picker doesn't auto-open on screen entry; only
|
||||||
// increments fired by LaunchCallRingtonePicker should launch it.
|
// increments fired by LaunchCallRingtonePicker should launch it.
|
||||||
@@ -441,7 +441,7 @@ private fun rememberSoundPickerOnClick(
|
|||||||
type: Int,
|
type: Int,
|
||||||
current: NotificationSound,
|
current: NotificationSound,
|
||||||
defaultUri: Uri,
|
defaultUri: Uri,
|
||||||
onSoundPicked: (NotificationSound) -> Unit,
|
onSoundPick: (NotificationSound) -> Unit,
|
||||||
): () -> Unit {
|
): () -> Unit {
|
||||||
// Paparazzi previews don't provide a LocalActivityResultRegistryOwner, which
|
// Paparazzi previews don't provide a LocalActivityResultRegistryOwner, which
|
||||||
// rememberLauncherForActivityResult requires. Skip the launcher in inspection mode and
|
// rememberLauncherForActivityResult requires. Skip the launcher in inspection mode and
|
||||||
@@ -453,7 +453,7 @@ private fun rememberSoundPickerOnClick(
|
|||||||
if (result.resultCode == Activity.RESULT_OK) {
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
val sound = result.data?.toPickedNotificationSound(defaultUri)
|
val sound = result.data?.toPickedNotificationSound(defaultUri)
|
||||||
if (sound != null) {
|
if (sound != null) {
|
||||||
onSoundPicked(sound)
|
onSoundPick(sound)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -20,6 +20,7 @@ interface BugReporter {
|
|||||||
* @param problemDescription the bug description
|
* @param problemDescription the bug description
|
||||||
* @param canContact true if the user opt in to be contacted directly
|
* @param canContact true if the user opt in to be contacted directly
|
||||||
* @param sendPushRules true to include the push rules
|
* @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
|
* @param listener the listener
|
||||||
*/
|
*/
|
||||||
suspend fun sendBugReport(
|
suspend fun sendBugReport(
|
||||||
@@ -29,6 +30,7 @@ interface BugReporter {
|
|||||||
problemDescription: String,
|
problemDescription: String,
|
||||||
canContact: Boolean = false,
|
canContact: Boolean = false,
|
||||||
sendPushRules: Boolean = false,
|
sendPushRules: Boolean = false,
|
||||||
|
ghIssueNumber: Int? = null,
|
||||||
listener: BugReporterListener
|
listener: BugReporterListener
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+1
@@ -18,4 +18,5 @@ sealed interface BugReportEvents {
|
|||||||
data class SetCanContact(val canContact: Boolean) : BugReportEvents
|
data class SetCanContact(val canContact: Boolean) : BugReportEvents
|
||||||
data class SetSendScreenshot(val sendScreenshot: Boolean) : BugReportEvents
|
data class SetSendScreenshot(val sendScreenshot: Boolean) : BugReportEvents
|
||||||
data class SetSendPushRules(val sendPushRules: Boolean) : BugReportEvents
|
data class SetSendPushRules(val sendPushRules: Boolean) : BugReportEvents
|
||||||
|
data class SetGhIssueNumber(val ghIssueNumber: Int?) : BugReportEvents
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -113,6 +113,9 @@ class BugReportPresenter(
|
|||||||
sendingProgress.floatValue = 0f
|
sendingProgress.floatValue = 0f
|
||||||
sendingAction.value = AsyncAction.Uninitialized
|
sendingAction.value = AsyncAction.Uninitialized
|
||||||
}
|
}
|
||||||
|
is BugReportEvents.SetGhIssueNumber -> updateFormState(formState) {
|
||||||
|
copy(ghIssueNumber = event.ghIssueNumber)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +145,8 @@ class BugReportPresenter(
|
|||||||
problemDescription = formState.description,
|
problemDescription = formState.description,
|
||||||
canContact = formState.canContact,
|
canContact = formState.canContact,
|
||||||
sendPushRules = formState.sendPushRules,
|
sendPushRules = formState.sendPushRules,
|
||||||
listener = listener
|
ghIssueNumber = formState.ghIssueNumber,
|
||||||
|
listener = listener,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
@@ -32,6 +32,7 @@ data class BugReportFormState(
|
|||||||
val canContact: Boolean,
|
val canContact: Boolean,
|
||||||
val sendScreenshot: Boolean,
|
val sendScreenshot: Boolean,
|
||||||
val sendPushRules: Boolean,
|
val sendPushRules: Boolean,
|
||||||
|
val ghIssueNumber: Int?,
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
companion object {
|
companion object {
|
||||||
val Default = BugReportFormState(
|
val Default = BugReportFormState(
|
||||||
@@ -40,6 +41,7 @@ data class BugReportFormState(
|
|||||||
canContact = false,
|
canContact = false,
|
||||||
sendScreenshot = false,
|
sendScreenshot = false,
|
||||||
sendPushRules = false,
|
sendPushRules = false,
|
||||||
|
ghIssueNumber = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+43
@@ -34,6 +34,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import coil3.request.CachePolicy
|
import coil3.request.CachePolicy
|
||||||
import coil3.request.ImageRequest
|
import coil3.request.ImageRequest
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.features.rageshake.impl.R
|
import io.element.android.features.rageshake.impl.R
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
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),
|
title = stringResource(R.string.screen_bug_report_send_notification_settings_title),
|
||||||
subtitle = stringResource(R.string.screen_bug_report_send_notification_settings_description),
|
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
|
// Submit
|
||||||
PreferenceRow {
|
PreferenceRow {
|
||||||
Button(
|
Button(
|
||||||
|
|||||||
+4
@@ -123,6 +123,7 @@ class DefaultBugReporter(
|
|||||||
problemDescription: String,
|
problemDescription: String,
|
||||||
canContact: Boolean,
|
canContact: Boolean,
|
||||||
sendPushRules: Boolean,
|
sendPushRules: Boolean,
|
||||||
|
ghIssueNumber: Int?,
|
||||||
listener: BugReporterListener,
|
listener: BugReporterListener,
|
||||||
) {
|
) {
|
||||||
val url = bugReporterUrlProvider.provide().first()
|
val url = bugReporterUrlProvider.provide().first()
|
||||||
@@ -144,6 +145,9 @@ class DefaultBugReporter(
|
|||||||
val crashCallStack = crashDataStore.crashInfo().first()
|
val crashCallStack = crashDataStore.crashInfo().first()
|
||||||
val bugDescription = buildString {
|
val bugDescription = buildString {
|
||||||
append(problemDescription)
|
append(problemDescription)
|
||||||
|
ghIssueNumber?.let {
|
||||||
|
append("\n\nhttps://github.com/element-hq/element-x-android/issues/$it")
|
||||||
|
}
|
||||||
if (crashCallStack.isNotEmpty() && withCrashLogs) {
|
if (crashCallStack.isNotEmpty() && withCrashLogs) {
|
||||||
append("\n\n\n\n--------------------------------- crash call stack ---------------------------------\n")
|
append("\n\n\n\n--------------------------------- crash call stack ---------------------------------\n")
|
||||||
append(crashCallStack)
|
append(crashCallStack)
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
<string name="screen_bug_report_editor_placeholder">"Describe the problem…"</string>
|
<string name="screen_bug_report_editor_placeholder">"Describe the problem…"</string>
|
||||||
<string name="screen_bug_report_editor_supporting">"If possible, please write the description in English."</string>
|
<string name="screen_bug_report_editor_supporting">"If possible, please write the description in English."</string>
|
||||||
<string name="screen_bug_report_error_description_too_short">"The description is too short, please provide more details about what happened. Thanks!"</string>
|
<string name="screen_bug_report_error_description_too_short">"The description is too short, please provide more details about what happened. Thanks!"</string>
|
||||||
|
<string name="screen_bug_report_github_issue_description">"You can enter the number of an associated GitHub issue, if any."</string>
|
||||||
|
<string name="screen_bug_report_github_issue_label">"GitHub issue"</string>
|
||||||
<string name="screen_bug_report_include_crash_logs">"Send crash logs"</string>
|
<string name="screen_bug_report_include_crash_logs">"Send crash logs"</string>
|
||||||
<string name="screen_bug_report_include_logs">"Allow logs"</string>
|
<string name="screen_bug_report_include_logs">"Allow logs"</string>
|
||||||
<string name="screen_bug_report_include_logs_error">"Your logs are excessively large so cannot be included in this report, please send them to us another way."</string>
|
<string name="screen_bug_report_include_logs_error">"Your logs are excessively large so cannot be included in this report, please send them to us another way."</string>
|
||||||
|
|||||||
+12
@@ -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
|
@Test
|
||||||
fun `present - reset all`() = runTest {
|
fun `present - reset all`() = runTest {
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
|
|||||||
+1
@@ -28,6 +28,7 @@ class FakeBugReporter(val mode: Mode = Mode.Success) : BugReporter {
|
|||||||
problemDescription: String,
|
problemDescription: String,
|
||||||
canContact: Boolean,
|
canContact: Boolean,
|
||||||
sendPushRules: Boolean,
|
sendPushRules: Boolean,
|
||||||
|
ghIssueNumber: Int?,
|
||||||
listener: BugReporterListener,
|
listener: BugReporterListener,
|
||||||
) {
|
) {
|
||||||
delay(100)
|
delay(100)
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "
|
|||||||
statemachine = "com.freeletics.flowredux:compose:1.2.2"
|
statemachine = "com.freeletics.flowredux:compose:1.2.2"
|
||||||
maplibre = "org.maplibre.gl:android-sdk:13.1.0"
|
maplibre = "org.maplibre.gl:android-sdk:13.1.0"
|
||||||
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.2"
|
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"
|
maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.2"
|
||||||
opusencoder = "io.element.android:opusencoder:1.2.0"
|
opusencoder = "io.element.android:opusencoder:1.2.0"
|
||||||
zxing_cpp = "io.github.zxing-cpp:android:3.0.2"
|
zxing_cpp = "io.github.zxing-cpp:android:3.0.2"
|
||||||
|
|||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:4a10513e98757e17cc3ffb8302807896839a78d036331e79bb207686f3f9db86
|
oid sha256:a4b243025f40d5d4a91e32d59a6cac9ec21a273aaddbfac020cb26b3dc86c906
|
||||||
size 47842
|
size 53339
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:05efb0fa45bb6440e75e9280b219cfdac83c372efe9e6d6254b4bbe516f8b998
|
oid sha256:e98722e0c21fe6b509b2bdf4a9ce7f78a1c83b606411329d2fd97a676fc4bed3
|
||||||
size 112842
|
size 113728
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:833d21633cb0f153b660112fcfd0923c588cd26b64005403a007d7782ecfe5d5
|
oid sha256:44eed3e8a093977f51d706acbec65cf29d592dcf0af3cdc9fce85d2b8e570eb9
|
||||||
size 45138
|
size 51038
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:4a10513e98757e17cc3ffb8302807896839a78d036331e79bb207686f3f9db86
|
oid sha256:a4b243025f40d5d4a91e32d59a6cac9ec21a273aaddbfac020cb26b3dc86c906
|
||||||
size 47842
|
size 53339
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ebe22ec065cc6dff2133dcc573a82e003d9061ae0446c2d8c13f4e1fba1f3c19
|
oid sha256:781287977ff2637fe6479c3653270783c4374bc10fffb91da8b45d4e4f588a98
|
||||||
size 37056
|
size 41744
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f647e92bf813ead7affcdb6ad07076fe9d739f6a4c0016b9787912f032e27476
|
oid sha256:f690c34f97eec23f01bfdc3c2606a229bb12abb91fe86fb79c89da79b08480a0
|
||||||
size 46567
|
size 52049
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:cdfbb9f70a97724a1b6687ad4f199efc2c9bcc5f29fe9475a17ee6e52e5ecf8e
|
oid sha256:751b442e5aecd9d3938817f8a1af11b7cc1d250c1166c9e8bd3154a85268471d
|
||||||
size 110829
|
size 111765
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1cfab4886243e6c914f466f5f127ec0347b0ab30ee43fb95c7bc82d2b16325d5
|
oid sha256:0719e803112640729fe2b59c40373343db3e9f2e8dded9258d4b6c44547f8fd1
|
||||||
size 43727
|
size 49420
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f647e92bf813ead7affcdb6ad07076fe9d739f6a4c0016b9787912f032e27476
|
oid sha256:f690c34f97eec23f01bfdc3c2606a229bb12abb91fe86fb79c89da79b08480a0
|
||||||
size 46567
|
size 52049
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:8c24515cc18b3c5cc52f2acfcee4faa65c02d8d5c8be847d4b1ec5671a858210
|
oid sha256:1cb10ddf4a9dee4a001276554cf32cf874062e1e5d7d0ebb673f6417641530bf
|
||||||
size 35201
|
size 39730
|
||||||
|
|||||||
Reference in New Issue
Block a user