Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ kotlin {
iosMain.dependencies {
implementation(libs.sqldelight.native)
implementation(libs.bundles.mobile)
implementation(libs.bundles.ios)
}
val desktopMain by getting {
dependencies {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import org.ooni.probe.shared.today
data class ArticleModel(
val url: Url,
val title: String,
val description: String?,
val source: Source,
val description: String?,
val imageUrl: String?,
val time: LocalDateTime,
) {
data class Url(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ class ArticleRepository(
database.articleQueries.insertOrReplace(
url = model.url.value,
title = model.title,
description = model.description,
source = model.source.value,
description = model.description,
image_url = model.imageUrl,
time = model.time.toEpoch(),
)
}
Expand All @@ -45,8 +46,9 @@ class ArticleRepository(
ArticleModel(
url = ArticleModel.Url(url),
title = title,
description = description,
source = ArticleModel.Source.fromValue(source) ?: return@run null,
description = description,
imageUrl = image_url,
time = time.toLocalDateTime(),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class GetFindings(
title = title ?: return@run null,
source = ArticleModel.Source.Finding,
description = shortDescription,
imageUrl = null,
time = createTime?.toLocalDateTime() ?: return@run null,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import kotlinx.datetime.format.byUnicodePattern
import kotlinx.datetime.parse
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import nl.adaptivity.xmlutil.ExperimentalXmlUtilApi
import nl.adaptivity.xmlutil.serialization.UnknownChildHandler
import nl.adaptivity.xmlutil.serialization.XML
import nl.adaptivity.xmlutil.serialization.XmlElement
import nl.adaptivity.xmlutil.serialization.XmlSerialName
Expand Down Expand Up @@ -56,6 +54,7 @@ class GetRSSFeed(
title = title ?: return@run null,
source = source,
description = description,
imageUrl = content?.url,
time = pubDate?.toLocalDateTime() ?: return@run null,
)
}
Expand All @@ -80,8 +79,7 @@ class GetRSSFeed(
private val Xml by lazy {
XML {
defaultPolicy {
@OptIn(ExperimentalXmlUtilApi::class)
unknownChildHandler = UnknownChildHandler { _, _, _, _, _ -> emptyList() }
ignoreUnknownChildren()
}
}
}
Expand Down Expand Up @@ -115,6 +113,16 @@ class GetRSSFeed(
@XmlSerialName("pubDate")
@XmlElement
val pubDate: String?,
@XmlSerialName("content", namespace = "http://search.yahoo.com/mrss/")
@XmlElement
val content: MediaContent?,
)

@Serializable
data class MediaContent(
@XmlSerialName("url")
@XmlElement(false)
val url: String?,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package org.ooni.probe.ui.articles

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.compose.AsyncImagePainter
import coil3.compose.rememberAsyncImagePainter
import kotlinx.datetime.LocalDate
import ooniprobe.composeapp.generated.resources.Dashboard_Articles_Blog
import ooniprobe.composeapp.generated.resources.Dashboard_Articles_Finding
import ooniprobe.composeapp.generated.resources.Dashboard_Articles_Recent
import ooniprobe.composeapp.generated.resources.Dashboard_Articles_Report
import ooniprobe.composeapp.generated.resources.Res
import ooniprobe.composeapp.generated.resources.ic_cloud_off
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
import org.jetbrains.compose.ui.tooling.preview.Preview
import org.ooni.probe.data.models.ArticleModel
import org.ooni.probe.shared.toDateTime
import org.ooni.probe.ui.shared.articleFormat
import org.ooni.probe.ui.theme.AppTheme
import org.ooni.probe.ui.theme.LocalCustomColors

@Composable
fun ArticleCard(
article: ArticleModel,
onClick: () -> Unit,
) {
OutlinedCard(
onClick = onClick,
colors = CardDefaults
.outlinedCardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLowest),
modifier = Modifier
.padding(horizontal = 16.dp)
.padding(bottom = 8.dp),
) {
Row(
Modifier
.height(IntrinsicSize.Min)
.defaultMinSize(minHeight = 88.dp),
) {
Column(Modifier.weight(2f)) {
Text(
article.title,
style = MaterialTheme.typography.titleMedium,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.padding(horizontal = 12.dp)
.padding(top = 8.dp, bottom = 4.dp),
)
Text(
buildAnnotatedString {
withStyle(
SpanStyle(
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold,
),
) {
append(
stringResource(
when (article.source) {
ArticleModel.Source.Blog -> Res.string.Dashboard_Articles_Blog
ArticleModel.Source.Finding -> Res.string.Dashboard_Articles_Finding
ArticleModel.Source.Report -> Res.string.Dashboard_Articles_Report
},
),
)
}
append(" • ")
append(article.time.articleFormat())
if (article.isRecent) {
append(" • ")
withStyle(SpanStyle(color = LocalCustomColors.current.success)) {
append(stringResource(Res.string.Dashboard_Articles_Recent))
}
}
},
style = MaterialTheme.typography.labelLarge,
modifier = Modifier.padding(horizontal = 12.dp).padding(bottom = 8.dp),
)
}
article.imageUrl?.let { imageUrl ->
val painter = rememberAsyncImagePainter(imageUrl)
val state by painter.state.collectAsStateWithLifecycle()
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxHeight()
.height(IntrinsicSize.Min)
.clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.colorScheme.surfaceVariant)
.border(
Dp.Hairline,
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.25f),
MaterialTheme.shapes.medium,
).weight(1f),
) {
Image(
painter = painter,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.matchParentSize(),
)
if (state is AsyncImagePainter.State.Loading) {
CircularProgressIndicator(
Modifier.size(48.dp),
)
}
if (state is AsyncImagePainter.State.Error) {
Image(
painterResource(Res.drawable.ic_cloud_off),
contentDescription = null,
modifier = Modifier.alpha(0.5f),
)
}
}
}
}
}
}

@Composable
@Preview
fun ArticleCellPreview() {
AppTheme {
ArticleCard(
article = ArticleModel(
url = ArticleModel.Url("http://ooni.org"),
title = "Join us at the OMG Village at the Global Gathering 2025!",
description = "Hello there.",
imageUrl = "https://ooni.org/images/logos/[email protected]",
source = ArticleModel.Source.Blog,
time = LocalDate(2025, 4, 1).toDateTime(),
),
onClick = {},
)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ fun ArticlesScreen(
state = lazyListState,
) {
items(state.articles, key = { it.url.value }) { article ->
ArticleCell(
ArticleCard(
article = article,
onClick = { onEvent(ArticlesViewModel.Event.ArticleClicked(article)) },
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ import org.ooni.probe.data.models.MeasurementStats
import org.ooni.probe.data.models.Run
import org.ooni.probe.data.models.RunBackgroundState
import org.ooni.probe.shared.largeNumberShort
import org.ooni.probe.ui.articles.ArticleCell
import org.ooni.probe.ui.articles.ArticleCard
import org.ooni.probe.ui.shared.IgnoreBatteryOptimizationDialog
import org.ooni.probe.ui.shared.TestRunErrorMessages
import org.ooni.probe.ui.shared.VerticalScrollbar
Expand Down Expand Up @@ -486,7 +486,7 @@ private fun ArticlesSection(
)

articles.forEach { article ->
ArticleCell(
ArticleCard(
article = article,
onClick = { onEvent(DashboardViewModel.Event.ArticleClicked(article)) },
)
Expand Down
1 change: 1 addition & 0 deletions composeApp/src/commonMain/sqldelight/migrations/15.sqm
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ CREATE TABLE Article(
title TEXT NOT NULL,
source TEXT NOT NULL,
description TEXT,
image_url TEXT,
time INTEGER NOT NULL
);
Loading
Loading