Merge pull request #2659 from element-hq/feature/bma/moreTest
Remove some dead code and add tests on RetrySendMessageMenu
This commit is contained in:
commit
c8ca43a391
12 changed files with 130 additions and 316 deletions
|
|
@ -1,98 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.timeline.components.event
|
|
||||||
|
|
||||||
import androidx.compose.material3.LocalTextStyle
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.TextMeasurer
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.text.rememberTextMeasurer
|
|
||||||
import androidx.compose.ui.unit.Density
|
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import io.element.android.compound.theme.ElementTheme
|
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
|
||||||
import io.element.android.libraries.core.bool.orFalse
|
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
|
||||||
import kotlin.math.ceil
|
|
||||||
|
|
||||||
// Allow to not overlap the timestamp with the text, in the message bubble.
|
|
||||||
// Compute the size of the worst case.
|
|
||||||
data class ExtraPadding(val extraWidth: Dp)
|
|
||||||
|
|
||||||
val noExtraPadding = ExtraPadding(0.dp)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* See [io.element.android.features.messages.impl.timeline.components.TimelineEventTimestampView] for the related View.
|
|
||||||
* And https://www.figma.com/file/0MMNu7cTOzLOlWb7ctTkv3/Element-X?node-id=1819%253A99506 for the design.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun TimelineItem.Event.toExtraPadding(): ExtraPadding {
|
|
||||||
val formattedTime = sentTime
|
|
||||||
val hasMessageSendingFailed = localSendState is LocalEventSendState.SendingFailed
|
|
||||||
val isMessageEdited = (content as? TimelineItemTextBasedContent)?.isEdited.orFalse()
|
|
||||||
|
|
||||||
val textMeasurer = rememberTextMeasurer(cacheSize = 128)
|
|
||||||
val density = LocalDensity.current
|
|
||||||
|
|
||||||
var strLen = 2.dp // Extra space char
|
|
||||||
if (isMessageEdited) {
|
|
||||||
val editedText = stringResource(id = CommonStrings.common_edited_suffix)
|
|
||||||
val extraLen = remember(editedText, density) { textMeasurer.getExtraPadding(editedText, density) } + 10.dp // Text + spacing
|
|
||||||
strLen += extraLen
|
|
||||||
}
|
|
||||||
strLen += remember(formattedTime, density) { textMeasurer.getExtraPadding(formattedTime, density) }
|
|
||||||
if (hasMessageSendingFailed) {
|
|
||||||
strLen += 19.dp // Image + spacing
|
|
||||||
// I do not know why, but adding extra widths avoid overlapping when the
|
|
||||||
// message is edited and in error.
|
|
||||||
if (isMessageEdited) {
|
|
||||||
strLen += 2.dp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ExtraPadding(strLen)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun TextMeasurer.getExtraPadding(text: String, density: Density): Dp {
|
|
||||||
val timestampTextStyle = ElementTheme.typography.fontBodyXsRegular
|
|
||||||
val textWidth = measure(text = text, style = timestampTextStyle).size.width
|
|
||||||
return (textWidth / density.density).dp
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a string to add to the content of the message to avoid overlapping the timestamp.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun ExtraPadding.getStr(textStyle: TextStyle = LocalTextStyle.current): String {
|
|
||||||
if (extraWidth == 0.dp) return ""
|
|
||||||
val density = LocalDensity.current
|
|
||||||
val textMeasurer = rememberTextMeasurer(128)
|
|
||||||
val charWidth = remember(textStyle) { textMeasurer.measure(text = "\u00A0", style = textStyle).size.width }
|
|
||||||
val widthPx = remember(density, extraWidth) { with(density) { extraWidth.toPx() } }
|
|
||||||
// A space and some unbreakable spaces, always rounding the result to the next value if not a integer
|
|
||||||
return " " + "\u00A0".repeat(ceil(widthPx / charWidth).toInt())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ExtraPadding.getDpSize(): Dp {
|
|
||||||
return extraWidth
|
|
||||||
}
|
|
||||||
|
|
@ -20,7 +20,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
|
||||||
sealed interface RetrySendMenuEvents {
|
sealed interface RetrySendMenuEvents {
|
||||||
data class EventSelected(val event: TimelineItem.Event) : RetrySendMenuEvents
|
data class EventSelected(val event: TimelineItem.Event) : RetrySendMenuEvents
|
||||||
data object RetrySend : RetrySendMenuEvents
|
data object Retry : RetrySendMenuEvents
|
||||||
data object RemoveFailed : RetrySendMenuEvents
|
data object Remove : RetrySendMenuEvents
|
||||||
data object Dismiss : RetrySendMenuEvents
|
data object Dismiss : RetrySendMenuEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ class RetrySendMenuPresenter @Inject constructor(
|
||||||
is RetrySendMenuEvents.EventSelected -> {
|
is RetrySendMenuEvents.EventSelected -> {
|
||||||
selectedEvent = event.event
|
selectedEvent = event.event
|
||||||
}
|
}
|
||||||
RetrySendMenuEvents.RetrySend -> {
|
RetrySendMenuEvents.Retry -> {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
selectedEvent?.transactionId?.let { transactionId ->
|
selectedEvent?.transactionId?.let { transactionId ->
|
||||||
room.retrySendMessage(transactionId)
|
room.retrySendMessage(transactionId)
|
||||||
|
|
@ -49,7 +49,7 @@ class RetrySendMenuPresenter @Inject constructor(
|
||||||
selectedEvent = null
|
selectedEvent = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RetrySendMenuEvents.RemoveFailed -> {
|
RetrySendMenuEvents.Remove -> {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
selectedEvent?.transactionId?.let { transactionId ->
|
selectedEvent?.transactionId?.let { transactionId ->
|
||||||
room.cancelSend(transactionId)
|
room.cancelSend(transactionId)
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
package io.element.android.features.messages.impl.timeline.components.retrysendmenu
|
package io.element.android.features.messages.impl.timeline.components.retrysendmenu
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
|
@ -54,18 +53,18 @@ internal fun RetrySendMessageMenu(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onRetry() {
|
fun onRetry() {
|
||||||
state.eventSink(RetrySendMenuEvents.RetrySend)
|
state.eventSink(RetrySendMenuEvents.Retry)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onRemoveFailed() {
|
fun onRemove() {
|
||||||
state.eventSink(RetrySendMenuEvents.RemoveFailed)
|
state.eventSink(RetrySendMenuEvents.Remove)
|
||||||
}
|
}
|
||||||
|
|
||||||
RetrySendMessageMenuBottomSheet(
|
RetrySendMessageMenuBottomSheet(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
isVisible = isVisible,
|
isVisible = isVisible,
|
||||||
onRetry = ::onRetry,
|
onRetry = ::onRetry,
|
||||||
onRemoveFailed = ::onRemoveFailed,
|
onRemove = ::onRemove,
|
||||||
onDismiss = ::onDismiss
|
onDismiss = ::onDismiss
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -75,7 +74,7 @@ internal fun RetrySendMessageMenu(
|
||||||
private fun RetrySendMessageMenuBottomSheet(
|
private fun RetrySendMessageMenuBottomSheet(
|
||||||
isVisible: Boolean,
|
isVisible: Boolean,
|
||||||
onRetry: () -> Unit,
|
onRetry: () -> Unit,
|
||||||
onRemoveFailed: () -> Unit,
|
onRemove: () -> Unit,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|
@ -95,7 +94,10 @@ private fun RetrySendMessageMenuBottomSheet(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
RetrySendMenuContents(onRetry = onRetry, onRemoveFailed = onRemoveFailed)
|
RetrySendMenuContents(
|
||||||
|
onRetry = onRetry,
|
||||||
|
onRemove = onRemove,
|
||||||
|
)
|
||||||
// FIXME remove after https://issuetracker.google.com/issues/275849044
|
// FIXME remove after https://issuetracker.google.com/issues/275849044
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
}
|
}
|
||||||
|
|
@ -106,7 +108,7 @@ private fun RetrySendMessageMenuBottomSheet(
|
||||||
@Composable
|
@Composable
|
||||||
private fun ColumnScope.RetrySendMenuContents(
|
private fun ColumnScope.RetrySendMenuContents(
|
||||||
onRetry: () -> Unit,
|
onRetry: () -> Unit,
|
||||||
onRemoveFailed: () -> Unit,
|
onRemove: () -> Unit,
|
||||||
sheetState: SheetState = rememberModalBottomSheetState(),
|
sheetState: SheetState = rememberModalBottomSheetState(),
|
||||||
) {
|
) {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
@ -142,22 +144,16 @@ private fun ColumnScope.RetrySendMenuContents(
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
sheetState.hide()
|
sheetState.hide()
|
||||||
onRemoveFailed()
|
onRemove()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNUSED_PARAMETER")
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun RetrySendMessageMenuPreview(@PreviewParameter(RetrySendMenuStateProvider::class) state: RetrySendMenuState) = ElementPreview {
|
internal fun RetrySendMessageMenuPreview(@PreviewParameter(RetrySendMenuStateProvider::class) state: RetrySendMenuState) = ElementPreview {
|
||||||
// TODO restore RetrySendMessageMenuBottomSheet once the issue with bottom sheet not being previewable is fixed
|
RetrySendMessageMenu(
|
||||||
Column {
|
state = state,
|
||||||
RetrySendMenuContents(
|
)
|
||||||
onRetry = {},
|
|
||||||
onRemoveFailed = {},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,6 @@ class RetrySendMenuPresenterTests {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
val selectedEvent = aTimelineItemEvent()
|
val selectedEvent = aTimelineItemEvent()
|
||||||
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
||||||
|
|
||||||
assertThat(awaitItem().selectedEvent).isSameInstanceAs(selectedEvent)
|
assertThat(awaitItem().selectedEvent).isSameInstanceAs(selectedEvent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -57,8 +56,9 @@ class RetrySendMenuPresenterTests {
|
||||||
val selectedEvent = aTimelineItemEvent()
|
val selectedEvent = aTimelineItemEvent()
|
||||||
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
||||||
initialState.eventSink(RetrySendMenuEvents.Dismiss)
|
initialState.eventSink(RetrySendMenuEvents.Dismiss)
|
||||||
|
assertThat(room.cancelSendCount).isEqualTo(0)
|
||||||
|
assertThat(room.retrySendMessageCount).isEqualTo(0)
|
||||||
assertThat(awaitItem().selectedEvent).isNull()
|
assertThat(awaitItem().selectedEvent).isNull()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -72,8 +72,8 @@ class RetrySendMenuPresenterTests {
|
||||||
val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID)
|
val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID)
|
||||||
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
initialState.eventSink(RetrySendMenuEvents.Retry)
|
||||||
initialState.eventSink(RetrySendMenuEvents.RetrySend)
|
assertThat(room.cancelSendCount).isEqualTo(0)
|
||||||
assertThat(room.retrySendMessageCount).isEqualTo(1)
|
assertThat(room.retrySendMessageCount).isEqualTo(1)
|
||||||
assertThat(awaitItem().selectedEvent).isNull()
|
assertThat(awaitItem().selectedEvent).isNull()
|
||||||
}
|
}
|
||||||
|
|
@ -88,8 +88,8 @@ class RetrySendMenuPresenterTests {
|
||||||
val selectedEvent = aTimelineItemEvent(transactionId = null)
|
val selectedEvent = aTimelineItemEvent(transactionId = null)
|
||||||
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
initialState.eventSink(RetrySendMenuEvents.Retry)
|
||||||
initialState.eventSink(RetrySendMenuEvents.RetrySend)
|
assertThat(room.cancelSendCount).isEqualTo(0)
|
||||||
assertThat(room.retrySendMessageCount).isEqualTo(0)
|
assertThat(room.retrySendMessageCount).isEqualTo(0)
|
||||||
assertThat(awaitItem().selectedEvent).isNull()
|
assertThat(awaitItem().selectedEvent).isNull()
|
||||||
}
|
}
|
||||||
|
|
@ -105,8 +105,8 @@ class RetrySendMenuPresenterTests {
|
||||||
val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID)
|
val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID)
|
||||||
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
initialState.eventSink(RetrySendMenuEvents.Retry)
|
||||||
initialState.eventSink(RetrySendMenuEvents.RetrySend)
|
assertThat(room.cancelSendCount).isEqualTo(0)
|
||||||
assertThat(room.retrySendMessageCount).isEqualTo(1)
|
assertThat(room.retrySendMessageCount).isEqualTo(1)
|
||||||
assertThat(awaitItem().selectedEvent).isNull()
|
assertThat(awaitItem().selectedEvent).isNull()
|
||||||
}
|
}
|
||||||
|
|
@ -121,9 +121,9 @@ class RetrySendMenuPresenterTests {
|
||||||
val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID)
|
val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID)
|
||||||
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
initialState.eventSink(RetrySendMenuEvents.Remove)
|
||||||
initialState.eventSink(RetrySendMenuEvents.RemoveFailed)
|
|
||||||
assertThat(room.cancelSendCount).isEqualTo(1)
|
assertThat(room.cancelSendCount).isEqualTo(1)
|
||||||
|
assertThat(room.retrySendMessageCount).isEqualTo(0)
|
||||||
assertThat(awaitItem().selectedEvent).isNull()
|
assertThat(awaitItem().selectedEvent).isNull()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -137,9 +137,9 @@ class RetrySendMenuPresenterTests {
|
||||||
val selectedEvent = aTimelineItemEvent(transactionId = null)
|
val selectedEvent = aTimelineItemEvent(transactionId = null)
|
||||||
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
initialState.eventSink(RetrySendMenuEvents.Remove)
|
||||||
initialState.eventSink(RetrySendMenuEvents.RemoveFailed)
|
|
||||||
assertThat(room.cancelSendCount).isEqualTo(0)
|
assertThat(room.cancelSendCount).isEqualTo(0)
|
||||||
|
assertThat(room.retrySendMessageCount).isEqualTo(0)
|
||||||
assertThat(awaitItem().selectedEvent).isNull()
|
assertThat(awaitItem().selectedEvent).isNull()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -154,9 +154,9 @@ class RetrySendMenuPresenterTests {
|
||||||
val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID)
|
val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID)
|
||||||
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
initialState.eventSink(RetrySendMenuEvents.Remove)
|
||||||
initialState.eventSink(RetrySendMenuEvents.RemoveFailed)
|
|
||||||
assertThat(room.cancelSendCount).isEqualTo(1)
|
assertThat(room.cancelSendCount).isEqualTo(1)
|
||||||
|
assertThat(room.retrySendMessageCount).isEqualTo(0)
|
||||||
assertThat(awaitItem().selectedEvent).isNull()
|
assertThat(awaitItem().selectedEvent).isNull()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.timeline.components.retrysendmenu
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import io.element.android.features.messages.impl.R
|
||||||
|
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
|
import io.element.android.tests.testutils.clickOn
|
||||||
|
import io.element.android.tests.testutils.pressBackKey
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TestRule
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class RetrySendMessageMenuTest {
|
||||||
|
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `dismiss the bottom sheet emits the expected event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<RetrySendMenuEvents>()
|
||||||
|
rule.setRetrySendMessageMenu(
|
||||||
|
aRetrySendMenuState(
|
||||||
|
event = aTimelineItemEvent(),
|
||||||
|
eventSink = eventsRecorder
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.pressBackKey()
|
||||||
|
// Cannot test this for now.
|
||||||
|
// eventsRecorder.assertSingle(RetrySendMenuEvents.Dismiss)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Config(qualifiers = "h1024dp")
|
||||||
|
@Test
|
||||||
|
fun `retry to send the event emits the expected event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<RetrySendMenuEvents>()
|
||||||
|
rule.setRetrySendMessageMenu(
|
||||||
|
aRetrySendMenuState(
|
||||||
|
event = aTimelineItemEvent(),
|
||||||
|
eventSink = eventsRecorder
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(R.string.screen_room_retry_send_menu_send_again_action)
|
||||||
|
eventsRecorder.assertSingle(RetrySendMenuEvents.Retry)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Config(qualifiers = "h1024dp")
|
||||||
|
@Test
|
||||||
|
fun `remove the event emits the expected event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<RetrySendMenuEvents>()
|
||||||
|
rule.setRetrySendMessageMenu(
|
||||||
|
aRetrySendMenuState(
|
||||||
|
event = aTimelineItemEvent(),
|
||||||
|
eventSink = eventsRecorder
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_remove)
|
||||||
|
eventsRecorder.assertSingle(RetrySendMenuEvents.Remove)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRetrySendMessageMenu(
|
||||||
|
state: RetrySendMenuState,
|
||||||
|
) {
|
||||||
|
setContent {
|
||||||
|
RetrySendMessageMenu(
|
||||||
|
state = state,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,107 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.libraries.designsystem.modifiers
|
|
||||||
|
|
||||||
import androidx.compose.animation.core.animateFloat
|
|
||||||
import androidx.compose.animation.core.updateTransition
|
|
||||||
import androidx.compose.runtime.State
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.composed
|
|
||||||
import androidx.compose.ui.draw.drawWithCache
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.geometry.Rect
|
|
||||||
import androidx.compose.ui.geometry.Size
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.Path
|
|
||||||
import androidx.compose.ui.graphics.drawscope.clipPath
|
|
||||||
import androidx.compose.ui.platform.debugInspectorInfo
|
|
||||||
import kotlin.math.sqrt
|
|
||||||
|
|
||||||
// Note: these modifiers come from https://gist.github.com/darvld/eb3844474baf2f3fc6d3ab44a4b4b5f8
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A modifier that clips the composable content using an animated circle. The circle will
|
|
||||||
* expand/shrink with an animation whenever [visible] changes.
|
|
||||||
*
|
|
||||||
* For more fine-grained control over the transition, see this method's overload, which allows passing
|
|
||||||
* a [State] object to control the progress of the reveal animation.
|
|
||||||
*
|
|
||||||
* By default, the circle is centered in the content, but custom positions may be specified using
|
|
||||||
* [revealFrom]. Specified offsets should be between 0 (left/top) and 1 (right/bottom).*/
|
|
||||||
fun Modifier.circularReveal(
|
|
||||||
visible: Boolean,
|
|
||||||
showScrim: Boolean = false,
|
|
||||||
revealFrom: Offset = Offset(0.5f, 0.5f),
|
|
||||||
): Modifier = composed(
|
|
||||||
factory = {
|
|
||||||
val factor = updateTransition(visible, label = "Visibility")
|
|
||||||
.animateFloat(label = "revealFactor") { if (it) 1f else 0f }
|
|
||||||
|
|
||||||
circularReveal(factor, showScrim, revealFrom)
|
|
||||||
},
|
|
||||||
inspectorInfo = debugInspectorInfo {
|
|
||||||
name = "circularReveal"
|
|
||||||
properties["visible"] = visible
|
|
||||||
properties["revealFrom"] = revealFrom
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A modifier that clips the composable content using a circular shape. The radius of the circle
|
|
||||||
* will be determined by the [transitionProgress].
|
|
||||||
*
|
|
||||||
* The values of the progress should be between 0 and 1.
|
|
||||||
*
|
|
||||||
* By default, the circle is centered in the content, but custom positions may be specified using
|
|
||||||
* [revealFrom]. Specified offsets should be between 0 (left/top) and 1 (right/bottom).
|
|
||||||
* */
|
|
||||||
fun Modifier.circularReveal(
|
|
||||||
transitionProgress: State<Float>,
|
|
||||||
showScrim: Boolean = false,
|
|
||||||
revealFrom: Offset = Offset(0.5f, 0.5f)
|
|
||||||
): Modifier {
|
|
||||||
return drawWithCache {
|
|
||||||
val path = Path()
|
|
||||||
val center = revealFrom.mapTo(size)
|
|
||||||
val radius = calculateRadius(revealFrom, size)
|
|
||||||
val scrimColor = if (showScrim) {
|
|
||||||
Color.Gray
|
|
||||||
} else {
|
|
||||||
Color.Transparent
|
|
||||||
}
|
|
||||||
|
|
||||||
path.addOval(Rect(center, radius * transitionProgress.value))
|
|
||||||
|
|
||||||
onDrawWithContent {
|
|
||||||
if (showScrim) {
|
|
||||||
drawRect(scrimColor, alpha = transitionProgress.value * 0.75f)
|
|
||||||
}
|
|
||||||
clipPath(path) { this@onDrawWithContent.drawContent() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Offset.mapTo(size: Size): Offset {
|
|
||||||
return Offset(x * size.width, y * size.height)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun calculateRadius(normalizedOrigin: Offset, size: Size) = with(normalizedOrigin) {
|
|
||||||
val x = (if (x > 0.5f) x else 1 - x) * size.width
|
|
||||||
val y = (if (y > 0.5f) y else 1 - y) * size.height
|
|
||||||
|
|
||||||
sqrt(x * x + y * y)
|
|
||||||
}
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2022 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.libraries.push.impl.permission
|
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import io.element.android.libraries.di.ApplicationContext
|
|
||||||
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
// TODO EAx move
|
|
||||||
class NotificationPermissionManager @Inject constructor(
|
|
||||||
private val sdkIntProvider: BuildVersionSdkIntProvider,
|
|
||||||
@ApplicationContext private val context: Context,
|
|
||||||
) {
|
|
||||||
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
|
||||||
fun isPermissionGranted(): Boolean {
|
|
||||||
return ContextCompat.checkSelfPermission(
|
|
||||||
context,
|
|
||||||
Manifest.permission.POST_NOTIFICATIONS
|
|
||||||
) == PackageManager.PERMISSION_GRANTED
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
fun eventuallyRequestPermission(
|
|
||||||
activity: Activity,
|
|
||||||
requestPermissionLauncher: ActivityResultLauncher<Array<String>>,
|
|
||||||
showRationale: Boolean = true,
|
|
||||||
ignorePreference: Boolean = false,
|
|
||||||
) {
|
|
||||||
if (!sdkIntProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) return
|
|
||||||
// if (!vectorPreferences.areNotificationEnabledForDevice() && !ignorePreference) return
|
|
||||||
checkPermissions(
|
|
||||||
listOf(Manifest.permission.POST_NOTIFICATIONS),
|
|
||||||
activity,
|
|
||||||
activityResultLauncher = requestPermissionLauncher,
|
|
||||||
if (showRationale) R.string.permissions_rationale_msg_notification else 0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
fun eventuallyRevokePermission(
|
|
||||||
activity: Activity,
|
|
||||||
) {
|
|
||||||
if (!sdkIntProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) return
|
|
||||||
activity.revokeSelfPermissionOnKill(Manifest.permission.POST_NOTIFICATIONS)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6293aceaed02886f3eb1d344c70f93772377d0ae09e69e6a382b33f577a26cd8
|
oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b
|
||||||
size 14765
|
size 4457
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6293aceaed02886f3eb1d344c70f93772377d0ae09e69e6a382b33f577a26cd8
|
oid sha256:703fe69f26ac8b3b7d199def97c4198bcf1e462b68fdc39924a865a54f298e47
|
||||||
size 14765
|
size 16524
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:2bc02697864bb7289bccfdfc137a7768b763c378e2ca057103058ae0d8c94f98
|
oid sha256:8c89ac73df77c2bccb0c2aa80cee1420f78e7d07f0eda89a90bffef55e8cf753
|
||||||
size 13711
|
size 4464
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:2bc02697864bb7289bccfdfc137a7768b763c378e2ca057103058ae0d8c94f98
|
oid sha256:7d1750522119e356ab0ee5697b16eddca04fe04f16c29b1d4f750756c5a59965
|
||||||
size 13711
|
size 14281
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue