Skip to content

Commit 5781408

Browse files
Add OHTTP relay settings screen with multiple custom relay support
1 parent 06d06e9 commit 5781408

14 files changed

Lines changed: 559 additions & 185 deletions

File tree

android/app/src/main/java/org/bitcoinppl/cove/flows/SettingsFlow/MainSettingsScreen.kt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,14 @@ fun MainSettingsScreen(
239239
)
240240
},
241241
)
242-
MaterialDivider()
242+
}
243+
}
244+
245+
WalletSettingsSection(app = app)
246+
247+
SectionHeader(stringResource(R.string.settings_advanced_section))
248+
MaterialSection {
249+
Column {
243250
MaterialSettingsItem(
244251
title = stringResource(R.string.title_settings_ohttp_relay),
245252
icon = Icons.Default.Hub,
@@ -253,8 +260,6 @@ fun MainSettingsScreen(
253260
}
254261
}
255262

256-
WalletSettingsSection(app = app)
257-
258263
SecuritySection(app = app)
259264

260265
BackupSection(
Lines changed: 141 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,33 @@
11
package org.bitcoinppl.cove.flows.SettingsFlow
22

3+
import androidx.compose.foundation.background
34
import androidx.compose.foundation.layout.Box
45
import androidx.compose.foundation.layout.Column
56
import androidx.compose.foundation.layout.Row
67
import androidx.compose.foundation.layout.WindowInsets
78
import androidx.compose.foundation.layout.asPaddingValues
89
import androidx.compose.foundation.layout.fillMaxSize
910
import androidx.compose.foundation.layout.fillMaxWidth
10-
import androidx.compose.foundation.layout.height
1111
import androidx.compose.foundation.layout.padding
1212
import androidx.compose.foundation.layout.safeDrawing
13-
import androidx.compose.foundation.layout.width
1413
import androidx.compose.foundation.rememberScrollState
1514
import androidx.compose.foundation.text.KeyboardActions
1615
import androidx.compose.foundation.text.KeyboardOptions
1716
import androidx.compose.foundation.verticalScroll
18-
import androidx.compose.material3.Button
19-
import androidx.compose.material3.CircularProgressIndicator
17+
import androidx.compose.material.icons.Icons
18+
import androidx.compose.material.icons.filled.Delete
2019
import androidx.compose.material3.ExperimentalMaterial3Api
20+
import androidx.compose.material3.Icon
2121
import androidx.compose.material3.MaterialTheme
2222
import androidx.compose.material3.OutlinedTextField
2323
import androidx.compose.material3.Scaffold
2424
import androidx.compose.material3.SnackbarHost
2525
import androidx.compose.material3.SnackbarHostState
26+
import androidx.compose.material3.SwipeToDismissBox
27+
import androidx.compose.material3.SwipeToDismissBoxValue
2628
import androidx.compose.material3.Text
2729
import androidx.compose.material3.TextButton
30+
import androidx.compose.material3.rememberSwipeToDismissBoxState
2831
import androidx.compose.runtime.Composable
2932
import androidx.compose.runtime.getValue
3033
import androidx.compose.runtime.mutableStateOf
@@ -43,6 +46,7 @@ import androidx.compose.ui.unit.dp
4346
import kotlinx.coroutines.launch
4447
import org.bitcoinppl.cove.R
4548
import org.bitcoinppl.cove.TaggedItem
49+
import org.bitcoinppl.cove.views.MaterialDivider
4650
import org.bitcoinppl.cove.views.MaterialSection
4751
import org.bitcoinppl.cove.views.SectionHeader
4852
import org.bitcoinppl.cove_core.AppAlertState
@@ -64,17 +68,18 @@ fun OhttpRelaySettingsScreen(
6468
modifier: Modifier = Modifier,
6569
) {
6670
val config = remember { Database().globalConfig() }
67-
val scope = rememberCoroutineScope()
6871
val snackbarHostState = remember { SnackbarHostState() }
72+
val scope = rememberCoroutineScope()
6973
val keyboardController = LocalSoftwareKeyboardController.current
70-
val savedMessage = stringResource(R.string.ohttp_relay_saved)
7174
val invalidUrlTitle = stringResource(R.string.ohttp_relay_invalid_url_title)
7275
val invalidUrlMessage = stringResource(R.string.ohttp_relay_invalid_url_message)
7376
val updateFailedTitle = stringResource(R.string.ohttp_relay_update_failed_title)
7477
val updateFailedMessage = stringResource(R.string.ohttp_relay_update_failed_message)
78+
val savedMessage = stringResource(R.string.ohttp_relay_saved)
7579

76-
var input by remember { mutableStateOf(config.ohttpRelayUrl() ?: "") }
77-
var isSaving by remember { mutableStateOf(false) }
80+
var relays by remember { mutableStateOf(config.ohttpRelayUrls()) }
81+
var newInput by remember { mutableStateOf("") }
82+
var isAdding by remember { mutableStateOf(false) }
7883

7984
fun showAlert(
8085
title: String,
@@ -89,41 +94,36 @@ fun OhttpRelaySettingsScreen(
8994
)
9095
}
9196

92-
fun save() {
93-
if (isSaving) return
94-
95-
val inputToSave = input
96-
scope.launch {
97-
isSaving = true
98-
try {
99-
val normalized = config.setOhttpRelayUrl(inputToSave)
100-
input = normalized ?: ""
101-
keyboardController?.hide()
102-
isSaving = false
103-
104-
launch {
105-
snackbarHostState.showSnackbar(savedMessage)
106-
}
107-
} catch (e: Exception) {
108-
if (e is DatabaseException.GlobalConfig &&
109-
e.v1 is GlobalConfigTableException.InvalidOhttpRelayUrl
110-
) {
111-
showAlert(invalidUrlTitle, invalidUrlMessage)
112-
} else {
113-
showAlert(updateFailedTitle, updateFailedMessage)
114-
}
115-
isSaving = false
97+
fun save(newRelays: List<String>, showSuccess: Boolean = true): Boolean {
98+
return try {
99+
relays = config.setOhttpRelayUrls(newRelays)
100+
newInput = ""
101+
isAdding = false
102+
keyboardController?.hide()
103+
if (showSuccess) scope.launch { snackbarHostState.showSnackbar(savedMessage) }
104+
true
105+
} catch (e: Exception) {
106+
if (e is DatabaseException.GlobalConfig &&
107+
e.v1 is GlobalConfigTableException.InvalidOhttpRelayUrl
108+
) {
109+
showAlert(invalidUrlTitle, invalidUrlMessage)
110+
} else {
111+
showAlert(updateFailedTitle, updateFailedMessage)
116112
}
113+
false
117114
}
118115
}
119116

120-
fun reset() {
121-
try {
122-
config.clearOhttpRelayUrl()
123-
input = ""
124-
} catch (e: Exception) {
125-
showAlert(updateFailedTitle, updateFailedMessage)
126-
}
117+
fun addRelay() {
118+
val url = newInput.trim()
119+
if (url.isEmpty()) return
120+
save(relays + url)
121+
}
122+
123+
fun deleteRelay(index: Int): Boolean {
124+
val updated = relays.toMutableList()
125+
updated.removeAt(index)
126+
return save(updated, showSuccess = false)
127127
}
128128

129129
Scaffold(
@@ -136,18 +136,6 @@ fun OhttpRelaySettingsScreen(
136136
SettingsTopAppBar(
137137
title = stringResource(R.string.title_settings_ohttp_relay),
138138
onBack = { app.popRoute() },
139-
actions = {
140-
if (isSaving) {
141-
Box(
142-
modifier = Modifier.padding(end = 16.dp),
143-
contentAlignment = Alignment.Center,
144-
) {
145-
CircularProgressIndicator(
146-
modifier = Modifier.width(24.dp).height(24.dp),
147-
)
148-
}
149-
}
150-
},
151139
)
152140
},
153141
) { paddingValues ->
@@ -184,42 +172,115 @@ fun OhttpRelaySettingsScreen(
184172

185173
SectionHeader(stringResource(R.string.ohttp_relay_custom_section))
186174
MaterialSection {
187-
Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) {
188-
OutlinedTextField(
189-
value = input,
190-
onValueChange = { input = it },
191-
label = { Text(stringResource(R.string.ohttp_relay_url_placeholder)) },
192-
keyboardOptions =
193-
KeyboardOptions(
194-
capitalization = KeyboardCapitalization.None,
195-
imeAction = ImeAction.Done,
196-
keyboardType = KeyboardType.Uri,
197-
),
198-
keyboardActions = KeyboardActions(onDone = { save() }),
199-
singleLine = true,
200-
modifier = Modifier.fillMaxWidth(),
201-
)
175+
Column {
176+
relays.forEachIndexed { index, relay ->
177+
RelayItem(
178+
relay = relay,
179+
onDelete = { deleteRelay(index) },
180+
)
181+
if (index < relays.lastIndex) {
182+
MaterialDivider(indent = 16.dp)
183+
}
184+
}
202185

203-
Row(
204-
modifier =
205-
Modifier
206-
.fillMaxWidth()
207-
.padding(top = 12.dp),
208-
verticalAlignment = Alignment.CenterVertically,
209-
) {
210-
Button(
211-
onClick = ::save,
212-
enabled = !isSaving,
186+
if (isAdding) {
187+
Row(
188+
modifier =
189+
Modifier
190+
.fillMaxWidth()
191+
.padding(horizontal = 16.dp, vertical = 8.dp),
192+
verticalAlignment = Alignment.CenterVertically,
213193
) {
214-
Text(stringResource(R.string.ohttp_relay_save))
215-
}
194+
OutlinedTextField(
195+
value = newInput,
196+
onValueChange = { newInput = it },
197+
label = {
198+
Text(stringResource(R.string.ohttp_relay_url_placeholder))
199+
},
200+
keyboardOptions =
201+
KeyboardOptions(
202+
capitalization = KeyboardCapitalization.None,
203+
imeAction = ImeAction.Done,
204+
keyboardType = KeyboardType.Uri,
205+
),
206+
keyboardActions = KeyboardActions(onDone = { addRelay() }),
207+
singleLine = true,
208+
modifier = Modifier.weight(1f),
209+
)
216210

217-
TextButton(onClick = ::reset) {
218-
Text(stringResource(R.string.ohttp_relay_reset))
211+
TextButton(
212+
onClick = ::addRelay,
213+
enabled = newInput.trim().isNotEmpty(),
214+
) {
215+
Text(stringResource(R.string.ohttp_relay_add))
216+
}
217+
}
218+
} else {
219+
TextButton(
220+
onClick = { isAdding = true },
221+
modifier = Modifier.padding(horizontal = 8.dp),
222+
) {
223+
Text(stringResource(R.string.ohttp_relay_add_relay))
219224
}
220225
}
221226
}
222227
}
223228
}
224229
}
225230
}
231+
232+
@OptIn(ExperimentalMaterial3Api::class)
233+
@Composable
234+
private fun RelayItem(
235+
relay: String,
236+
onDelete: () -> Boolean,
237+
) {
238+
val dismissState =
239+
rememberSwipeToDismissBoxState(
240+
confirmValueChange = { value ->
241+
if (value == SwipeToDismissBoxValue.EndToStart) {
242+
onDelete()
243+
} else {
244+
false
245+
}
246+
},
247+
positionalThreshold = { it * 0.4f },
248+
)
249+
250+
SwipeToDismissBox(
251+
state = dismissState,
252+
backgroundContent = {
253+
Box(
254+
modifier =
255+
Modifier
256+
.fillMaxSize()
257+
.background(MaterialTheme.colorScheme.error)
258+
.padding(end = 16.dp),
259+
contentAlignment = Alignment.CenterEnd,
260+
) {
261+
Icon(
262+
imageVector = Icons.Default.Delete,
263+
contentDescription = null,
264+
tint = MaterialTheme.colorScheme.onError,
265+
)
266+
}
267+
},
268+
enableDismissFromStartToEnd = false,
269+
) {
270+
Row(
271+
modifier =
272+
Modifier
273+
.fillMaxWidth()
274+
.background(MaterialTheme.colorScheme.surface)
275+
.padding(horizontal = 16.dp, vertical = 12.dp),
276+
verticalAlignment = Alignment.CenterVertically,
277+
) {
278+
Text(
279+
text = relay,
280+
style = MaterialTheme.typography.bodySmall,
281+
fontFamily = FontFamily.Monospace,
282+
modifier = Modifier.weight(1f),
283+
)
284+
}
285+
}
286+
}

0 commit comments

Comments
 (0)