[#785] Remove press-and-hold from Send
* [#785] Remove press-and-hold for send confirmation - Timed button replaced by standard behaviour button - Related SendView test updated * Remove unused API If we need this back, we can restore it from the Git history. * Remove unused imports --------- Co-authored-by: Carter Jernigan <git@carterjernigan.com>
This commit is contained in:
parent
d37310a935
commit
0c0bf8cb34
|
@ -1,93 +0,0 @@
|
||||||
package co.electriccoin.zcash.ui.design.component
|
|
||||||
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.interaction.PressInteraction
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
|
||||||
import androidx.compose.ui.test.junit4.createComposeRule
|
|
||||||
import androidx.compose.ui.test.onNodeWithText
|
|
||||||
import androidx.compose.ui.test.performClick
|
|
||||||
import androidx.test.filters.MediumTest
|
|
||||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
|
||||||
import kotlinx.coroutines.test.advanceTimeBy
|
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import kotlinx.coroutines.test.testTimeSource
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
|
||||||
import kotlin.test.assertFalse
|
|
||||||
import kotlin.test.assertTrue
|
|
||||||
import kotlin.time.Duration
|
|
||||||
import kotlin.time.Duration.Companion.seconds
|
|
||||||
import kotlin.time.ExperimentalTime
|
|
||||||
|
|
||||||
class ButtonTest {
|
|
||||||
@get:Rule
|
|
||||||
val composeTestRule = createComposeRule()
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTime::class)
|
|
||||||
@Test
|
|
||||||
@MediumTest
|
|
||||||
fun timedButtonTest(): Unit = runTest {
|
|
||||||
val testDispatcher = StandardTestDispatcher(testScheduler)
|
|
||||||
val testSetup = newTestSetup(testDispatcher, 2.seconds)
|
|
||||||
val mark = testTimeSource.markNow()
|
|
||||||
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
testSetup.interactionSource.emit(PressInteraction.Press(Offset.Zero))
|
|
||||||
advanceTimeBy(3.seconds.inWholeMilliseconds)
|
|
||||||
}
|
|
||||||
|
|
||||||
launch {
|
|
||||||
testSetup.mutableActionExecuted.collect {
|
|
||||||
if (!it) return@collect
|
|
||||||
|
|
||||||
assertTrue { mark.elapsedNow() >= 2.seconds }
|
|
||||||
this.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
@Test
|
|
||||||
@MediumTest
|
|
||||||
fun buttonClickTest() = runTest {
|
|
||||||
val testDispatcher = StandardTestDispatcher(testScheduler)
|
|
||||||
val testSetup = newTestSetup(testDispatcher, 2.seconds)
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithText("button").also {
|
|
||||||
it.performClick()
|
|
||||||
}
|
|
||||||
advanceTimeBy(3.seconds.inWholeMilliseconds)
|
|
||||||
assertFalse { testSetup.mutableActionExecuted.value }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun newTestSetup(testDispatcher: CoroutineDispatcher, duration: Duration) = TestSetup(testDispatcher, composeTestRule, duration)
|
|
||||||
|
|
||||||
private class TestSetup(coroutineDispatcher: CoroutineDispatcher, composeTestRule: ComposeContentTestRule, duration: Duration) {
|
|
||||||
val mutableActionExecuted = MutableStateFlow(false)
|
|
||||||
val interactionSource = MutableInteractionSource()
|
|
||||||
|
|
||||||
init {
|
|
||||||
composeTestRule.setContent {
|
|
||||||
ZcashTheme {
|
|
||||||
TimedButton(
|
|
||||||
duration = duration,
|
|
||||||
onClick = { mutableActionExecuted.update { true } },
|
|
||||||
coroutineDispatcher = coroutineDispatcher,
|
|
||||||
content = { Text(text = "button") },
|
|
||||||
interactionSource = interactionSource
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +1,6 @@
|
||||||
package co.electriccoin.zcash.ui.design.component
|
package co.electriccoin.zcash.ui.design.component
|
||||||
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.interaction.PressInteraction
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.RowScope
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
|
@ -12,20 +9,10 @@ import androidx.compose.material3.ButtonDefaults.buttonColors
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import kotlin.time.Duration
|
|
||||||
import kotlin.time.Duration.Companion.seconds
|
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -158,41 +145,3 @@ fun DangerousButton(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("LongParameterList")
|
|
||||||
@Composable
|
|
||||||
fun TimedButton(
|
|
||||||
onClick: () -> Unit,
|
|
||||||
content: @Composable (RowScope.() -> Unit),
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
duration: Duration = 5.seconds,
|
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
|
||||||
coroutineDispatcher: CoroutineDispatcher = Dispatchers.Default
|
|
||||||
) {
|
|
||||||
LaunchedEffect(interactionSource) {
|
|
||||||
var action: Job? = null
|
|
||||||
|
|
||||||
interactionSource.interactions.collect { interaction ->
|
|
||||||
when (interaction) {
|
|
||||||
is PressInteraction.Press -> {
|
|
||||||
action = launch(coroutineDispatcher) {
|
|
||||||
delay(duration)
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
onClick()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is PressInteraction.Release -> {
|
|
||||||
action?.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(
|
|
||||||
modifier = modifier,
|
|
||||||
onClick = {},
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
content = content
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
package co.electriccoin.zcash.ui.screen.send.view
|
package co.electriccoin.zcash.ui.screen.send.view
|
||||||
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.interaction.PressInteraction
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.test.assertIsEnabled
|
import androidx.compose.ui.test.assertIsEnabled
|
||||||
import androidx.compose.ui.test.assertIsNotEnabled
|
import androidx.compose.ui.test.assertIsNotEnabled
|
||||||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||||
|
@ -26,13 +23,11 @@ import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||||
import co.electriccoin.zcash.ui.R
|
import co.electriccoin.zcash.ui.R
|
||||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||||
import co.electriccoin.zcash.ui.test.getStringResource
|
import co.electriccoin.zcash.ui.test.getStringResource
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.test.TestScope
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
@ -70,7 +65,7 @@ class SendViewTest : UiTestPrerequisites() {
|
||||||
composeTestRule.setValidAddress()
|
composeTestRule.setValidAddress()
|
||||||
composeTestRule.clickCreateAndSend()
|
composeTestRule.clickCreateAndSend()
|
||||||
composeTestRule.assertOnConfirmation()
|
composeTestRule.assertOnConfirmation()
|
||||||
clickConfirmation(testSetup.interactionSource)
|
composeTestRule.clickConfirmation()
|
||||||
|
|
||||||
launch {
|
launch {
|
||||||
testSetup.mutableActionExecuted.collectWith(this) {
|
testSetup.mutableActionExecuted.collectWith(this) {
|
||||||
|
@ -106,7 +101,7 @@ class SendViewTest : UiTestPrerequisites() {
|
||||||
|
|
||||||
composeTestRule.clickCreateAndSend()
|
composeTestRule.clickCreateAndSend()
|
||||||
composeTestRule.assertOnConfirmation()
|
composeTestRule.assertOnConfirmation()
|
||||||
clickConfirmation(testSetup.interactionSource)
|
composeTestRule.clickConfirmation()
|
||||||
|
|
||||||
launch {
|
launch {
|
||||||
testSetup.mutableActionExecuted.collectWith(this) {
|
testSetup.mutableActionExecuted.collectWith(this) {
|
||||||
|
@ -164,7 +159,7 @@ class SendViewTest : UiTestPrerequisites() {
|
||||||
|
|
||||||
composeTestRule.clickCreateAndSend()
|
composeTestRule.clickCreateAndSend()
|
||||||
composeTestRule.assertOnConfirmation()
|
composeTestRule.assertOnConfirmation()
|
||||||
clickConfirmation(testSetup.interactionSource)
|
composeTestRule.clickConfirmation()
|
||||||
|
|
||||||
launch {
|
launch {
|
||||||
testSetup.mutableActionExecuted.collectWith(this) {
|
testSetup.mutableActionExecuted.collectWith(this) {
|
||||||
|
@ -242,7 +237,7 @@ class SendViewTest : UiTestPrerequisites() {
|
||||||
|
|
||||||
composeTestRule.clickCreateAndSend()
|
composeTestRule.clickCreateAndSend()
|
||||||
composeTestRule.assertOnConfirmation()
|
composeTestRule.assertOnConfirmation()
|
||||||
clickConfirmation(testSetup.interactionSource)
|
composeTestRule.clickConfirmation()
|
||||||
|
|
||||||
launch {
|
launch {
|
||||||
testSetup.mutableActionExecuted.collectWith(this) {
|
testSetup.mutableActionExecuted.collectWith(this) {
|
||||||
|
@ -296,7 +291,6 @@ class SendViewTest : UiTestPrerequisites() {
|
||||||
|
|
||||||
private val onBackCount = AtomicInteger(0)
|
private val onBackCount = AtomicInteger(0)
|
||||||
private val onCreateCount = AtomicInteger(0)
|
private val onCreateCount = AtomicInteger(0)
|
||||||
val interactionSource = MutableInteractionSource()
|
|
||||||
val mutableActionExecuted = MutableStateFlow(false)
|
val mutableActionExecuted = MutableStateFlow(false)
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
|
@ -322,7 +316,6 @@ class SendViewTest : UiTestPrerequisites() {
|
||||||
ZcashTheme {
|
ZcashTheme {
|
||||||
Send(
|
Send(
|
||||||
mySpendableBalance = ZatoshiFixture.new(),
|
mySpendableBalance = ZatoshiFixture.new(),
|
||||||
pressAndHoldInteractionSource = interactionSource,
|
|
||||||
goBack = {
|
goBack = {
|
||||||
onBackCount.incrementAndGet()
|
onBackCount.incrementAndGet()
|
||||||
},
|
},
|
||||||
|
@ -386,10 +379,9 @@ private fun ComposeContentTestRule.clickCreateAndSend() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
private fun ComposeContentTestRule.clickConfirmation() {
|
||||||
private fun TestScope.clickConfirmation(interactionSource: MutableInteractionSource) {
|
onNodeWithText(getStringResource(R.string.send_confirm)).also {
|
||||||
launch(Dispatchers.Main) {
|
it.performClick()
|
||||||
interactionSource.emit(PressInteraction.Press(Offset.Zero))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package co.electriccoin.zcash.ui.screen.send.view
|
package co.electriccoin.zcash.ui.screen.send.view
|
||||||
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
@ -22,7 +21,6 @@ import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
@ -47,7 +45,6 @@ import co.electriccoin.zcash.ui.design.component.FormTextField
|
||||||
import co.electriccoin.zcash.ui.design.component.GradientSurface
|
import co.electriccoin.zcash.ui.design.component.GradientSurface
|
||||||
import co.electriccoin.zcash.ui.design.component.Header
|
import co.electriccoin.zcash.ui.design.component.Header
|
||||||
import co.electriccoin.zcash.ui.design.component.PrimaryButton
|
import co.electriccoin.zcash.ui.design.component.PrimaryButton
|
||||||
import co.electriccoin.zcash.ui.design.component.TimedButton
|
|
||||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme.dimens
|
import co.electriccoin.zcash.ui.design.theme.ZcashTheme.dimens
|
||||||
import co.electriccoin.zcash.ui.screen.send.ext.ABBREVIATION_INDEX
|
import co.electriccoin.zcash.ui.screen.send.ext.ABBREVIATION_INDEX
|
||||||
|
@ -69,16 +66,12 @@ fun PreviewSend() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param pressAndHoldInteractionSource This is an argument that can be injected for automated testing.
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun Send(
|
fun Send(
|
||||||
mySpendableBalance: Zatoshi,
|
mySpendableBalance: Zatoshi,
|
||||||
goBack: () -> Unit,
|
goBack: () -> Unit,
|
||||||
onCreateAndSend: (ZecSend) -> Unit,
|
onCreateAndSend: (ZecSend) -> Unit
|
||||||
pressAndHoldInteractionSource: MutableInteractionSource = remember { MutableInteractionSource() }
|
|
||||||
) {
|
) {
|
||||||
// For now, we're avoiding sub-navigation to keep the navigation logic simple. But this might
|
// For now, we're avoiding sub-navigation to keep the navigation logic simple. But this might
|
||||||
// change once deep-linking support is added. It depends on whether deep linking should do one of:
|
// change once deep-linking support is added. It depends on whether deep linking should do one of:
|
||||||
|
@ -98,7 +91,6 @@ fun Send(
|
||||||
SendMainContent(
|
SendMainContent(
|
||||||
myBalance = mySpendableBalance,
|
myBalance = mySpendableBalance,
|
||||||
sendStage = sendStage,
|
sendStage = sendStage,
|
||||||
pressAndHoldInteractionSource = pressAndHoldInteractionSource,
|
|
||||||
setSendStage = setSendStage,
|
setSendStage = setSendStage,
|
||||||
onCreateAndSend = onCreateAndSend,
|
onCreateAndSend = onCreateAndSend,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
@ -138,7 +130,6 @@ private fun SendTopAppBar(onBack: () -> Unit) {
|
||||||
private fun SendMainContent(
|
private fun SendMainContent(
|
||||||
myBalance: Zatoshi,
|
myBalance: Zatoshi,
|
||||||
sendStage: SendStage,
|
sendStage: SendStage,
|
||||||
pressAndHoldInteractionSource: MutableInteractionSource,
|
|
||||||
setSendStage: (SendStage) -> Unit,
|
setSendStage: (SendStage) -> Unit,
|
||||||
onCreateAndSend: (ZecSend) -> Unit,
|
onCreateAndSend: (ZecSend) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
|
@ -158,7 +149,6 @@ private fun SendMainContent(
|
||||||
} else {
|
} else {
|
||||||
Confirmation(
|
Confirmation(
|
||||||
zecSend = zecSend,
|
zecSend = zecSend,
|
||||||
pressAndHoldInteractionSource = pressAndHoldInteractionSource,
|
|
||||||
onConfirmation = {
|
onConfirmation = {
|
||||||
onCreateAndSend(zecSend)
|
onCreateAndSend(zecSend)
|
||||||
},
|
},
|
||||||
|
@ -293,7 +283,6 @@ private fun SendForm(
|
||||||
@Composable
|
@Composable
|
||||||
private fun Confirmation(
|
private fun Confirmation(
|
||||||
zecSend: ZecSend,
|
zecSend: ZecSend,
|
||||||
pressAndHoldInteractionSource: MutableInteractionSource,
|
|
||||||
onConfirmation: () -> Unit,
|
onConfirmation: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
@ -306,12 +295,9 @@ private fun Confirmation(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
TimedButton(
|
PrimaryButton(
|
||||||
onClick = onConfirmation,
|
onClick = onConfirmation,
|
||||||
{
|
text = stringResource(id = R.string.send_confirm)
|
||||||
Text(text = stringResource(id = R.string.send_confirm))
|
|
||||||
},
|
|
||||||
interactionSource = pressAndHoldInteractionSource
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,6 @@
|
||||||
|
|
||||||
<string name="send_amount_and_address_format" formatted="true">Send <xliff:g id="amount" example="12.345">%1$s</xliff:g> ZEC to <xliff:g id="address" example="zs1g7cqw … mvyzgm">%2$s</xliff:g>?</string>
|
<string name="send_amount_and_address_format" formatted="true">Send <xliff:g id="amount" example="12.345">%1$s</xliff:g> ZEC to <xliff:g id="address" example="zs1g7cqw … mvyzgm">%2$s</xliff:g>?</string>
|
||||||
<string name="send_abbreviated_address_format" formatted="true"><xliff:g id="first_five" example="zs1g7">%1$s</xliff:g>…<xliff:g id="last_five" example="mvyzg">%2$s</xliff:g></string>
|
<string name="send_abbreviated_address_format" formatted="true"><xliff:g id="first_five" example="zs1g7">%1$s</xliff:g>…<xliff:g id="last_five" example="mvyzg">%2$s</xliff:g></string>
|
||||||
<string name="send_confirm">Press and hold to send ZEC</string>
|
<string name="send_confirm">Press to send ZEC</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Reference in New Issue