Skip to content

Commit 1c19c1c

Browse files
authored
Merge pull request #917 from ooni/descriptor-websites-screen
Show all descriptor websites in a separate screen
2 parents 5416df5 + 1ddd938 commit 1c19c1c

File tree

24 files changed

+320
-176
lines changed

24 files changed

+320
-176
lines changed

composeApp/src/commonMain/composeResources/values/strings-common.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
<string name="Dashboard_Runv2_Overview_UninstallLink">Uninstall Link</string>
2929
<string name="Dashboard_Runv2_Overview_PreviousRevisions">Previous revisions</string>
3030
<string name="Dashboard_Runv2_Overview_SeeMore">See More</string>
31+
<string name="Dashboard_Runv2_Websites_SeeAll">See all %1$d websites</string>
3132
<string name="Dashboard_RunV2_ExpiredTag">EXPIRED</string>
3233
<string name="Dashboard_RunV2_UpdatedTag">UPDATED</string>
3334
<string name="Dashboard_RunV2_UpdateTag">UPDATE</string>

composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ import org.ooni.probe.ui.dashboard.DashboardViewModel
9797
import org.ooni.probe.ui.descriptor.DescriptorViewModel
9898
import org.ooni.probe.ui.descriptor.add.AddDescriptorViewModel
9999
import org.ooni.probe.ui.descriptor.review.ReviewUpdatesViewModel
100+
import org.ooni.probe.ui.descriptor.websites.DescriptorWebsitesViewModel
100101
import org.ooni.probe.ui.log.LogViewModel
101102
import org.ooni.probe.ui.measurement.MeasurementRawViewModel
102103
import org.ooni.probe.ui.measurement.MeasurementViewModel
@@ -557,13 +558,15 @@ class Dependencies(
557558
goToReviewDescriptorUpdates: (List<InstalledTestDescriptorModel.Id>?) -> Unit,
558559
goToChooseWebsites: () -> Unit,
559560
goToResult: (ResultModel.Id) -> Unit,
561+
goToDescriptorWebsites: (InstalledTestDescriptorModel.Id) -> Unit,
560562
) = DescriptorViewModel(
561563
descriptorKey = descriptorKey,
562564
onBack = onBack,
563565
goToReviewDescriptorUpdates = goToReviewDescriptorUpdates,
564566
goToChooseWebsites = goToChooseWebsites,
565567
goToResult = goToResult,
566-
getLatestTestDescriptors = getTestDescriptors::latest,
568+
goToDescriptorWebsites = goToDescriptorWebsites,
569+
getTestDescriptor = getTestDescriptors::single,
567570
getLastResultOfDescriptor = getLastResultOfDescriptor::invoke,
568571
preferenceRepository = preferenceRepository,
569572
launchAction = launchAction::invoke,
@@ -578,6 +581,15 @@ class Dependencies(
578581
canPullToRefresh = platformInfo.canPullToRefresh,
579582
)
580583

584+
fun descriptorWebsitesViewModel(
585+
descriptorId: InstalledTestDescriptorModel.Id,
586+
onBack: () -> Unit,
587+
) = DescriptorWebsitesViewModel(
588+
descriptorId = descriptorId,
589+
onBack = onBack,
590+
getTestDescriptor = getTestDescriptors::single,
591+
)
592+
581593
fun logViewModel(onBack: () -> Unit) =
582594
LogViewModel(
583595
onBack = onBack,

composeApp/src/commonMain/kotlin/org/ooni/probe/domain/descriptors/GetTestDescriptors.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ class GetTestDescriptors(
2929

3030
fun latest(): Flow<List<Descriptor>> = get(listLatestInstalledTestDescriptors)
3131

32+
fun single(key: String) =
33+
latest().map { list ->
34+
list.firstOrNull { it.key == key }
35+
}
36+
3237
private fun get(installedDescriptorFlow: () -> Flow<List<InstalledTestDescriptorModel>>): Flow<List<Descriptor>> {
3338
return combine(
3439
installedDescriptorFlow(),

composeApp/src/commonMain/kotlin/org/ooni/probe/ui/choosewebsites/ChooseWebsitesScreen.kt

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ import androidx.compose.foundation.layout.padding
1515
import androidx.compose.foundation.lazy.LazyColumn
1616
import androidx.compose.foundation.lazy.itemsIndexed
1717
import androidx.compose.foundation.text.KeyboardOptions
18-
import androidx.compose.material.icons.Icons
19-
import androidx.compose.material.icons.automirrored.filled.ArrowBack
2018
import androidx.compose.material3.AlertDialog
2119
import androidx.compose.material3.Button
2220
import androidx.compose.material3.ButtonDefaults
@@ -32,7 +30,6 @@ import androidx.compose.ui.Modifier
3230
import androidx.compose.ui.platform.testTag
3331
import androidx.compose.ui.text.input.KeyboardType
3432
import androidx.compose.ui.unit.dp
35-
import ooniprobe.composeapp.generated.resources.Common_Back
3633
import ooniprobe.composeapp.generated.resources.Common_Clear
3734
import ooniprobe.composeapp.generated.resources.CustomWebsites_Fab_Text
3835
import ooniprobe.composeapp.generated.resources.Modal_Cancel
@@ -51,6 +48,7 @@ import org.jetbrains.compose.resources.painterResource
5148
import org.jetbrains.compose.resources.pluralStringResource
5249
import org.jetbrains.compose.resources.stringResource
5350
import org.jetbrains.compose.ui.tooling.preview.Preview
51+
import org.ooni.probe.ui.shared.NavigationBackButton
5452
import org.ooni.probe.ui.shared.TopBar
5553
import org.ooni.probe.ui.theme.AppTheme
5654

@@ -65,12 +63,7 @@ fun ChooseWebsitesScreen(
6563
TopBar(
6664
title = { Text(stringResource(Res.string.Settings_Websites_CustomURL_Title)) },
6765
navigationIcon = {
68-
IconButton(onClick = { onEvent(ChooseWebsitesViewModel.Event.BackClicked) }) {
69-
Icon(
70-
Icons.AutoMirrored.Filled.ArrowBack,
71-
contentDescription = stringResource(Res.string.Common_Back),
72-
)
73-
}
66+
NavigationBackButton({ onEvent(ChooseWebsitesViewModel.Event.BackClicked) })
7467
},
7568
actions = {
7669
if (state.canClearUrls) {

composeApp/src/commonMain/kotlin/org/ooni/probe/ui/descriptor/DescriptorScreen.kt

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,10 @@ import androidx.compose.foundation.selection.toggleable
1616
import androidx.compose.foundation.selection.triStateToggleable
1717
import androidx.compose.foundation.shape.RoundedCornerShape
1818
import androidx.compose.foundation.verticalScroll
19-
import androidx.compose.material.icons.Icons
20-
import androidx.compose.material.icons.automirrored.filled.ArrowBack
2119
import androidx.compose.material3.ButtonDefaults
2220
import androidx.compose.material3.Checkbox
2321
import androidx.compose.material3.HorizontalDivider
2422
import androidx.compose.material3.Icon
25-
import androidx.compose.material3.IconButton
2623
import androidx.compose.material3.MaterialTheme
2724
import androidx.compose.material3.OutlinedButton
2825
import androidx.compose.material3.Surface
@@ -45,11 +42,11 @@ import androidx.compose.ui.unit.dp
4542
import ooniprobe.composeapp.generated.resources.AddDescriptor_AutoRun
4643
import ooniprobe.composeapp.generated.resources.AddDescriptor_AutoRunDisabled
4744
import ooniprobe.composeapp.generated.resources.AddDescriptor_Settings
48-
import ooniprobe.composeapp.generated.resources.Common_Back
4945
import ooniprobe.composeapp.generated.resources.Common_Enable
5046
import ooniprobe.composeapp.generated.resources.Dashboard_Overview_ChooseWebsites
5147
import ooniprobe.composeapp.generated.resources.Dashboard_Overview_Estimated
5248
import ooniprobe.composeapp.generated.resources.Dashboard_Runv2_Overview_ReviewUpdates
49+
import ooniprobe.composeapp.generated.resources.Dashboard_Runv2_Websites_SeeAll
5350
import ooniprobe.composeapp.generated.resources.Descriptor_LastTestResult
5451
import ooniprobe.composeapp.generated.resources.OONIRun_Run
5552
import ooniprobe.composeapp.generated.resources.Res
@@ -67,12 +64,13 @@ import org.ooni.probe.ui.results.ResultCell
6764
import org.ooni.probe.ui.shared.DisableVpnInstructionsDialog
6865
import org.ooni.probe.ui.shared.ExpiredChip
6966
import org.ooni.probe.ui.shared.MarkdownViewer
67+
import org.ooni.probe.ui.shared.NavigationBackButton
7068
import org.ooni.probe.ui.shared.SelectableItem
7169
import org.ooni.probe.ui.shared.TopBar
7270
import org.ooni.probe.ui.shared.UpdateProgressStatus
7371
import org.ooni.probe.ui.shared.VpnWarningDialog
74-
import org.ooni.probe.ui.shared.isHeightCompact
7572
import org.ooni.probe.ui.shared.format
73+
import org.ooni.probe.ui.shared.isHeightCompact
7674
import org.ooni.probe.ui.theme.LocalCustomColors
7775

7876
@Composable
@@ -103,12 +101,7 @@ fun DescriptorScreen(
103101
)
104102
},
105103
navigationIcon = {
106-
IconButton(onClick = { onEvent(DescriptorViewModel.Event.BackClicked) }) {
107-
Icon(
108-
Icons.AutoMirrored.Filled.ArrowBack,
109-
contentDescription = stringResource(Res.string.Common_Back),
110-
)
111-
}
104+
NavigationBackButton({ onEvent(DescriptorViewModel.Event.BackClicked) })
112105
},
113106
colors = TopAppBarDefaults.topAppBarColors(
114107
containerColor = descriptorColor,
@@ -226,24 +219,32 @@ fun DescriptorScreen(
226219

227220
when (OrganizationConfig.testDisplayMode) {
228221
TestDisplayMode.Regular -> {
229-
if (!state.tests.isSingleWebConnectivityTest() || descriptor.source is Descriptor.Source.Default) {
222+
if (descriptor.source is Descriptor.Source.Default || !state.tests.isSingleWebConnectivityTest()) {
230223
TestItems(
231224
descriptor,
232225
state.tests,
233226
state.isAutoRunEnabled,
234227
onEvent,
235228
)
229+
} else if (state.tests.isSingleWebConnectivityTest()) {
230+
WebsiteItems(
231+
tests = state.tests,
232+
onSeeMoreClick = {
233+
onEvent(DescriptorViewModel.Event.SeeMoreWebsitesClicked)
234+
},
235+
)
236236
}
237237
}
238238

239-
TestDisplayMode.WebsitesOnly -> WebsiteItems(state.tests)
239+
TestDisplayMode.WebsitesOnly -> WebsiteItems(
240+
tests = state.tests,
241+
onSeeMoreClick = {
242+
onEvent(DescriptorViewModel.Event.SeeMoreWebsitesClicked)
243+
},
244+
)
240245
}
241246

242247
if (descriptor.source is Descriptor.Source.Installed) {
243-
if (state.tests.isSingleWebConnectivityTest()) {
244-
WebsiteItems(state.tests)
245-
}
246-
247248
InstalledDescriptorActionsView(
248249
descriptor = descriptor.source.value,
249250
showCheckUpdatesButton = !state.canPullToRefresh,
@@ -444,16 +445,32 @@ private fun TestItems(
444445
}
445446

446447
@Composable
447-
private fun WebsiteItems(tests: List<SelectableItem<NetTest>>) {
448+
private fun WebsiteItems(
449+
tests: List<SelectableItem<NetTest>>,
450+
onSeeMoreClick: () -> Unit,
451+
) {
448452
val websites = tests
449453
.map { it.item }
450454
.filter { it.test is TestType.WebConnectivity }
451455
.flatMap { it.inputs.orEmpty() }
452456

453-
websites.forEach { website ->
454-
Text(
455-
website,
456-
Modifier.padding(start = 48.dp, top = 4.dp),
457-
)
457+
websites
458+
.take(MAX_WEBSITES_TO_SHOW)
459+
.forEach { website ->
460+
Text(
461+
website,
462+
modifier = Modifier.padding(start = 48.dp, top = 4.dp),
463+
)
464+
}
465+
466+
if (websites.size > MAX_WEBSITES_TO_SHOW) {
467+
TextButton(
468+
onClick = onSeeMoreClick,
469+
modifier = Modifier.padding(start = 48.dp, top = 4.dp),
470+
) {
471+
Text(stringResource(Res.string.Dashboard_Runv2_Websites_SeeAll, websites.size))
472+
}
458473
}
459474
}
475+
476+
private const val MAX_WEBSITES_TO_SHOW = 10

composeApp/src/commonMain/kotlin/org/ooni/probe/ui/descriptor/DescriptorViewModel.kt

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ class DescriptorViewModel(
4141
goToReviewDescriptorUpdates: (List<InstalledTestDescriptorModel.Id>?) -> Unit,
4242
goToChooseWebsites: () -> Unit,
4343
goToResult: (ResultModel.Id) -> Unit,
44-
private val getLatestTestDescriptors: () -> Flow<List<Descriptor>>,
44+
goToDescriptorWebsites: (InstalledTestDescriptorModel.Id) -> Unit,
45+
getTestDescriptor: (String) -> Flow<Descriptor?>,
4546
getLastResultOfDescriptor: (String) -> Flow<ResultListItem?>,
4647
private val preferenceRepository: PreferenceRepository,
4748
private val launchAction: (PlatformAction) -> Boolean,
@@ -70,7 +71,7 @@ class DescriptorViewModel(
7071
}
7172
}.launchIn(viewModelScope)
7273

73-
getDescriptor()
74+
getTestDescriptor(descriptorKey)
7475
.onEach { if (it == null) onBack() }
7576
.filterNotNull()
7677
.flatMapLatest { descriptor ->
@@ -266,17 +267,19 @@ class DescriptorViewModel(
266267
event.resultListItem.result.id
267268
?.let { goToResult(it) }
268269
}.launchIn(viewModelScope)
270+
271+
events
272+
.filterIsInstance<Event.SeeMoreWebsitesClicked>()
273+
.onEach {
274+
(state.value.descriptor?.source as? Descriptor.Source.Installed)
275+
?.let { descriptor -> goToDescriptorWebsites(descriptor.value.id) }
276+
}.launchIn(viewModelScope)
269277
}
270278

271279
fun onEvent(event: Event) {
272280
events.tryEmit(event)
273281
}
274282

275-
private fun getDescriptor() =
276-
getLatestTestDescriptors().map { list ->
277-
list.firstOrNull { it.key == descriptorKey }
278-
}
279-
280283
private fun getMaxRuntime(): Flow<Duration?> =
281284
preferenceRepository
282285
.allSettings(
@@ -392,6 +395,8 @@ class DescriptorViewModel(
392395
data class ResultClicked(
393396
val resultListItem: ResultListItem,
394397
) : Event
398+
399+
data object SeeMoreWebsitesClicked : Event
395400
}
396401
}
397402

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package org.ooni.probe.ui.descriptor.websites
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.layout.Box
5+
import androidx.compose.foundation.layout.Column
6+
import androidx.compose.foundation.layout.PaddingValues
7+
import androidx.compose.foundation.layout.WindowInsets
8+
import androidx.compose.foundation.layout.asPaddingValues
9+
import androidx.compose.foundation.layout.fillMaxSize
10+
import androidx.compose.foundation.layout.navigationBars
11+
import androidx.compose.foundation.layout.padding
12+
import androidx.compose.foundation.lazy.LazyColumn
13+
import androidx.compose.foundation.lazy.items
14+
import androidx.compose.foundation.lazy.rememberLazyListState
15+
import androidx.compose.foundation.text.selection.SelectionContainer
16+
import androidx.compose.material3.MaterialTheme
17+
import androidx.compose.material3.Text
18+
import androidx.compose.material3.TopAppBarDefaults
19+
import androidx.compose.runtime.Composable
20+
import androidx.compose.ui.Alignment
21+
import androidx.compose.ui.Modifier
22+
import androidx.compose.ui.unit.dp
23+
import org.ooni.probe.ui.shared.NavigationBackButton
24+
import org.ooni.probe.ui.shared.TopBar
25+
import org.ooni.probe.ui.shared.VerticalScrollbar
26+
import org.ooni.probe.ui.theme.LocalCustomColors
27+
28+
@Composable
29+
fun DescriptorWebsitesViewModel(
30+
state: DescriptorWebsitesViewModel.State,
31+
onEvent: (DescriptorWebsitesViewModel.Event) -> Unit,
32+
) {
33+
val showState = state as? DescriptorWebsitesViewModel.State.Show
34+
35+
Column(Modifier.background(MaterialTheme.colorScheme.background)) {
36+
val descriptorColor = showState?.color ?: MaterialTheme.colorScheme.primary
37+
val onDescriptorColor = LocalCustomColors.current.onDescriptor
38+
39+
TopBar(
40+
title = { Text(showState?.title().orEmpty()) },
41+
navigationIcon = {
42+
NavigationBackButton({ onEvent(DescriptorWebsitesViewModel.Event.BackClicked) })
43+
},
44+
colors = TopAppBarDefaults.topAppBarColors(
45+
containerColor = descriptorColor,
46+
scrolledContainerColor = descriptorColor,
47+
navigationIconContentColor = onDescriptorColor,
48+
titleContentColor = onDescriptorColor,
49+
actionIconContentColor = onDescriptorColor,
50+
),
51+
)
52+
53+
Box {
54+
val lazyListState = rememberLazyListState()
55+
LazyColumn(
56+
contentPadding = PaddingValues(
57+
top = 8.dp,
58+
bottom = WindowInsets.navigationBars
59+
.asPaddingValues()
60+
.calculateBottomPadding() + 8.dp,
61+
),
62+
state = lazyListState,
63+
modifier = Modifier.fillMaxSize(),
64+
) {
65+
items(showState?.websites.orEmpty()) { website ->
66+
SelectionContainer {
67+
Text(
68+
text = website,
69+
modifier = Modifier.padding(horizontal = 16.dp, vertical = 2.dp),
70+
)
71+
}
72+
}
73+
}
74+
VerticalScrollbar(
75+
state = lazyListState,
76+
modifier = Modifier.align(Alignment.CenterEnd),
77+
)
78+
}
79+
}
80+
}

0 commit comments

Comments
 (0)