Skip to content

Commit

Permalink
Fix hashtags matching on note content
Browse files Browse the repository at this point in the history
  • Loading branch information
AleksandarIlic committed Aug 29, 2023
1 parent 46d9163 commit 57919f8
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import net.primal.android.core.compose.feed.model.NostrResourceUi
import net.primal.android.core.compose.media.model.MediaResourceUi
import net.primal.android.core.ext.calculateImageSize
import net.primal.android.core.ext.findNearestOrNull
import net.primal.android.core.utils.HashtagMatcher
import net.primal.android.core.utils.parseHashtags
import net.primal.android.feed.db.ReferencedPost
import net.primal.android.feed.db.ReferencedUser
Expand Down Expand Up @@ -145,53 +146,57 @@ fun FeedPostContent(

refinedUrlResources.map { it.url }.forEach {
val startIndex = refinedContent.indexOf(it)
val endIndex = startIndex + it.length
addStyle(
style = SpanStyle(color = primaryColor),
start = startIndex,
end = endIndex,
)
addStringAnnotation(
tag = URL_ANNOTATION_TAG,
annotation = it,
start = startIndex,
end = endIndex,
)
if (startIndex >= 0) {
val endIndex = startIndex + it.length
addStyle(
style = SpanStyle(color = primaryColor),
start = startIndex,
end = endIndex,
)
addStringAnnotation(
tag = URL_ANNOTATION_TAG,
annotation = it,
start = startIndex,
end = endIndex,
)
}
}

referencedUserResources.forEach {
checkNotNull(it.referencedUser)
val displayHandle = it.referencedUser.displayHandle
val startIndex = refinedContent.indexOf(displayHandle)
val endIndex = startIndex + displayHandle.length
addStyle(
style = SpanStyle(color = primaryColor),
start = startIndex,
end = endIndex,
)
addStringAnnotation(
tag = PROFILE_ID_ANNOTATION_TAG,
annotation = it.referencedUser.userId,
start = startIndex,
end = endIndex,
)
if (startIndex >= 0) {
val endIndex = startIndex + displayHandle.length
addStyle(
style = SpanStyle(color = primaryColor),
start = startIndex,
end = endIndex,
)
addStringAnnotation(
tag = PROFILE_ID_ANNOTATION_TAG,
annotation = it.referencedUser.userId,
start = startIndex,
end = endIndex,
)
}
}

hashtags.forEach {
val startIndex = refinedContent.indexOf(it)
val endIndex = startIndex + it.length
addStyle(
style = SpanStyle(color = primaryColor),
start = startIndex,
end = endIndex,
)
addStringAnnotation(
tag = HASHTAG_ANNOTATION_TAG,
annotation = it,
start = startIndex,
end = endIndex,
)
}
HashtagMatcher(content = refinedContent, hashtags = hashtags)
.matches()
.forEach {
addStyle(
style = SpanStyle(color = primaryColor),
start = it.startIndex,
end = it.endIndex,
)
addStringAnnotation(
tag = HASHTAG_ANNOTATION_TAG,
annotation = it.value,
start = it.startIndex,
end = it.endIndex,
)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package net.primal.android.core.utils

class HashtagMatcher(
content: String,
hashtags: List<String>,
) {

private val matches: MutableList<HashtagMatch> = mutableListOf()
fun matches(): List<HashtagMatch> = matches

private fun containsIndex(index: Int) = matches.find { it.startIndex == index } != null

private fun String.indexOfNotMatchedBefore(hashtag: String): Int? {
var startIndex = 0
var foundIndex: Int? = -1
while (foundIndex == -1) {
val indexOf = this.indexOf(hashtag, startIndex = startIndex)
when {
indexOf == -1 -> foundIndex = null
containsIndex(index = indexOf) -> startIndex = indexOf + 1
else -> foundIndex = indexOf
}
}
return foundIndex
}

init {
hashtags.forEach {
val startIndex = content.indexOfNotMatchedBefore(hashtag = it)
if (startIndex != null) {
matches.add(
HashtagMatch(
value = it,
startIndex = startIndex,
endIndex = startIndex + it.length,
)
)
}
}
}
}

data class HashtagMatch(
val value: String,
val startIndex: Int,
val endIndex: Int,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package net.primal.android.core.utils

import io.kotest.matchers.collections.shouldContain
import io.kotest.matchers.collections.shouldContainAll
import io.kotest.matchers.collections.shouldNotContain
import org.junit.Test

class HashtagMatcherTest {

@Test
fun `matches should match all known hashtags`() {
val hashtags = listOf("#TextureTuesday", "#Texture", "#Details", "#Photography")
val matcher = HashtagMatcher(
content = "Rusty iron chain on gray gravel. #TextureTuesday #Texture #Details #Photography",
hashtags = hashtags,
)

val actual = matcher.matches().map { it.value }
actual.shouldContainAll(hashtags)
}

@Test
fun `matches should match hashtags which are contained in other hashtags`() {
val hashtags = listOf("#TextureTuesday", "#Texture", "#Zapathon", "#Zap")
val matcher = HashtagMatcher(
content = "Rusty iron chain on gray gravel. #TextureTuesday #Texture #Zap #Zapathon",
hashtags = hashtags,
)

val actual = matcher.matches().map { it.value }
actual.shouldContainAll(hashtags)
}

@Test
fun `matches should not match unknown hashtags`() {
val hashtags = listOf("#Hiking", "#Trails")
val matcher = HashtagMatcher(
content = "The #Hiking Trails is available in Art prints with or without frame.",
hashtags = hashtags,
)

val actual = matcher.matches().map { it.value }
actual.shouldContain(hashtags.first())
actual.shouldNotContain(hashtags.last())
}
}

0 comments on commit 57919f8

Please sign in to comment.