pull/287/head
Vitor Pamplona 2023-03-15 17:02:49 -04:00
rodzic 59cdd81aec
commit 1812ec296e
13 zmienionych plików z 317 dodań i 3 usunięć

Wyświetl plik

@ -32,6 +32,7 @@ Amethyst brings the best social network to your Android phone. Just insert your
- [x] Online Relay Search (NIP-50) - [x] Online Relay Search (NIP-50)
- [x] Internationalization - [x] Internationalization
- [x] Badges (NIP-58) - [x] Badges (NIP-58)
- [x] Hashtags
- [ ] Local Database - [ ] Local Database
- [ ] View Individual Reactions (Like, Boost, Zaps, Reports) per Post - [ ] View Individual Reactions (Like, Boost, Zaps, Reports) per Post
- [ ] Bookmarks, Pinned Posts, Muted Events (NIP-51) - [ ] Bookmarks, Pinned Posts, Muted Events (NIP-51)

Wyświetl plik

@ -0,0 +1,38 @@
package com.vitorpamplona.amethyst.service
import androidx.compose.ui.text.capitalize
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
object NostrHashtagDataSource : NostrDataSource("SingleHashtagFeed") {
private var hashtagToWatch: String? = null
fun createLoadHashtagFilter(): TypedFilter? {
val hashToLoad = hashtagToWatch ?: return null
return TypedFilter(
types = FeedType.values().toSet(),
filter = JsonFilter(
tags = mapOf("t" to listOf(hashToLoad, hashToLoad.lowercase(), hashToLoad.uppercase(), hashToLoad.capitalize())),
kinds = listOf(TextNoteEvent.kind, ChannelMessageEvent.kind, LongTextNoteEvent.kind),
limit = 200
)
)
}
val loadHashtagChannel = requestNewChannel()
override fun updateChannelFilters() {
loadHashtagChannel.typedFilters = listOfNotNull(createLoadHashtagFilter()).ifEmpty { null }
}
fun loadHashtag(tag: String?) {
hashtagToWatch = tag
invalidateFilters()
}
}

Wyświetl plik

@ -39,8 +39,12 @@ open class Event(
fun taggedUsers() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } fun taggedUsers() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
fun hashtags() = tags.filter { it.firstOrNull() == "t" }.mapNotNull { it.getOrNull(1) }
override fun isTaggedUser(idHex: String) = tags.any { it.getOrNull(0) == "p" && it.getOrNull(1) == idHex } override fun isTaggedUser(idHex: String) = tags.any { it.getOrNull(0) == "p" && it.getOrNull(1) == idHex }
override fun isTaggedHash(hashtag: String) = tags.any { it.getOrNull(0) == "t" && it.getOrNull(1).equals(hashtag, true) }
/** /**
* Checks if the ID is correct and then if the pubKey's secret key signed the event. * Checks if the ID is correct and then if the pubKey's secret key signed the event.
*/ */

Wyświetl plik

@ -24,4 +24,6 @@ interface EventInterface {
fun hasValidSignature(): Boolean fun hasValidSignature(): Boolean
fun isTaggedUser(loggedInUser: String): Boolean fun isTaggedUser(loggedInUser: String): Boolean
fun isTaggedHash(hashtag: String): Boolean
} }

Wyświetl plik

@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.service.model
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.ui.screen.loggedIn.findHashtags
import nostr.postr.Utils import nostr.postr.Utils
import java.util.Date import java.util.Date
@ -29,6 +30,9 @@ class TextNoteEvent(
addresses?.forEach { addresses?.forEach {
tags.add(listOf("a", it.toTag())) tags.add(listOf("a", it.toTag()))
} }
findHashtags(msg).forEach {
tags.add(listOf("t", it))
}
val id = generateId(pubKey, createdAt, kind, tags, msg) val id = generateId(pubKey, createdAt, kind, tags, msg)
val sig = Utils.sign(id, privateKey) val sig = Utils.sign(id, privateKey)
return TextNoteEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey()) return TextNoteEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey())

Wyświetl plik

@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.LocalTextStyle import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
@ -17,6 +18,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
@ -46,8 +48,8 @@ val videoExtension = Pattern.compile("(.*/)*.+\\.(mp4|avi|wmv|mpg|amv|webm|mov)$
val noProtocolUrlValidator = Pattern.compile("^[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b(?:[-a-zA-Z0-9()@:%_\\+.~#?&//=]*)$") val noProtocolUrlValidator = Pattern.compile("^[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b(?:[-a-zA-Z0-9()@:%_\\+.~#?&//=]*)$")
val tagIndex = Pattern.compile(".*\\#\\[([0-9]+)\\].*") val tagIndex = Pattern.compile(".*\\#\\[([0-9]+)\\].*")
val mentionsPattern: Pattern = Pattern.compile("@([A-Za-z0-9_-]+)") val mentionsPattern: Pattern = Pattern.compile("@([A-Za-z0-9_\\-]+)")
val hashTagsPattern: Pattern = Pattern.compile("#([A-Za-z0-9_-]+)") val hashTagsPattern: Pattern = Pattern.compile("#([A-Za-z0-9_\\-]+)")
val urlPattern: Pattern = Patterns.WEB_URL val urlPattern: Pattern = Patterns.WEB_URL
fun isValidURL(url: String?): Boolean { fun isValidURL(url: String?): Boolean {
@ -144,6 +146,8 @@ fun RichTextViewer(
UrlPreview("https://$word", word) UrlPreview("https://$word", word)
} else if (tagIndex.matcher(word).matches() && tags != null) { } else if (tagIndex.matcher(word).matches() && tags != null) {
TagLink(word, tags, canPreview, backgroundColor, accountViewModel, navController) TagLink(word, tags, canPreview, backgroundColor, accountViewModel, navController)
} else if (hashTagsPattern.matcher(word).matches()) {
HashTag(word, accountViewModel, navController)
} else if (isBechLink(word)) { } else if (isBechLink(word)) {
BechLink(word, navController) BechLink(word, navController)
} else { } else {
@ -163,6 +167,8 @@ fun RichTextViewer(
ClickableUrl(word, "https://$word") ClickableUrl(word, "https://$word")
} else if (tagIndex.matcher(word).matches() && tags != null) { } else if (tagIndex.matcher(word).matches() && tags != null) {
TagLink(word, tags, canPreview, backgroundColor, accountViewModel, navController) TagLink(word, tags, canPreview, backgroundColor, accountViewModel, navController)
} else if (hashTagsPattern.matcher(word).matches()) {
HashTag(word, accountViewModel, navController)
} else if (isBechLink(word)) { } else if (isBechLink(word)) {
BechLink(word, navController) BechLink(word, navController)
} else { } else {
@ -212,6 +218,29 @@ fun BechLink(word: String, navController: NavController) {
} }
} }
@Composable
fun HashTag(word: String, accountViewModel: AccountViewModel, navController: NavController) {
val hashtagMatcher = hashTagsPattern.matcher(word)
val tag = try {
hashtagMatcher.find()
hashtagMatcher.group(1)
} catch (e: Exception) {
println("Couldn't link hashtag $word")
null
}
if (tag != null) {
ClickableText(
text = AnnotatedString("#$tag "),
onClick = { navController.navigate("Hashtag/$tag") },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
)
} else {
Text(text = "$word ")
}
}
@Composable @Composable
fun TagLink(word: String, tags: List<List<String>>, canPreview: Boolean, backgroundColor: Color, accountViewModel: AccountViewModel, navController: NavController) { fun TagLink(word: String, tags: List<List<String>>, canPreview: Boolean, backgroundColor: Color, accountViewModel: AccountViewModel, navController: NavController) {
val matcher = tagIndex.matcher(word) val matcher = tagIndex.matcher(word)

Wyświetl plik

@ -0,0 +1,39 @@
package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
object HashtagFeedFilter : FeedFilter<Note>() {
lateinit var account: Account
var tag: String? = null
override fun feed(): List<Note> {
val myTag = tag ?: return emptyList()
return LocalCache.notes.values
.asSequence()
.filter {
(
it.event is TextNoteEvent ||
it.event is LongTextNoteEvent ||
it.event is ChannelMessageEvent ||
it.event is PrivateDmEvent
) &&
it.event?.isTaggedHash(myTag) == true
}
.filter { account.isAcceptable(it) }
.sortedBy { it.createdAt() }
.toList()
.reversed()
}
fun loadHashtag(account: Account, tag: String?) {
this.account = account
this.tag = tag
}
}

Wyświetl plik

@ -20,6 +20,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomListScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomListScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.FiltersScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.FiltersScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.HashtagScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.HomeScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.HomeScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.NotificationScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.NotificationScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ProfileScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.ProfileScreen
@ -94,6 +95,16 @@ fun AppNavigation(
}) })
} }
Route.Hashtag.let { route ->
composable(route.route, route.arguments, content = {
HashtagScreen(
tag = it.arguments?.getString("id"),
accountViewModel = accountViewModel,
navController = navController
)
})
}
Route.Room.let { route -> Route.Room.let { route ->
composable(route.route, route.arguments, content = { composable(route.route, route.arguments, content = {
ChatroomScreen( ChatroomScreen(

Wyświetl plik

@ -47,6 +47,7 @@ import com.vitorpamplona.amethyst.service.NostrChannelDataSource
import com.vitorpamplona.amethyst.service.NostrChatroomDataSource import com.vitorpamplona.amethyst.service.NostrChatroomDataSource
import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource
import com.vitorpamplona.amethyst.service.NostrGlobalDataSource import com.vitorpamplona.amethyst.service.NostrGlobalDataSource
import com.vitorpamplona.amethyst.service.NostrHashtagDataSource
import com.vitorpamplona.amethyst.service.NostrHomeDataSource import com.vitorpamplona.amethyst.service.NostrHomeDataSource
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
import com.vitorpamplona.amethyst.service.NostrSingleChannelDataSource import com.vitorpamplona.amethyst.service.NostrSingleChannelDataSource
@ -138,6 +139,7 @@ fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel)
NostrSingleChannelDataSource.printCounter() NostrSingleChannelDataSource.printCounter()
NostrSingleUserDataSource.printCounter() NostrSingleUserDataSource.printCounter()
NostrThreadDataSource.printCounter() NostrThreadDataSource.printCounter()
NostrHashtagDataSource.printCounter()
NostrUserProfileDataSource.printCounter() NostrUserProfileDataSource.printCounter()

Wyświetl plik

@ -65,6 +65,12 @@ sealed class Route(
arguments = listOf(navArgument("id") { type = NavType.StringType }) arguments = listOf(navArgument("id") { type = NavType.StringType })
) )
object Hashtag : Route(
route = "Hashtag/{id}",
icon = R.drawable.ic_moments,
arguments = listOf(navArgument("id") { type = NavType.StringType })
)
object Room : Route( object Room : Route(
route = "Room/{id}", route = "Room/{id}",
icon = R.drawable.ic_moments, icon = R.drawable.ic_moments,

Wyświetl plik

@ -11,6 +11,7 @@ import com.vitorpamplona.amethyst.ui.dal.ChatroomListKnownFeedFilter
import com.vitorpamplona.amethyst.ui.dal.ChatroomListNewFeedFilter import com.vitorpamplona.amethyst.ui.dal.ChatroomListNewFeedFilter
import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.amethyst.ui.dal.FeedFilter
import com.vitorpamplona.amethyst.ui.dal.GlobalFeedFilter import com.vitorpamplona.amethyst.ui.dal.GlobalFeedFilter
import com.vitorpamplona.amethyst.ui.dal.HashtagFeedFilter
import com.vitorpamplona.amethyst.ui.dal.HomeConversationsFeedFilter import com.vitorpamplona.amethyst.ui.dal.HomeConversationsFeedFilter
import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter
import com.vitorpamplona.amethyst.ui.dal.ThreadFeedFilter import com.vitorpamplona.amethyst.ui.dal.ThreadFeedFilter
@ -33,6 +34,7 @@ class NostrChannelFeedViewModel : FeedViewModel(ChannelFeedFilter)
class NostrChatRoomFeedViewModel : FeedViewModel(ChatroomFeedFilter) class NostrChatRoomFeedViewModel : FeedViewModel(ChatroomFeedFilter)
class NostrGlobalFeedViewModel : FeedViewModel(GlobalFeedFilter) class NostrGlobalFeedViewModel : FeedViewModel(GlobalFeedFilter)
class NostrThreadFeedViewModel : FeedViewModel(ThreadFeedFilter) class NostrThreadFeedViewModel : FeedViewModel(ThreadFeedFilter)
class NostrHashtagFeedViewModel : FeedViewModel(HashtagFeedFilter)
class NostrUserProfileNewThreadsFeedViewModel : FeedViewModel(UserProfileNewThreadFeedFilter) class NostrUserProfileNewThreadsFeedViewModel : FeedViewModel(UserProfileNewThreadFeedFilter)
class NostrUserProfileConversationsFeedViewModel : FeedViewModel(UserProfileConversationsFeedFilter) class NostrUserProfileConversationsFeedViewModel : FeedViewModel(UserProfileConversationsFeedFilter)
class NostrUserProfileReportFeedViewModel : FeedViewModel(UserProfileReportsFeedFilter) class NostrUserProfileReportFeedViewModel : FeedViewModel(UserProfileReportsFeedFilter)

Wyświetl plik

@ -0,0 +1,109 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Divider
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.vitorpamplona.amethyst.service.NostrHashtagDataSource
import com.vitorpamplona.amethyst.ui.dal.HashtagFeedFilter
import com.vitorpamplona.amethyst.ui.screen.FeedView
import com.vitorpamplona.amethyst.ui.screen.NostrHashtagFeedViewModel
@Composable
fun HashtagScreen(tag: String?, accountViewModel: AccountViewModel, navController: NavController) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
val lifeCycleOwner = LocalLifecycleOwner.current
if (tag != null) {
val feedViewModel: NostrHashtagFeedViewModel = viewModel()
LaunchedEffect(tag) {
HashtagFeedFilter.loadHashtag(account, tag)
NostrHashtagDataSource.loadHashtag(tag)
feedViewModel.invalidateData()
}
DisposableEffect(accountViewModel) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
println("Hashtag Start")
HashtagFeedFilter.loadHashtag(account, tag)
NostrHashtagDataSource.loadHashtag(tag)
NostrHashtagDataSource.start()
feedViewModel.invalidateData()
}
if (event == Lifecycle.Event.ON_PAUSE) {
println("Hashtag Stop")
HashtagFeedFilter.loadHashtag(account, null)
NostrHashtagDataSource.loadHashtag(null)
NostrHashtagDataSource.stop()
}
}
lifeCycleOwner.lifecycle.addObserver(observer)
onDispose {
lifeCycleOwner.lifecycle.removeObserver(observer)
}
}
Column(Modifier.fillMaxHeight()) {
Column(
modifier = Modifier.padding(vertical = 0.dp)
) {
HashtagHeader(tag)
FeedView(feedViewModel, accountViewModel, navController, null)
}
}
}
}
@Composable
fun HashtagHeader(tag: String) {
Column() {
Column(modifier = Modifier.padding(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Column(
modifier = Modifier
.padding(start = 10.dp)
.weight(1f)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
Text(
"#$tag",
fontWeight = FontWeight.Bold
)
}
}
}
}
Divider(
modifier = Modifier.padding(start = 12.dp, end = 12.dp),
thickness = 0.25.dp
)
}
}

Wyświetl plik

@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Divider import androidx.compose.material.Divider
@ -72,6 +73,7 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.regex.Pattern
import kotlinx.coroutines.channels.Channel as CoroutineChannel import kotlinx.coroutines.channels.Channel as CoroutineChannel
@Composable @Composable
@ -126,7 +128,9 @@ private fun SearchBar(accountViewModel: AccountViewModel, navController: NavCont
val searchResults = remember { mutableStateOf<List<User>>(emptyList()) } val searchResults = remember { mutableStateOf<List<User>>(emptyList()) }
val searchResultsNotes = remember { mutableStateOf<List<Note>>(emptyList()) } val searchResultsNotes = remember { mutableStateOf<List<Note>>(emptyList()) }
val searchResultsChannels = remember { mutableStateOf<List<Channel>>(emptyList()) } val searchResultsChannels = remember { mutableStateOf<List<Channel>>(emptyList()) }
val hashtagResults = remember { mutableStateOf<List<String>>(emptyList()) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val listState = rememberLazyListState()
val onlineSearch = NostrSearchEventOrUserDataSource val onlineSearch = NostrSearchEventOrUserDataSource
@ -149,6 +153,8 @@ private fun SearchBar(accountViewModel: AccountViewModel, navController: NavCont
.distinctUntilChanged() .distinctUntilChanged()
.debounce(300) .debounce(300)
.collectLatest { .collectLatest {
hashtagResults.value = findHashtags(it)
if (it.removePrefix("npub").removePrefix("note").length >= 4) { if (it.removePrefix("npub").removePrefix("note").length >= 4) {
onlineSearch.search(it.trim()) onlineSearch.search(it.trim())
} }
@ -156,6 +162,9 @@ private fun SearchBar(accountViewModel: AccountViewModel, navController: NavCont
searchResults.value = LocalCache.findUsersStartingWith(it) searchResults.value = LocalCache.findUsersStartingWith(it)
searchResultsNotes.value = LocalCache.findNotesStartingWith(it).sortedBy { it.createdAt() }.reversed() searchResultsNotes.value = LocalCache.findNotesStartingWith(it).sortedBy { it.createdAt() }.reversed()
searchResultsChannels.value = LocalCache.findChannelsStartingWith(it) searchResultsChannels.value = LocalCache.findChannelsStartingWith(it)
// makes sure to show the top of the search
scope.launch(Dispatchers.Main) { listState.animateScrollToItem(0) }
} }
} }
} }
@ -236,8 +245,15 @@ private fun SearchBar(accountViewModel: AccountViewModel, navController: NavCont
contentPadding = PaddingValues( contentPadding = PaddingValues(
top = 10.dp, top = 10.dp,
bottom = 10.dp bottom = 10.dp
) ),
state = listState
) { ) {
itemsIndexed(hashtagResults.value, key = { _, item -> "#" + item }) { _, item ->
HashtagLine(item) {
navController.navigate("Hashtag/$item")
}
}
itemsIndexed(searchResults.value, key = { _, item -> "u" + item.pubkeyHex }) { _, item -> itemsIndexed(searchResults.value, key = { _, item -> "u" + item.pubkeyHex }) { _, item ->
UserCompose(item, accountViewModel = accountViewModel, navController = navController) UserCompose(item, accountViewModel = accountViewModel, navController = navController)
} }
@ -266,6 +282,57 @@ private fun SearchBar(accountViewModel: AccountViewModel, navController: NavCont
} }
} }
val hashtagSearch = Pattern.compile("(?:\\s|\\A)#([A-Za-z0-9_\\-]+)")
fun findHashtags(content: String): List<String> {
val matcher = hashtagSearch.matcher(content)
val returningList = mutableSetOf<String>()
while (matcher.find()) {
try {
val tag = matcher.group(1)
if (tag != null && tag.isNotBlank()) {
returningList.add(tag)
}
} catch (e: Exception) {
}
}
return returningList.toList()
}
@Composable
fun HashtagLine(tag: String, onClick: () -> Unit) {
Column(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
) {
Row(
modifier = Modifier
.padding(
start = 12.dp,
end = 12.dp,
top = 10.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
Text(
"Search hashtag: #$tag",
fontWeight = FontWeight.Bold
)
}
}
Divider(
modifier = Modifier.padding(top = 10.dp),
thickness = 0.25.dp
)
}
}
@Composable @Composable
fun UserLine( fun UserLine(
baseUser: User, baseUser: User,