Skip to content

Commit 52aba99

Browse files
committed
OONI articles
1 parent 9b4dcbf commit 52aba99

File tree

32 files changed

+1135
-76
lines changed

32 files changed

+1135
-76
lines changed

composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/RunningTestsTest.kt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ import androidx.compose.ui.test.performClick
88
import androidx.test.ext.junit.runners.AndroidJUnit4
99
import kotlinx.coroutines.test.runTest
1010
import ooniprobe.composeapp.generated.resources.Common_Expand
11+
import ooniprobe.composeapp.generated.resources.Dashboard_LastResults_SeeResults
1112
import ooniprobe.composeapp.generated.resources.Dashboard_RunTests_RunButton_Label
1213
import ooniprobe.composeapp.generated.resources.Dashboard_RunTests_SelectNone
13-
import ooniprobe.composeapp.generated.resources.Dashboard_RunV2_RunFinished
14+
import ooniprobe.composeapp.generated.resources.Dashboard_LastResults_SeeResults
1415
import ooniprobe.composeapp.generated.resources.Measurement_Title
1516
import ooniprobe.composeapp.generated.resources.OONIRun_Run
1617
import ooniprobe.composeapp.generated.resources.Res
@@ -66,7 +67,7 @@ class RunningTestsTest {
6667
clickOnText(Res.string.Test_Signal_Fullname)
6768
clickOnRunButton(1)
6869

69-
clickOnText(Res.string.Dashboard_RunV2_RunFinished, timeout = TEST_WAIT_TIMEOUT)
70+
clickOnText(Res.string.Dashboard_LastResults_SeeResults, timeout = TEST_WAIT_TIMEOUT)
7071

7172
clickOnText(Res.string.Test_InstantMessaging_Fullname)
7273
clickOnText(Res.string.Test_Signal_Fullname)
@@ -87,7 +88,7 @@ class RunningTestsTest {
8788
clickOnText(Res.string.Test_Psiphon_Fullname)
8889
clickOnRunButton(1)
8990

90-
clickOnText(Res.string.Dashboard_RunV2_RunFinished, timeout = TEST_WAIT_TIMEOUT)
91+
clickOnText(Res.string.Dashboard_LastResults_SeeResults, timeout = TEST_WAIT_TIMEOUT)
9192

9293
clickOnText(Res.string.Test_Circumvention_Fullname)
9394
clickOnText(Res.string.Test_Psiphon_Fullname)
@@ -108,7 +109,7 @@ class RunningTestsTest {
108109
clickOnText("HTTP Header", substring = true)
109110
clickOnRunButton(1)
110111

111-
clickOnText(Res.string.Dashboard_RunV2_RunFinished, timeout = TEST_WAIT_TIMEOUT)
112+
clickOnText(Res.string.Dashboard_LastResults_SeeResults, timeout = TEST_WAIT_TIMEOUT)
112113

113114
clickOnText(Res.string.Test_Performance_Fullname)
114115
clickOnText("HTTP Header", substring = true)
@@ -129,7 +130,7 @@ class RunningTestsTest {
129130
clickOnText("stunreachability", substring = true)
130131
clickOnRunButton(1)
131132

132-
clickOnText(Res.string.Dashboard_RunV2_RunFinished, timeout = TEST_WAIT_TIMEOUT)
133+
clickOnText(Res.string.Dashboard_LastResults_SeeResults, timeout = TEST_WAIT_TIMEOUT)
133134

134135
clickOnText(Res.string.Test_Experimental_Fullname)
135136
compose.onAllNodesWithText("stunreachability")[0].performClick()
@@ -150,7 +151,7 @@ class RunningTestsTest {
150151
clickOnText("Trusted International Media")
151152
clickOnRunButton(1)
152153

153-
clickOnText(Res.string.Dashboard_RunV2_RunFinished, timeout = TEST_WAIT_TIMEOUT)
154+
clickOnText(Res.string.Dashboard_LastResults_SeeResults, timeout = TEST_WAIT_TIMEOUT)
154155

155156
clickOnText("Trusted International Media")
156157
clickOnText("https://www.dw.com")

composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/helpers/ComposeTestHelpers.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import androidx.compose.ui.test.SemanticsNodeInteraction
44
import androidx.compose.ui.test.isDisplayed
55
import androidx.compose.ui.test.junit4.ComposeTestRule
66
import androidx.compose.ui.test.onAllNodesWithText
7+
import androidx.compose.ui.test.onFirst
78
import androidx.compose.ui.test.onNodeWithContentDescription
89
import androidx.compose.ui.test.onNodeWithTag
910
import androidx.compose.ui.test.onNodeWithText
@@ -31,8 +32,8 @@ fun ComposeTestRule.clickOnText(
3132
substring: Boolean = false,
3233
timeout: Duration = DEFAULT_WAIT_TIMEOUT,
3334
): SemanticsNodeInteraction {
34-
wait(timeout) { onNodeWithText(text, substring = substring).isDisplayed() }
35-
return onNodeWithText(text, substring = substring).performClick()
35+
wait(timeout) { onAllNodesWithText(text, substring = substring).onFirst().isDisplayed() }
36+
return onAllNodesWithText(text, substring = substring).onFirst().performClick()
3637
}
3738

3839
suspend fun ComposeTestRule.clickOnContentDescription(stringRes: StringResource) = clickOnContentDescription(getString(stringRes))

composeApp/src/androidInstrumentedTest/kotlin/org/ooni/probe/uitesting/helpers/StateTestHelpers.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ import org.ooni.probe.data.models.SettingsKey
44
import org.ooni.probe.domain.organizationPreferenceDefaults
55

66
suspend fun skipOnboarding() {
7-
preferences.setValueByKey(SettingsKey.FIRST_RUN, false)
7+
preferences.setValuesByKey(
8+
listOf(
9+
SettingsKey.FIRST_RUN to false,
10+
SettingsKey.TESTS_MOVED_NOTICE to true,
11+
)
12+
)
813
}
914

1015
suspend fun defaultSettings() {

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@
4242
</plurals>
4343
<string name="Dashboard_Stats_Empty">Start running tests to see your statistics here.</string>
4444

45+
<string name="Dashboard_Articles_Title">OONI News</string>
46+
<string name="Dashboard_Articles_Blog">Blog Post</string>
47+
<string name="Dashboard_Articles_Finding">Finding</string>
48+
<string name="Dashboard_Articles_Report">Report</string>
49+
<string name="Dashboard_Articles_ReadMore">Read More</string>
50+
<string name="Dashboard_Articles_Recent">Recent</string>
51+
4552
<string name="Dashboard_RunV2_Ooni_Title">OONI Tests</string>
4653
<string name="Dashboard_RunV2_Title">OONI Run Links</string>
4754
<string name="Dashboard_Runv2_Overview_Description">Created by %1$s on %2$s</string>
@@ -92,11 +99,9 @@
9299
<string name="Test_Tor_Fullname">Tor Test</string>
93100
<string name="Test_Signal_Fullname">Signal Test</string>
94101

95-
<!-- Test Results -->
102+
<!-- Results -->
96103

97-
<string name="TestResults_Overview_Title">Test Results</string>
98-
<string name="TestResults_Overview_Tab_Label">Test Results</string>
99-
<string name="TestResults_Overview_Hero_Results">Results</string>
104+
<string name="TestResults">Results</string>
100105
<string name="TestResults_Overview_Hero_Networks">Networks</string>
101106
<string name="TestResults_Overview_Hero_DataUsage">Data Usage</string>
102107
<plurals name="TestResults_Overview_Websites_Blocked">

composeApp/src/commonMain/kotlin/org/ooni/probe/App.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ fun App(
125125
LaunchedEffect(Unit) {
126126
dependencies.finishInProgressData()
127127
dependencies.deleteOldResults()
128+
dependencies.refreshArticles()
128129
}
129130
LaunchedEffect(Unit) {
130131
dependencies.observeAndConfigureAutoRun()

composeApp/src/commonMain/kotlin/org/ooni/probe/config/OrganizationConfig.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ interface OrganizationConfigInterface {
1010
val updateDescriptorTaskId: String
1111
val hasWebsitesDescriptor: Boolean
1212
val donateUrl: String?
13+
val hasOoniNews: Boolean
1314

1415
val ooniApiBaseUrl get() = BuildTypeDefaults.ooniApiBaseUrl
1516
val ooniRunDomain get() = BuildTypeDefaults.ooniRunDomain
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package org.ooni.probe.data.models
2+
3+
import kotlinx.datetime.DateTimeUnit
4+
import kotlinx.datetime.LocalDate
5+
import kotlinx.datetime.LocalDateTime
6+
import kotlinx.datetime.minus
7+
import org.ooni.probe.shared.today
8+
9+
data class ArticleModel(
10+
val url: Url,
11+
val title: String,
12+
val description: String?,
13+
val source: Source,
14+
val time: LocalDateTime,
15+
) {
16+
data class Url(
17+
val value: String,
18+
)
19+
20+
val isRecent get() = time.date >= LocalDate.today().minus(7, DateTimeUnit.DAY)
21+
22+
enum class Source(
23+
val value: String,
24+
) {
25+
Blog("blog"),
26+
Finding("finding"),
27+
Report("report"),
28+
;
29+
30+
companion object {
31+
fun fromValue(value: String) = entries.firstOrNull { it.value == value }
32+
}
33+
}
34+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package org.ooni.probe.data.repositories
2+
3+
import app.cash.sqldelight.coroutines.asFlow
4+
import app.cash.sqldelight.coroutines.mapToList
5+
import kotlinx.coroutines.flow.Flow
6+
import kotlinx.coroutines.flow.map
7+
import kotlinx.coroutines.withContext
8+
import org.ooni.probe.Database
9+
import org.ooni.probe.data.Article
10+
import org.ooni.probe.data.models.ArticleModel
11+
import org.ooni.probe.shared.toEpoch
12+
import org.ooni.probe.shared.toLocalDateTime
13+
import kotlin.coroutines.CoroutineContext
14+
15+
class ArticleRepository(
16+
private val database: Database,
17+
private val backgroundContext: CoroutineContext,
18+
) {
19+
suspend fun refresh(models: List<ArticleModel>) {
20+
withContext(backgroundContext) {
21+
database.transaction {
22+
models.forEach { model ->
23+
database.articleQueries.insertOrReplace(
24+
url = model.url.value,
25+
title = model.title,
26+
description = model.description,
27+
source = model.source.value,
28+
time = model.time.toEpoch(),
29+
)
30+
}
31+
database.articleQueries.deleteExceptUrls(models.map { it.url.value })
32+
}
33+
}
34+
}
35+
36+
fun list(): Flow<List<ArticleModel>> =
37+
database.articleQueries
38+
.selectAll()
39+
.asFlow()
40+
.mapToList(backgroundContext)
41+
.map { list -> list.mapNotNull { it.toModel() } }
42+
43+
private fun Article.toModel() =
44+
run {
45+
ArticleModel(
46+
url = ArticleModel.Url(url),
47+
title = title,
48+
description = description,
49+
source = ArticleModel.Source.fromValue(source) ?: return@run null,
50+
time = time.toLocalDateTime(),
51+
)
52+
}
53+
}

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import org.ooni.probe.data.disk.ReadFile
2828
import org.ooni.probe.data.disk.ReadFileOkio
2929
import org.ooni.probe.data.disk.WriteFile
3030
import org.ooni.probe.data.disk.WriteFileOkio
31+
import org.ooni.probe.data.models.ArticleModel
3132
import org.ooni.probe.data.models.AutoRunParameters
3233
import org.ooni.probe.data.models.BatteryState
3334
import org.ooni.probe.data.models.InstalledTestDescriptorModel
@@ -38,6 +39,7 @@ import org.ooni.probe.data.models.PreferenceCategoryKey
3839
import org.ooni.probe.data.models.ResultModel
3940
import org.ooni.probe.data.models.RunSpecification
4041
import org.ooni.probe.data.repositories.AppReviewRepository
42+
import org.ooni.probe.data.repositories.ArticleRepository
4143
import org.ooni.probe.data.repositories.MeasurementRepository
4244
import org.ooni.probe.data.repositories.NetworkRepository
4345
import org.ooni.probe.data.repositories.PreferenceRepository
@@ -72,6 +74,8 @@ import org.ooni.probe.domain.ShouldShowVpnWarning
7274
import org.ooni.probe.domain.UploadMissingMeasurements
7375
import org.ooni.probe.domain.appreview.MarkAppReviewAsShown
7476
import org.ooni.probe.domain.appreview.ShouldShowAppReview
77+
import org.ooni.probe.domain.articles.GetArticles
78+
import org.ooni.probe.domain.articles.RefreshArticles
7579
import org.ooni.probe.domain.descriptors.AcceptDescriptorUpdate
7680
import org.ooni.probe.domain.descriptors.BootstrapTestDescriptors
7781
import org.ooni.probe.domain.descriptors.DeleteTestDescriptor
@@ -95,6 +99,8 @@ import org.ooni.probe.domain.results.GetResults
9599
import org.ooni.probe.shared.PlatformInfo
96100
import org.ooni.probe.shared.monitoring.AppLogger
97101
import org.ooni.probe.shared.monitoring.CrashMonitoring
102+
import org.ooni.probe.ui.articles.ArticleViewModel
103+
import org.ooni.probe.ui.articles.ArticlesViewModel
98104
import org.ooni.probe.ui.choosewebsites.ChooseWebsitesViewModel
99105
import org.ooni.probe.ui.dashboard.DashboardViewModel
100106
import org.ooni.probe.ui.descriptor.DescriptorViewModel
@@ -157,6 +163,9 @@ class Dependencies(
157163

158164
private val appReviewRepository by lazy { AppReviewRepository(dataStore) }
159165

166+
@VisibleForTesting
167+
val articleRepository by lazy { ArticleRepository(database, backgroundContext) }
168+
160169
@VisibleForTesting
161170
val measurementRepository by lazy {
162171
MeasurementRepository(database, json, backgroundContext)
@@ -315,6 +324,9 @@ class Dependencies(
315324
updateState = descriptorUpdateStateManager::update,
316325
)
317326
}
327+
private val getArticles by lazy {
328+
GetArticles(articleRepository::list)
329+
}
318330
val getAutoRunSettings by lazy { GetAutoRunSettings(preferenceRepository::allSettings) }
319331
private val getAutoRunSpecification by lazy {
320332
GetAutoRunSpecification(getTestDescriptors::latest, preferenceRepository)
@@ -486,6 +498,12 @@ class Dependencies(
486498
private val shouldShowVpnWarning by lazy {
487499
ShouldShowVpnWarning(preferenceRepository, networkTypeFinder::invoke)
488500
}
501+
val refreshArticles by lazy {
502+
RefreshArticles(
503+
httpDo = engine::httpDo,
504+
refreshArticlesInDatabase = articleRepository::refresh,
505+
)
506+
}
489507
val runBackgroundStateManager by lazy { RunBackgroundStateManager() }
490508
private val undoRejectedDescriptorUpdate by lazy {
491509
UndoRejectedDescriptorUpdate(
@@ -565,6 +583,27 @@ class Dependencies(
565583
startBackgroundRun = startSingleRunInner,
566584
)
567585

586+
fun articleViewModel(
587+
url: ArticleModel.Url,
588+
onBack: () -> Unit,
589+
) = ArticleViewModel(
590+
url = url,
591+
onBack = onBack,
592+
launchAction = launchAction::invoke,
593+
isWebViewAvailable = isWebViewAvailable,
594+
)
595+
596+
fun articlesViewModel(
597+
onBack: () -> Unit,
598+
goToArticle: (ArticleModel.Url) -> Unit,
599+
) = ArticlesViewModel(
600+
onBack = onBack,
601+
goToArticle = goToArticle,
602+
getArticles = getArticles::invoke,
603+
refreshArticles = refreshArticles::invoke,
604+
canPullToRefresh = platformInfo.canPullToRefresh,
605+
)
606+
568607
fun chooseWebsitesViewModel(
569608
initialUrl: String?,
570609
onBack: () -> Unit,
@@ -585,13 +624,17 @@ class Dependencies(
585624
goToRunTests: () -> Unit,
586625
goToTests: () -> Unit,
587626
goToTestSettings: () -> Unit,
627+
goToArticles: () -> Unit,
628+
goToArticle: (ArticleModel.Url) -> Unit,
588629
) = DashboardViewModel(
589630
goToOnboarding = goToOnboarding,
590631
goToResults = goToResults,
591632
goToRunningTest = goToRunningTest,
592633
goToRunTests = goToRunTests,
593634
goToTests = goToTests,
594635
goToTestSettings = goToTestSettings,
636+
goToArticles = goToArticles,
637+
goToArticle = goToArticle,
595638
getFirstRun = getFirstRun::invoke,
596639
observeRunBackgroundState = runBackgroundStateManager::observeState,
597640
observeTestRunErrors = runBackgroundStateManager::observeErrors,
@@ -602,6 +645,7 @@ class Dependencies(
602645
getPreference = preferenceRepository::getValueByKey,
603646
setPreference = preferenceRepository::setValueByKey,
604647
getStats = getStats::invoke,
648+
getArticles = getArticles::invoke,
605649
batteryOptimization = batteryOptimization,
606650
)
607651

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package org.ooni.probe.domain.articles
2+
3+
import kotlinx.coroutines.flow.Flow
4+
import org.ooni.probe.data.models.ArticleModel
5+
6+
class GetArticles(
7+
val getArticles: () -> Flow<List<ArticleModel>>,
8+
) {
9+
operator fun invoke() = getArticles()
10+
}

0 commit comments

Comments
 (0)