Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Image Loader and Implement Naive content templating #109

Open
wants to merge 33 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
5258902
feed data flow
digitalbuddha Dec 15, 2022
c11a9a5
feed data flow
digitalbuddha Dec 15, 2022
d59e1c8
feed data flow
digitalbuddha Dec 15, 2022
4c0bbe5
pr feedback and more data modeling
digitalbuddha Dec 18, 2022
9c5edfb
Update data/network/src/commonMain/kotlin/social/androiddev/common/ne…
digitalbuddha Dec 15, 2022
146f20c
Update data/repository/src/commonMain/kotlin/social/androiddev/common…
digitalbuddha Dec 15, 2022
42c9a89
Update gradle/libs.versions.toml
digitalbuddha Dec 15, 2022
a6bac8e
Update ui/root/src/commonMain/kotlin/social/androiddev/root/navigatio…
digitalbuddha Dec 15, 2022
324fcdd
Update ui/timeline/src/commonMain/kotlin/social/androiddev/timeline/T…
digitalbuddha Dec 15, 2022
1201e83
Update ui/timeline/src/commonMain/kotlin/social/androiddev/timeline/T…
digitalbuddha Dec 15, 2022
2308c25
Update ui/signed-in/src/commonMain/kotlin/social/androiddev/signedin/…
digitalbuddha Dec 15, 2022
f5a4c93
Update ui/timeline/src/commonMain/kotlin/social/androiddev/timeline/T…
digitalbuddha Dec 15, 2022
cb895a5
Update data/network/src/commonMain/kotlin/social/androiddev/common/ne…
digitalbuddha Dec 15, 2022
2c82f3b
split timelineRepoModule into delegates
digitalbuddha Dec 18, 2022
36ce08a
pr comments
digitalbuddha Dec 19, 2022
ee77e1b
start of tests
digitalbuddha Dec 19, 2022
5476f65
spotless
digitalbuddha Dec 20, 2022
70ea9c6
spotless
digitalbuddha Dec 20, 2022
d156b69
image loading + wip parsing
digitalbuddha Dec 20, 2022
5d52624
parse html into annotated strings
digitalbuddha Dec 21, 2022
997c106
Merge branch 'miken/feedData' into miken/feedUI
digitalbuddha Dec 21, 2022
41e597f
rebase
digitalbuddha Dec 21, 2022
f9a53d8
spotless
digitalbuddha Dec 21, 2022
41b5549
add test dependency
digitalbuddha Dec 21, 2022
a865d14
Update data/repository/src/commonTest/kotlin/social/androiddev/common…
digitalbuddha Dec 23, 2022
3c9a5f4
pr comments
digitalbuddha Dec 23, 2022
ad7f6d9
Merge branch 'miken/feedUI' of https://github.com/AndroidDev-social/D…
digitalbuddha Dec 23, 2022
f7e7ed2
Merge branch 'main' into miken/feedUI
digitalbuddha Dec 23, 2022
d419760
merging
digitalbuddha Dec 23, 2022
2e582d5
rebase on detekt changes
digitalbuddha Dec 23, 2022
e96e7b6
detekt fixes
digitalbuddha Dec 23, 2022
2396112
Merge branch 'main' into miken/feedUI
digitalbuddha Jan 16, 2023
47f6962
rebase
aclassen Jan 16, 2023
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
5 changes: 5 additions & 0 deletions app-android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ plugins {

android {
namespace = "social.androiddev.dodo"
packagingOptions {
exclude ("META-INF/DEPENDENCIES")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've always wondering what this stuff is for? Is this generated or you had to add manually to fix a build issue?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Manually to fix a build issue. My naive understanding is that multiple libraries have the same metadata file name and clash

exclude ("META-INF/NOTICE")
exclude ("mozilla/public-suffix-list.txt")

}
defaultConfig {
versionCode = 1
versionName = "1.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ import kotlin.test.assertTrue
import kotlin.test.fail

class RealHomeTimelineRepositoryTest {
@Test
fun sucessTest(): TestResult {
@Test fun successTest(): TestResult {
return runTest {
val testRepo = RealHomeTimelineRepository(fakeSuccessStore)
val result = testRepo.read(FeedType.Home).first()
Expand All @@ -41,8 +40,7 @@ class RealHomeTimelineRepositoryTest {
}
}

@Test
fun failureTest(): TestResult {
@Test fun failureTest(): TestResult {
return runTest {
val testRepo = RealHomeTimelineRepository(fakeFailureStore)
val result = testRepo.read(FeedType.Home).first()
Expand All @@ -51,7 +49,6 @@ class RealHomeTimelineRepositoryTest {
}
}
}

val fakeSuccessStore = object : Store<FeedType, List<StatusLocal>> {
override suspend fun clear(key: FeedType) {
TODO("Not yet implemented")
Expand Down
6 changes: 5 additions & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,8 @@ android.suppressUnsupportedCompileSdk=33
kotlin.mpp.stability.nowarn=true

# suppress warning about not building iOS targets on linux
kotlin.native.ignoreDisabledTargets=true
kotlin.native.ignoreDisabledTargets=true

# to get around OOM on github actions
org.gradle.daemon=false

21 changes: 20 additions & 1 deletion ui/common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ plugins {

android {
namespace = "social.androiddev.common"
packagingOptions {
exclude("META-INF/DEPENDENCIES")
}
}

kotlin {
Expand All @@ -15,7 +18,23 @@ kotlin {
implementation(projects.logging)
api(libs.com.arkivanov.decompose)
api(libs.com.arkivanov.decompose.extensions.compose.jetbrains)
api(libs.kotlinx.collections.immutable)
implementation(libs.kotlinx.collections.immutable)
implementation("com.alialbaali.kamel:kamel-image:0.4.0")
implementation("it.skrape:skrapeit:1.2.2")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this also JVM only? Maybe we can use this as a resource and roll our own purely kotlin parser which also supports iOS

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup yup jvm only. I think this gets us off the ground and agree we would need something more robust in future


}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
implementation(libs.org.jetbrains.kotlin.test.common)
implementation(libs.org.jetbrains.kotlin.test.annotations.common)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formatting here looks off, but it could be because I'm on my phone

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ran spotless apply just in case

}
}
val androidTest by getting {
dependencies {
implementation(kotlin("test"))
implementation(libs.org.jetbrains.kotlin.test.junit)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,25 @@
*/
package social.androiddev.common.composables

import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import social.androiddev.common.theme.DodoTheme
import social.androiddev.common.utils.AsyncImage
import social.androiddev.common.utils.loadImageIntoPainter

@Composable
fun UserAvatar(
url: String,
modifier: Modifier = Modifier
) {
AsyncImage(
load = { loadImageIntoPainter(url = url) },
painterFor = { remember { it } },
url = url,
contentDescription = "User avatar",
modifier = modifier.width(48.dp).clip(RoundedCornerShape(5.dp))
modifier = modifier.width(48.dp).height(48.dp).clip(RoundedCornerShape(5.dp))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

modifier.size(48.dp) should already set it for both dimensions

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for some reason it was not rendering the avatars after switching to the new library. We can play with it later but yeah it never assumed a height

)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,59 +12,29 @@
*/
package social.androiddev.common.utils

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Spacer
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import social.androiddev.common.logging.DodoLogger
import social.androiddev.common.network.util.runCatchingIgnoreCancelled
import io.kamel.image.KamelImage
import io.kamel.image.lazyPainterResource

/**
* Use this helper until we switch to a image loading library which supports multiplatform
*/
@Composable
fun <T> AsyncImage(
fun AsyncImage(
contentDescription: String,
load: suspend () -> T,
painterFor: @Composable (T) -> Painter,
url: Any,
modifier: Modifier = Modifier,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit,
contentScale: ContentScale = ContentScale.Fit
) {
val image: T? by produceState<T?>(null) {
value = withContext(Dispatchers.IO) {
runCatchingIgnoreCancelled {
load()
}.fold(
onSuccess = { it },
onFailure = { t ->
DodoLogger.w(throwable = t) {
"Error when loading image."
}
null
}
)
}
}

if (image != null) {
Image(
painter = painterFor(image!!),
contentDescription = contentDescription,
contentScale = contentScale,
modifier = modifier,
alignment = alignment,
)
} else {
Spacer(
modifier = modifier
)
}
KamelImage(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this truly KMP support or just JVM?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just jvm, but ios has native support afaik

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, we should follow-up on this and change this to an expect/actual

resource = lazyPainterResource(data = url),
contentDescription = contentDescription,
modifier = modifier,
alignment = alignment,
contentScale = contentScale
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* This file is part of Dodo.
*
* Dodo is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* Dodo is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Dodo.
* If not, see <https://www.gnu.org/licenses/>.
*/
package social.androiddev.common.utils

import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import it.skrape.core.htmlDocument
import it.skrape.selects.eachLink
import it.skrape.selects.eachText
import it.skrape.selects.html5.a
import it.skrape.selects.html5.body

/**
* Takes a String reciever of html text
* converts it to an annotated string of text and links
*/
@Suppress("ReturnCount")
fun String.extractContentFromMicroFormat(): AnnotatedString {
val newlineReplace = this.replace("<br>", "\n")

if (newlineReplace.contains("<p>").not()) return buildAnnotatedString { }

val content = htmlDocument(newlineReplace)

val body = content.body {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point, the only thing we are using skrape for is to get the paragraph body without any other tags and then for getting the list of links.

We can hand roll that logic ourselves and target ios

findAll {
eachText
}
}
val paragraph = body

@Suppress("SwallowedException")
val links = try {
content.a { findAll { eachLink } }
} catch (exception: Exception) {
emptyMap()
}

val plainText = paragraph.joinToString("\n")
if (links.isNullOrEmpty()) {
buildAnnotatedString {
append(plainText)
}
}
return buildAnnotatedString {
// everything before first link
append(plainText.substringBefore(links.keys.first()))

withStyle(style = SpanStyle(color = Color.Blue, fontWeight = FontWeight.Bold)) {
appendLink(links.keys.first(), links.values.first())
}
val restOfLinks = links.entries.drop(1)
var restOfPlainText = plainText.substringAfter(links.keys.first())
restOfLinks.forEach {
val (key, value) = it
val segment = restOfPlainText.substringBefore(key)
append(segment)
withStyle(style = SpanStyle(color = Color.Blue, fontWeight = FontWeight.Bold)) {
appendLink(key, value)
}
restOfPlainText = restOfPlainText.substringAfter(key)
}
append(restOfPlainText)
}
}

private fun AnnotatedString.Builder.appendLink(linkText: String, linkUrl: String) {
pushStringAnnotation(tag = linkUrl, annotation = linkUrl)
append(linkText)
pop()
}

// private fun AnnotatedString.onLinkClick(offset: Int, onClick: (String) -> Unit) {
// getStringAnnotations(start = offset, end = offset).firstOrNull()?.let {
// onClick(it.item)
// }
// }
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* This file is part of Dodo.
*
* Dodo is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* Dodo is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Dodo.
* If not, see <https://www.gnu.org/licenses/>.
*/
package social.androiddev.common.utils
import org.junit.Test
import kotlin.test.assertTrue

class HtmlParserKtTest {

@Test
fun renderHtml() {
val result = """
<p>Been debating for a few days, and I think its time to open the <a href="https://eggy.app/tags/relay" class="mention hashtag" rel="nofollow noopener noreferrer" target="_blank">#<span>relay</span></a> to anyone. If you're looking for a general Relay please feel free to add <a href="https://relay.eggy.app/inbox" rel="nofollow noopener noreferrer" target="_blank"><span class="invisible">https://</span><span class="">relay.eggy.app/inbox</span><span class="invisible"></span></a> to your instances. More information can be found <a href="https://relay.eggy.app" rel="nofollow noopener noreferrer" target="_blank"><span class="invisible">https://</span><span class="">relay.eggy.app</span><span class="invisible"></span></a>, but Its a general open relay for anybody to use. <a href="https://eggy.app/tags/mastoadmin" class="mention hashtag" rel="nofollow noopener noreferrer" target="_blank">#<span>mastoadmin</span></a> Only thing I'm missing is a metrics tracking type page.. Hoping to have something soon.</p>
""".extractContentFromMicroFormat()

assertTrue { result.spanStyles.size == 4 }
assertTrue { result.paragraphStyles.isEmpty() }
assertTrue { result.length == 393 }
}
}
4 changes: 4 additions & 0 deletions ui/signed-in/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ kotlin {
implementation(projects.data.repository)
implementation(projects.domain.timeline)
implementation(libs.io.insert.koin.core)
implementation(projects.data.persistence)
implementation(projects.data.repository)
implementation(projects.domain.timeline)
implementation(libs.kotlinx.collections.immutable)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
Expand All @@ -41,7 +41,6 @@ import social.androiddev.common.composables.buttons.DodoButton
import social.androiddev.common.theme.Blue
import social.androiddev.common.theme.DodoTheme
import social.androiddev.common.utils.AsyncImage
import social.androiddev.common.utils.loadImageIntoPainter

/**
* Landing view that delegates business logic to [LandingContent]
Expand All @@ -52,12 +51,12 @@ fun LandingContent(
modifier: Modifier = Modifier,
appIcon: @Composable () -> Unit = {
AsyncImage(
load = { loadImageIntoPainter(url = "https://via.placeholder.com/200x200/6FA4DE/010041?text=Dodo") },
painterFor = { remember { it } },
url = "https://via.placeholder.com/200x200/6FA4DE/010041?text=Dodo",
contentDescription = "App Logo",
modifier = Modifier
.padding(horizontal = 48.dp)
.size(240.dp)
.width(240.dp)
.height(240.dp)
.clip(CircleShape),
contentScale = ContentScale.Crop,
)
Expand All @@ -76,8 +75,7 @@ fun LandingContent(
modifier: Modifier = Modifier,
appIcon: @Composable () -> Unit = {
AsyncImage(
load = { loadImageIntoPainter(url = "https://via.placeholder.com/200x200/6FA4DE/010041?text=Dodo") },
painterFor = { remember { it } },
url = "https://via.placeholder.com/200x200/6FA4DE/010041?text=Dodo",
contentDescription = "App Logo",
modifier = Modifier
.padding(horizontal = 48.dp)
Expand Down
1 change: 1 addition & 0 deletions ui/timeline/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ kotlin {
implementation(projects.domain.timeline)
implementation(projects.ui.common)
implementation(libs.io.insert.koin.core)
implementation(libs.kotlinx.collections.immutable)
}
}

Expand Down
Loading