Allows following of Geohashes

This commit is contained in:
Vitor Pamplona
2023-07-25 18:45:28 -04:00
parent 1098c31787
commit 5ba091de0d
27 changed files with 601 additions and 25 deletions
@@ -17,6 +17,7 @@ import com.vitorpamplona.amethyst.service.NostrChatroomDataSource
import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource
import com.vitorpamplona.amethyst.service.NostrCommunityDataSource import com.vitorpamplona.amethyst.service.NostrCommunityDataSource
import com.vitorpamplona.amethyst.service.NostrDiscoveryDataSource import com.vitorpamplona.amethyst.service.NostrDiscoveryDataSource
import com.vitorpamplona.amethyst.service.NostrGeohashDataSource
import com.vitorpamplona.amethyst.service.NostrHashtagDataSource 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
@@ -97,6 +98,7 @@ object ServiceManager {
NostrCommunityDataSource.stop() NostrCommunityDataSource.stop()
NostrHashtagDataSource.stop() NostrHashtagDataSource.stop()
NostrGeohashDataSource.stop()
NostrSearchEventOrUserDataSource.stop() NostrSearchEventOrUserDataSource.stop()
NostrSingleChannelDataSource.stop() NostrSingleChannelDataSource.stop()
NostrSingleEventDataSource.stop() NostrSingleEventDataSource.stop()
@@ -174,6 +174,7 @@ class Account(
val event = ContactListEvent.createFromScratch( val event = ContactListEvent.createFromScratch(
followUsers = listOf(), followUsers = listOf(),
followTags = listOf(), followTags = listOf(),
followGeohashes = listOf(),
followCommunities = listOf(), followCommunities = listOf(),
followEvents = DefaultChannels.toList(), followEvents = DefaultChannels.toList(),
relayUse = relays, relayUse = relays,
@@ -453,6 +454,7 @@ class Account(
ContactListEvent.createFromScratch( ContactListEvent.createFromScratch(
followUsers = listOf(Contact(user.pubkeyHex, null)), followUsers = listOf(Contact(user.pubkeyHex, null)),
followTags = emptyList(), followTags = emptyList(),
followGeohashes = emptyList(),
followCommunities = emptyList(), followCommunities = emptyList(),
followEvents = DefaultChannels.toList(), followEvents = DefaultChannels.toList(),
relayUse = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) }, relayUse = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) },
@@ -475,6 +477,7 @@ class Account(
ContactListEvent.createFromScratch( ContactListEvent.createFromScratch(
followUsers = emptyList(), followUsers = emptyList(),
followTags = emptyList(), followTags = emptyList(),
followGeohashes = emptyList(),
followCommunities = emptyList(), followCommunities = emptyList(),
followEvents = DefaultChannels.toList().plus(channel.idHex), followEvents = DefaultChannels.toList().plus(channel.idHex),
relayUse = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) }, relayUse = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) },
@@ -498,6 +501,7 @@ class Account(
ContactListEvent.createFromScratch( ContactListEvent.createFromScratch(
followUsers = emptyList(), followUsers = emptyList(),
followTags = emptyList(), followTags = emptyList(),
followGeohashes = emptyList(),
followCommunities = listOf(community.address), followCommunities = listOf(community.address),
followEvents = DefaultChannels.toList(), followEvents = DefaultChannels.toList(),
relayUse = relays, relayUse = relays,
@@ -509,7 +513,7 @@ class Account(
LocalCache.consume(event) LocalCache.consume(event)
} }
fun follow(tag: String) { fun followHashtag(tag: String) {
if (!isWriteable()) return if (!isWriteable()) return
val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList) val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList)
@@ -524,6 +528,34 @@ class Account(
ContactListEvent.createFromScratch( ContactListEvent.createFromScratch(
followUsers = emptyList(), followUsers = emptyList(),
followTags = listOf(tag), followTags = listOf(tag),
followGeohashes = emptyList(),
followCommunities = emptyList(),
followEvents = DefaultChannels.toList(),
relayUse = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) },
privateKey = keyPair.privKey!!
)
}
Client.send(event)
LocalCache.consume(event)
}
fun followGeohash(geohash: String) {
if (!isWriteable()) return
val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList)
val event = if (contactList != null) {
ContactListEvent.followGeohash(
contactList,
geohash,
keyPair.privKey!!
)
} else {
ContactListEvent.createFromScratch(
followUsers = emptyList(),
followTags = emptyList(),
followGeohashes = listOf(geohash),
followCommunities = emptyList(), followCommunities = emptyList(),
followEvents = DefaultChannels.toList(), followEvents = DefaultChannels.toList(),
relayUse = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) }, relayUse = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) },
@@ -552,7 +584,7 @@ class Account(
} }
} }
fun unfollow(tag: String) { fun unfollowHashtag(tag: String) {
if (!isWriteable()) return if (!isWriteable()) return
val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList) val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList)
@@ -569,6 +601,23 @@ class Account(
} }
} }
fun unfollowGeohash(geohash: String) {
if (!isWriteable()) return
val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList)
if (contactList != null && contactList.tags.isNotEmpty()) {
val event = ContactListEvent.unfollowGeohash(
contactList,
geohash,
keyPair.privKey!!
)
Client.send(event)
LocalCache.consume(event)
}
}
fun unfollow(channel: Channel) { fun unfollow(channel: Channel) {
if (!isWriteable()) return if (!isWriteable()) return
@@ -1222,6 +1271,30 @@ class Account(
} }
} }
fun selectedGeohashesFollowList(listName: String?): Set<String>? {
if (listName == GLOBAL_FOLLOWS) return null
if (listName == KIND3_FOLLOWS) return userProfile().cachedFollowingGeohashSet()
val privKey = keyPair.privKey
return if (listName != null) {
val aTag = ATag(PeopleListEvent.kind, userProfile().pubkeyHex, listName, null).toTag()
val list = LocalCache.addressables[aTag]
if (list != null) {
val publicAddresses = list.event?.geohashes() ?: emptySet()
val privateAddresses = privKey?.let {
(list.event as? GeneralListEvent)?.privateGeohashes(it)
} ?: emptySet()
(publicAddresses + privateAddresses).toSet()
} else {
emptySet()
}
} else {
emptySet()
}
}
fun selectedCommunitiesFollowList(listName: String?): Set<String>? { fun selectedCommunitiesFollowList(listName: String?): Set<String>? {
if (listName == GLOBAL_FOLLOWS) return null if (listName == GLOBAL_FOLLOWS) return null
if (listName == KIND3_FOLLOWS) return userProfile().cachedFollowingCommunitiesSet() if (listName == KIND3_FOLLOWS) return userProfile().cachedFollowingCommunitiesSet()
@@ -249,6 +249,12 @@ class User(val pubkeyHex: String) {
} ?: false } ?: false
} }
fun isFollowingGeohashCached(geoTag: String): Boolean {
return latestContactList?.verifiedFollowGeohashSet?.let {
return geoTag.lowercase() in it
} ?: false
}
fun isFollowingCached(user: User): Boolean { fun isFollowingCached(user: User): Boolean {
return latestContactList?.verifiedFollowKeySet?.let { return latestContactList?.verifiedFollowKeySet?.let {
return user.pubkeyHex in it return user.pubkeyHex in it
@@ -277,6 +283,10 @@ class User(val pubkeyHex: String) {
return latestContactList?.verifiedFollowTagSet ?: emptySet() return latestContactList?.verifiedFollowTagSet ?: emptySet()
} }
fun cachedFollowingGeohashSet(): Set<HexKey> {
return latestContactList?.verifiedFollowGeohashSet ?: emptySet()
}
fun cachedFollowingCommunitiesSet(): Set<HexKey> { fun cachedFollowingCommunitiesSet(): Set<HexKey> {
return latestContactList?.verifiedFollowCommunitySet ?: emptySet() return latestContactList?.verifiedFollowCommunitySet ?: emptySet()
} }
@@ -92,6 +92,26 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
) )
} }
fun createLiveStreamGeohashesFilter(): TypedFilter? {
val hashToLoad = account.selectedGeohashesFollowList(account.defaultDiscoveryFollowList)
if (hashToLoad.isNullOrEmpty()) return null
return TypedFilter(
types = setOf(FeedType.GLOBAL),
filter = JsonFilter(
kinds = listOf(LiveActivitiesChatMessageEvent.kind, LiveActivitiesEvent.kind),
tags = mapOf(
"g" to hashToLoad.map {
listOf(it, it.lowercase(), it.uppercase(), it.capitalize())
}.flatten()
),
limit = 300,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
)
)
}
fun createPublicChatsTagsFilter(): TypedFilter? { fun createPublicChatsTagsFilter(): TypedFilter? {
val hashToLoad = account.selectedTagsFollowList(account.defaultDiscoveryFollowList) val hashToLoad = account.selectedTagsFollowList(account.defaultDiscoveryFollowList)
@@ -112,6 +132,26 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
) )
} }
fun createPublicChatsGeohashesFilter(): TypedFilter? {
val hashToLoad = account.selectedGeohashesFollowList(account.defaultDiscoveryFollowList)
if (hashToLoad.isNullOrEmpty()) return null
return TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS),
filter = JsonFilter(
kinds = listOf(ChannelCreateEvent.kind, ChannelMetadataEvent.kind, ChannelMessageEvent.kind),
tags = mapOf(
"g" to hashToLoad.map {
listOf(it, it.lowercase(), it.uppercase(), it.capitalize())
}.flatten()
),
limit = 300,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
)
)
}
fun createCommunitiesTagsFilter(): TypedFilter? { fun createCommunitiesTagsFilter(): TypedFilter? {
val hashToLoad = account.selectedTagsFollowList(account.defaultDiscoveryFollowList) val hashToLoad = account.selectedTagsFollowList(account.defaultDiscoveryFollowList)
@@ -132,6 +172,26 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
) )
} }
fun createCommunitiesGeohashesFilter(): TypedFilter? {
val hashToLoad = account.selectedGeohashesFollowList(account.defaultDiscoveryFollowList)
if (hashToLoad.isNullOrEmpty()) return null
return TypedFilter(
types = setOf(FeedType.GLOBAL),
filter = JsonFilter(
kinds = listOf(CommunityDefinitionEvent.kind, CommunityPostApprovalEvent.kind),
tags = mapOf(
"g" to hashToLoad.map {
listOf(it, it.lowercase(), it.uppercase(), it.capitalize())
}.flatten()
),
limit = 300,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
)
)
}
val discoveryFeedChannel = requestNewChannel() { time, relayUrl -> val discoveryFeedChannel = requestNewChannel() { time, relayUrl ->
latestEOSEs.addOrUpdate(account.userProfile(), account.defaultDiscoveryFollowList, relayUrl, time) latestEOSEs.addOrUpdate(account.userProfile(), account.defaultDiscoveryFollowList, relayUrl, time)
} }
@@ -143,7 +203,10 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
createCommunitiesFilter(), createCommunitiesFilter(),
createLiveStreamTagsFilter(), createLiveStreamTagsFilter(),
createPublicChatsTagsFilter(), createPublicChatsTagsFilter(),
createCommunitiesTagsFilter() createCommunitiesTagsFilter(),
createCommunitiesGeohashesFilter(),
createPublicChatsGeohashesFilter(),
createLiveStreamGeohashesFilter()
).ifEmpty { null } ).ifEmpty { null }
} }
} }
@@ -0,0 +1,42 @@
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
import com.vitorpamplona.amethyst.service.model.PollNoteEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
object NostrGeohashDataSource : NostrDataSource("SingleGeoHashFeed") {
private var geohashToWatch: String? = null
fun createLoadHashtagFilter(): TypedFilter? {
val hashToLoad = geohashToWatch ?: return null
return TypedFilter(
types = COMMON_FEED_TYPES,
filter = JsonFilter(
tags = mapOf(
"g" to listOf(
hashToLoad
)
),
kinds = listOf(TextNoteEvent.kind, ChannelMessageEvent.kind, LongTextNoteEvent.kind, LongTextNoteEvent.kind, PollNoteEvent.kind),
limit = 200
)
)
}
val loadGeohashChannel = requestNewChannel()
override fun updateChannelFilters() {
loadGeohashChannel.typedFilters = listOfNotNull(createLoadHashtagFilter()).ifEmpty { null }
}
fun loadHashtag(tag: String?) {
geohashToWatch = tag
invalidateFilters()
}
}
@@ -104,6 +104,26 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
) )
} }
fun createFollowGeohashesFilter(): TypedFilter? {
val hashToLoad = account.selectedGeohashesFollowList(account.defaultHomeFollowList) ?: emptySet()
if (hashToLoad.isEmpty()) return null
return TypedFilter(
types = setOf(FeedType.FOLLOWS),
filter = JsonFilter(
kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, ClassifiedsEvent.kind, HighlightEvent.kind, AudioTrackEvent.kind, PinListEvent.kind),
tags = mapOf(
"g" to hashToLoad.map {
listOf(it, it.lowercase(), it.uppercase(), it.capitalize())
}.flatten()
),
limit = 100,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultHomeFollowList)?.relayList
)
)
}
fun createFollowCommunitiesFilter(): TypedFilter? { fun createFollowCommunitiesFilter(): TypedFilter? {
val communitiesToLoad = account.selectedCommunitiesFollowList(account.defaultHomeFollowList) ?: emptySet() val communitiesToLoad = account.selectedCommunitiesFollowList(account.defaultHomeFollowList) ?: emptySet()
@@ -135,6 +155,11 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
} }
override fun updateChannelFilters() { override fun updateChannelFilters() {
followAccountChannel.typedFilters = listOfNotNull(createFollowAccountsFilter(), createFollowCommunitiesFilter(), createFollowTagsFilter()).ifEmpty { null } followAccountChannel.typedFilters = listOfNotNull(
createFollowAccountsFilter(),
createFollowCommunitiesFilter(),
createFollowTagsFilter(),
createFollowGeohashesFilter()
).ifEmpty { null }
} }
} }
@@ -51,11 +51,35 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") {
) )
} }
fun createFollowGeohashesFilter(): TypedFilter? {
val hashToLoad = account.selectedGeohashesFollowList(account.defaultStoriesFollowList)
if (hashToLoad.isNullOrEmpty()) return null
return TypedFilter(
types = setOf(FeedType.GLOBAL),
filter = JsonFilter(
kinds = listOf(FileHeaderEvent.kind, FileStorageHeaderEvent.kind),
tags = mapOf(
"g" to hashToLoad.map {
listOf(it, it.lowercase(), it.uppercase(), it.capitalize())
}.flatten()
),
limit = 100,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultStoriesFollowList)?.relayList
)
)
}
val videoFeedChannel = requestNewChannel() { time, relayUrl -> val videoFeedChannel = requestNewChannel() { time, relayUrl ->
latestEOSEs.addOrUpdate(account.userProfile(), account.defaultStoriesFollowList, relayUrl, time) latestEOSEs.addOrUpdate(account.userProfile(), account.defaultStoriesFollowList, relayUrl, time)
} }
override fun updateChannelFilters() { override fun updateChannelFilters() {
videoFeedChannel.typedFilters = listOfNotNull(createContextualFilter(), createFollowTagsFilter()).ifEmpty { null } videoFeedChannel.typedFilters = listOfNotNull(
createContextualFilter(),
createFollowTagsFilter(),
createFollowGeohashesFilter()
).ifEmpty { null }
} }
} }
@@ -39,6 +39,10 @@ class ContactListEvent(
unverifiedFollowTagSet().map { it.lowercase() }.toSet() unverifiedFollowTagSet().map { it.lowercase() }.toSet()
} }
val verifiedFollowGeohashSet: Set<String> by lazy {
unverifiedFollowGeohashSet().map { it.lowercase() }.toSet()
}
val verifiedFollowCommunitySet: Set<String> by lazy { val verifiedFollowCommunitySet: Set<String> by lazy {
unverifiedFollowAddressSet().toSet() unverifiedFollowAddressSet().toSet()
} }
@@ -49,6 +53,7 @@ class ContactListEvent(
fun unverifiedFollowKeySet() = tags.filter { it[0] == "p" }.mapNotNull { it.getOrNull(1) } fun unverifiedFollowKeySet() = tags.filter { it[0] == "p" }.mapNotNull { it.getOrNull(1) }
fun unverifiedFollowTagSet() = tags.filter { it[0] == "t" }.mapNotNull { it.getOrNull(1) } fun unverifiedFollowTagSet() = tags.filter { it[0] == "t" }.mapNotNull { it.getOrNull(1) }
fun unverifiedFollowGeohashSet() = tags.filter { it[0] == "g" }.mapNotNull { it.getOrNull(1) }
fun unverifiedFollowAddressSet() = tags.filter { it[0] == "a" }.mapNotNull { it.getOrNull(1) } fun unverifiedFollowAddressSet() = tags.filter { it[0] == "a" }.mapNotNull { it.getOrNull(1) }
@@ -82,6 +87,7 @@ class ContactListEvent(
fun createFromScratch( fun createFromScratch(
followUsers: List<Contact>, followUsers: List<Contact>,
followTags: List<String>, followTags: List<String>,
followGeohashes: List<String>,
followCommunities: List<ATag>, followCommunities: List<ATag>,
followEvents: List<String>, followEvents: List<String>,
relayUse: Map<String, ReadWrite>?, relayUse: Map<String, ReadWrite>?,
@@ -110,6 +116,8 @@ class ContactListEvent(
} else { } else {
listOf("a", it.toTag()) listOf("a", it.toTag())
} }
} + followGeohashes.map {
listOf("g", it)
} }
return create( return create(
@@ -164,6 +172,28 @@ class ContactListEvent(
) )
} }
fun followGeohash(earlierVersion: ContactListEvent, hashtag: String, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent {
if (earlierVersion.isTaggedGeoHash(hashtag)) return earlierVersion
return create(
content = earlierVersion.content,
tags = earlierVersion.tags.plus(element = listOf("g", hashtag)),
privateKey = privateKey,
createdAt = createdAt
)
}
fun unfollowGeohash(earlierVersion: ContactListEvent, hashtag: String, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent {
if (!earlierVersion.isTaggedGeoHash(hashtag)) return earlierVersion
return create(
content = earlierVersion.content,
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != hashtag },
privateKey = privateKey,
createdAt = createdAt
)
}
fun followEvent(earlierVersion: ContactListEvent, idHex: String, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent { fun followEvent(earlierVersion: ContactListEvent, idHex: String, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent {
if (earlierVersion.isTaggedEvent(idHex)) return earlierVersion if (earlierVersion.isTaggedEvent(idHex)) return earlierVersion
@@ -69,6 +69,7 @@ open class Event(
} }
override fun hashtags() = tags.filter { it.size > 1 && it[0] == "t" }.map { it[1] } override fun hashtags() = tags.filter { it.size > 1 && it[0] == "t" }.map { it[1] }
override fun geohashes() = tags.filter { it.size > 1 && it[0] == "g" }.map { it[1] }
override fun matchTag1With(text: String) = tags.any { it.size > 1 && it[1].contains(text, true) } override fun matchTag1With(text: String) = tags.any { it.size > 1 && it[1].contains(text, true) }
@@ -81,7 +82,10 @@ open class Event(
override fun isTaggedAddressableNotes(idHexes: Set<String>) = tags.any { it.size > 1 && it[0] == "a" && it[1] in idHexes } override fun isTaggedAddressableNotes(idHexes: Set<String>) = tags.any { it.size > 1 && it[0] == "a" && it[1] in idHexes }
override fun isTaggedHash(hashtag: String) = tags.any { it.size > 1 && it[0] == "t" && it[1].equals(hashtag, true) } override fun isTaggedHash(hashtag: String) = tags.any { it.size > 1 && it[0] == "t" && it[1].equals(hashtag, true) }
override fun isTaggedGeoHash(hashtag: String) = tags.any { it.size > 1 && it[0] == "g" && it[1].startsWith(hashtag, true) }
override fun isTaggedHashes(hashtags: Set<String>) = tags.any { it.size > 1 && it[0] == "t" && it[1].lowercase() in hashtags } override fun isTaggedHashes(hashtags: Set<String>) = tags.any { it.size > 1 && it[0] == "t" && it[1].lowercase() in hashtags }
override fun isTaggedGeoHashes(hashtags: Set<String>) = tags.any { it.size > 1 && it[0] == "g" && it[1].lowercase() in hashtags }
override fun firstIsTaggedHashes(hashtags: Set<String>) = tags.firstOrNull { it.size > 1 && it[0] == "t" && it[1].lowercase() in hashtags }?.getOrNull(1) override fun firstIsTaggedHashes(hashtags: Set<String>) = tags.firstOrNull { it.size > 1 && it[0] == "t" && it[1].lowercase() in hashtags }?.getOrNull(1)
override fun firstIsTaggedAddressableNote(addressableNotes: Set<String>) = tags.firstOrNull { it.size > 1 && it[0] == "a" && it[1] in addressableNotes }?.getOrNull(1) override fun firstIsTaggedAddressableNote(addressableNotes: Set<String>) = tags.firstOrNull { it.size > 1 && it[0] == "a" && it[1] in addressableNotes }?.getOrNull(1)
@@ -34,7 +34,11 @@ interface EventInterface {
fun isTaggedAddressableNotes(idHexes: Set<String>): Boolean fun isTaggedAddressableNotes(idHexes: Set<String>): Boolean
fun isTaggedHash(hashtag: String): Boolean fun isTaggedHash(hashtag: String): Boolean
fun isTaggedGeoHash(hashtag: String): Boolean
fun isTaggedHashes(hashtags: Set<String>): Boolean fun isTaggedHashes(hashtags: Set<String>): Boolean
fun isTaggedGeoHashes(hashtags: Set<String>): Boolean
fun firstIsTaggedHashes(hashtags: Set<String>): String? fun firstIsTaggedHashes(hashtags: Set<String>): String?
fun firstIsTaggedAddressableNote(addressableNotes: Set<String>): String? fun firstIsTaggedAddressableNote(addressableNotes: Set<String>): String?
@@ -42,6 +46,7 @@ interface EventInterface {
fun getTagOfAddressableKind(kind: Int): ATag? fun getTagOfAddressableKind(kind: Int): ATag?
fun hashtags(): List<String> fun hashtags(): List<String>
fun geohashes(): List<String>
fun getReward(): BigDecimal? fun getReward(): BigDecimal?
fun getPoWRank(): Int fun getPoWRank(): Int
@@ -61,6 +61,7 @@ abstract class GeneralListEvent(
fun privateTaggedUsers(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "p" }?.map { it[1] } fun privateTaggedUsers(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "p" }?.map { it[1] }
fun privateHashtags(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "t" }?.map { it[1] } fun privateHashtags(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "t" }?.map { it[1] }
fun privateGeohashes(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "g" }?.map { it[1] }
fun privateTaggedEvents(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "e" }?.map { it[1] } fun privateTaggedEvents(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "e" }?.map { it[1] }
fun privateTaggedAddresses(privKey: ByteArray) = privateTags(privKey)?.filter { it.firstOrNull() == "a" }?.mapNotNull { fun privateTaggedAddresses(privKey: ByteArray) = privateTags(privKey)?.filter { it.firstOrNull() == "a" }?.mapNotNull {
val aTagValue = it.getOrNull(1) val aTagValue = it.getOrNull(1)
@@ -36,6 +36,7 @@ open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter<Not
val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet() val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
val followingTagSet = account.selectedTagsFollowList(account.defaultDiscoveryFollowList) ?: emptySet() val followingTagSet = account.selectedTagsFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
val followingGeohashSet = account.selectedGeohashesFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
val createEvents = collection.filter { it.event is ChannelCreateEvent } val createEvents = collection.filter { it.event is ChannelCreateEvent }
val anyOtherChannelEvent = collection val anyOtherChannelEvent = collection
@@ -48,7 +49,7 @@ open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter<Not
val activities = (createEvents + anyOtherChannelEvent) val activities = (createEvents + anyOtherChannelEvent)
.asSequence() .asSequence()
// .filter { it.event is ChannelCreateEvent } // Event heads might not be loaded yet. // .filter { it.event is ChannelCreateEvent } // Event heads might not be loaded yet.
.filter { isGlobal || it.author?.pubkeyHex in followingKeySet || it.event?.isTaggedHashes(followingTagSet) == true } .filter { isGlobal || it.author?.pubkeyHex in followingKeySet || it.event?.isTaggedHashes(followingTagSet) == true || it.event?.isTaggedGeoHashes(followingGeohashSet) == true }
.filter { isHiddenList || it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true } .filter { isHiddenList || it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true }
.filter { (it.createdAt() ?: 0) <= now } .filter { (it.createdAt() ?: 0) <= now }
.toSet() .toSet()
@@ -36,6 +36,7 @@ open class DiscoverCommunityFeedFilter(val account: Account) : AdditiveFeedFilte
val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet() val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
val followingTagSet = account.selectedTagsFollowList(account.defaultDiscoveryFollowList) ?: emptySet() val followingTagSet = account.selectedTagsFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
val followingGeohashSet = account.selectedGeohashesFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
val createEvents = collection.filter { it.event is CommunityDefinitionEvent } val createEvents = collection.filter { it.event is CommunityDefinitionEvent }
val anyOtherCommunityEvent = collection val anyOtherCommunityEvent = collection
@@ -48,7 +49,7 @@ open class DiscoverCommunityFeedFilter(val account: Account) : AdditiveFeedFilte
val activities = (createEvents + anyOtherCommunityEvent) val activities = (createEvents + anyOtherCommunityEvent)
.asSequence() .asSequence()
.filter { it.event is CommunityDefinitionEvent } .filter { it.event is CommunityDefinitionEvent }
.filter { isGlobal || it.author?.pubkeyHex in followingKeySet || it.event?.isTaggedHashes(followingTagSet) == true } .filter { isGlobal || it.author?.pubkeyHex in followingKeySet || it.event?.isTaggedHashes(followingTagSet) == true || it.event?.isTaggedGeoHashes(followingGeohashSet) == true }
.filter { isHiddenList || it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true } .filter { isHiddenList || it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true }
.filter { (it.createdAt() ?: 0) <= now } .filter { (it.createdAt() ?: 0) <= now }
.toSet() .toSet()
@@ -39,10 +39,9 @@ open class DiscoverLiveFeedFilter(val account: Account) : AdditiveFeedFilter<Not
val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS
val isHiddenList = showHiddenKey() val isHiddenList = showHiddenKey()
val followingKeySet = val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet() val followingTagSet = account.selectedTagsFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
val followingTagSet = val followingGeohashSet = account.selectedGeohashesFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
account.selectedTagsFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
val activities = collection val activities = collection
.asSequence() .asSequence()
@@ -50,6 +49,8 @@ open class DiscoverLiveFeedFilter(val account: Account) : AdditiveFeedFilter<Not
.filter { .filter {
isGlobal || it.author?.pubkeyHex in followingKeySet || it.event?.isTaggedHashes( isGlobal || it.author?.pubkeyHex in followingKeySet || it.event?.isTaggedHashes(
followingTagSet followingTagSet
) == true || it.event?.isTaggedGeoHashes(
followingGeohashSet
) == true ) == true
} }
.filter { isHiddenList || it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true } .filter { isHiddenList || it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true }
@@ -0,0 +1,46 @@
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
class GeoHashFeedFilter(val tag: String, val account: Account) : AdditiveFeedFilter<Note>() {
override fun feedKey(): String {
return account.userProfile().pubkeyHex + "-" + tag
}
override fun feed(): List<Note> {
return sort(innerApplyFilter(LocalCache.notes.values))
}
override fun applyFilter(collection: Set<Note>): Set<Note> {
return innerApplyFilter(collection)
}
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
val myTag = tag ?: return emptySet()
return collection
.asSequence()
.filter {
(
it.event is TextNoteEvent ||
it.event is LongTextNoteEvent ||
it.event is ChannelMessageEvent ||
it.event is PrivateDmEvent
) &&
it.event?.isTaggedGeoHash(myTag) == true
}
.filter { account.isAcceptable(it) }
.toSet()
}
override fun sort(collection: Set<Note>): List<Note> {
return collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed()
}
}
@@ -35,6 +35,7 @@ class HomeConversationsFeedFilter(val account: Account) : AdditiveFeedFilter<Not
val followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet() val followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet()
val followingTagSet = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet() val followingTagSet = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet()
val followingGeoHashSet = account.selectedGeohashesFollowList(account.defaultHomeFollowList) ?: emptySet()
val now = TimeUtils.now() val now = TimeUtils.now()
@@ -42,7 +43,7 @@ class HomeConversationsFeedFilter(val account: Account) : AdditiveFeedFilter<Not
.asSequence() .asSequence()
.filter { .filter {
(it.event is TextNoteEvent || it.event is PollNoteEvent || it.event is ChannelMessageEvent || it.event is LiveActivitiesChatMessageEvent) && (it.event is TextNoteEvent || it.event is PollNoteEvent || it.event is ChannelMessageEvent || it.event is LiveActivitiesChatMessageEvent) &&
(isGlobal || it.author?.pubkeyHex in followingKeySet || it.event?.isTaggedHashes(followingTagSet) ?: false) && (isGlobal || it.author?.pubkeyHex in followingKeySet || it.event?.isTaggedHashes(followingTagSet) ?: false || it.event?.isTaggedGeoHashes(followingGeoHashSet) ?: false) &&
// && account.isAcceptable(it) // This filter follows only. No need to check if acceptable // && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
(isHiddenList || it.author?.let { !account.isHidden(it) } ?: true) && (isHiddenList || it.author?.let { !account.isHidden(it) } ?: true) &&
((it.event?.createdAt() ?: 0) < now) && ((it.event?.createdAt() ?: 0) < now) &&
@@ -41,6 +41,7 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
val followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet() val followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet()
val followingTagSet = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet() val followingTagSet = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet()
val followingGeoSet = account.selectedGeohashesFollowList(account.defaultHomeFollowList) ?: emptySet()
val followingCommunities = account.selectedCommunitiesFollowList(account.defaultHomeFollowList) ?: emptySet() val followingCommunities = account.selectedCommunitiesFollowList(account.defaultHomeFollowList) ?: emptySet()
val oneMinuteInTheFuture = TimeUtils.now() + (1 * 60) // one minute in the future. val oneMinuteInTheFuture = TimeUtils.now() + (1 * 60) // one minute in the future.
@@ -51,7 +52,7 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
.filter { it -> .filter { it ->
val noteEvent = it.event val noteEvent = it.event
(noteEvent is TextNoteEvent || noteEvent is ClassifiedsEvent || noteEvent is RepostEvent || noteEvent is GenericRepostEvent || noteEvent is LongTextNoteEvent || noteEvent is PollNoteEvent || noteEvent is HighlightEvent || noteEvent is AudioTrackEvent) && (noteEvent is TextNoteEvent || noteEvent is ClassifiedsEvent || noteEvent is RepostEvent || noteEvent is GenericRepostEvent || noteEvent is LongTextNoteEvent || noteEvent is PollNoteEvent || noteEvent is HighlightEvent || noteEvent is AudioTrackEvent) &&
(isGlobal || it.author?.pubkeyHex in followingKeySet || noteEvent.isTaggedHashes(followingTagSet) || noteEvent.isTaggedAddressableNotes(followingCommunities)) && (isGlobal || it.author?.pubkeyHex in followingKeySet || noteEvent.isTaggedHashes(followingTagSet) || noteEvent.isTaggedGeoHashes(followingGeoSet) || noteEvent.isTaggedAddressableNotes(followingCommunities)) &&
// && account.isAcceptable(it) // This filter follows only. No need to check if acceptable // && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
(isHiddenList || it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true) && (isHiddenList || it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true) &&
((it.event?.createdAt() ?: 0) < oneMinuteInTheFuture) && ((it.event?.createdAt() ?: 0) < oneMinuteInTheFuture) &&
@@ -33,11 +33,12 @@ class VideoFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
val followingKeySet = account.selectedUsersFollowList(account.defaultStoriesFollowList) ?: emptySet() val followingKeySet = account.selectedUsersFollowList(account.defaultStoriesFollowList) ?: emptySet()
val followingTagSet = account.selectedTagsFollowList(account.defaultStoriesFollowList) ?: emptySet() val followingTagSet = account.selectedTagsFollowList(account.defaultStoriesFollowList) ?: emptySet()
val followingGeohashSet = account.selectedGeohashesFollowList(account.defaultStoriesFollowList) ?: emptySet()
return collection return collection
.asSequence() .asSequence()
.filter { it.event is FileHeaderEvent || it.event is FileStorageHeaderEvent } .filter { it.event is FileHeaderEvent || it.event is FileStorageHeaderEvent }
.filter { isGlobal || it.author?.pubkeyHex in followingKeySet || (it.event?.isTaggedHashes(followingTagSet) ?: false) } .filter { isGlobal || it.author?.pubkeyHex in followingKeySet || (it.event?.isTaggedHashes(followingTagSet) ?: false) || (it.event?.isTaggedGeoHashes(followingGeohashSet) ?: false) }
.filter { isHiddenList || account.isAcceptable(it) } .filter { isHiddenList || account.isAcceptable(it) }
.filter { it.createdAt()!! <= now } .filter { it.createdAt()!! <= now }
.toSet() .toSet()
@@ -25,6 +25,7 @@ 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.CommunityScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.CommunityScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.DiscoverScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.DiscoverScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.GeoHashScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.HashtagScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.HashtagScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.HiddenUsersScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.HiddenUsersScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.HomeScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.HomeScreen
@@ -178,6 +179,16 @@ fun AppNavigation(
}) })
} }
Route.Geohash.let { route ->
composable(route.route, route.arguments, content = {
GeoHashScreen(
tag = it.arguments?.getString("id"),
accountViewModel = accountViewModel,
nav = nav
)
})
}
Route.Community.let { route -> Route.Community.let { route ->
composable(route.route, route.arguments, content = { composable(route.route, route.arguments, content = {
CommunityScreen( CommunityScreen(
@@ -63,6 +63,7 @@ import com.vitorpamplona.amethyst.service.NostrChatroomDataSource
import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource
import com.vitorpamplona.amethyst.service.NostrCommunityDataSource import com.vitorpamplona.amethyst.service.NostrCommunityDataSource
import com.vitorpamplona.amethyst.service.NostrDiscoveryDataSource import com.vitorpamplona.amethyst.service.NostrDiscoveryDataSource
import com.vitorpamplona.amethyst.service.NostrGeohashDataSource
import com.vitorpamplona.amethyst.service.NostrHashtagDataSource 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
@@ -124,6 +125,7 @@ private fun RenderTopRouteBar(
Route.Room.base -> NoTopBar() Route.Room.base -> NoTopBar()
Route.Community.base -> NoTopBar() Route.Community.base -> NoTopBar()
Route.Hashtag.base -> NoTopBar() Route.Hashtag.base -> NoTopBar()
Route.Geohash.base -> NoTopBar()
// Route.Profile.route -> TopBarWithBackButton(nav) // Route.Profile.route -> TopBarWithBackButton(nav)
Route.Home.base -> HomeTopBar(followLists, scaffoldState, accountViewModel, nav) Route.Home.base -> HomeTopBar(followLists, scaffoldState, accountViewModel, nav)
Route.Video.base -> StoriesTopBar(followLists, scaffoldState, accountViewModel, nav) Route.Video.base -> StoriesTopBar(followLists, scaffoldState, accountViewModel, nav)
@@ -492,6 +494,7 @@ fun debugState(context: Context) {
NostrCommunityDataSource.printCounter() NostrCommunityDataSource.printCounter()
NostrDiscoveryDataSource.printCounter() NostrDiscoveryDataSource.printCounter()
NostrHashtagDataSource.printCounter() NostrHashtagDataSource.printCounter()
NostrGeohashDataSource.printCounter()
NostrHomeDataSource.printCounter() NostrHomeDataSource.printCounter()
NostrSearchEventOrUserDataSource.printCounter() NostrSearchEventOrUserDataSource.printCounter()
NostrSingleChannelDataSource.printCounter() NostrSingleChannelDataSource.printCounter()
@@ -107,6 +107,12 @@ sealed class Route(
arguments = listOf(navArgument("id") { type = NavType.StringType }).toImmutableList() arguments = listOf(navArgument("id") { type = NavType.StringType }).toImmutableList()
) )
object Geohash : Route(
route = "Geohash/{id}",
icon = R.drawable.ic_moments,
arguments = listOf(navArgument("id") { type = NavType.StringType }).toImmutableList()
)
object Community : Route( object Community : Route(
route = "Community/{id}", route = "Community/{id}",
icon = R.drawable.ic_moments, icon = R.drawable.ic_moments,
@@ -35,6 +35,7 @@ import com.vitorpamplona.amethyst.ui.note.NIP05VerifiedIcon
import com.vitorpamplona.amethyst.ui.theme.NIP05IconSize import com.vitorpamplona.amethyst.ui.theme.NIP05IconSize
import com.vitorpamplona.amethyst.ui.theme.Size16Modifier import com.vitorpamplona.amethyst.ui.theme.Size16Modifier
import com.vitorpamplona.amethyst.ui.theme.nip05 import com.vitorpamplona.amethyst.ui.theme.nip05
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -115,6 +116,14 @@ fun ObserveDisplayNip05Status(baseUser: User, columnModifier: Modifier = Modifie
Crossfade(targetState = nip05, modifier = columnModifier) { Crossfade(targetState = nip05, modifier = columnModifier) {
if (it != null) { if (it != null) {
DisplayNIP05Line(it, baseUser, columnModifier) DisplayNIP05Line(it, baseUser, columnModifier)
} else {
Text(
text = baseUser.pubkeyDisplayHex(),
color = MaterialTheme.colors.placeholderText,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = columnModifier
)
} }
} }
} }
@@ -2409,7 +2409,7 @@ fun SecondUserInfoRow(
val geo = remember { noteEvent.getGeoHash() } val geo = remember { noteEvent.getGeoHash() }
if (geo != null) { if (geo != null) {
DisplayLocation(geo) DisplayLocation(geo, nav)
} }
val baseReward = remember { noteEvent.getReward()?.let { Reward(it) } } val baseReward = remember { noteEvent.getReward()?.let { Reward(it) } }
@@ -2425,17 +2425,22 @@ fun SecondUserInfoRow(
} }
@Composable @Composable
fun DisplayLocation(geohash: String) { fun DisplayLocation(geohash: String, nav: (String) -> Unit) {
val context = LocalContext.current val context = LocalContext.current
val cityName = remember(geohash) { val cityName = remember(geohash) {
ReverseGeoLocationUtil().execute(geohash.toGeoHash().toLocation(), context) ReverseGeoLocationUtil().execute(geohash.toGeoHash().toLocation(), context)
} }
Text( ClickableText(
text = cityName ?: geohash, text = AnnotatedString(cityName ?: geohash),
color = MaterialTheme.colors.lessImportantLink, onClick = { nav("Geohash/$geohash") },
fontSize = Font14SP, style = LocalTextStyle.current.copy(
fontWeight = FontWeight.Bold, color = MaterialTheme.colors.primary.copy(
alpha = 0.52f
),
fontSize = Font14SP,
fontWeight = FontWeight.Bold
),
maxLines = 1 maxLines = 1
) )
} }
@@ -85,7 +85,7 @@ private fun UserNameDisplay(
} }
@Composable @Composable
private fun NPubDisplay(npubDisplay: String, modifier: Modifier) { fun NPubDisplay(npubDisplay: String, modifier: Modifier) {
Text( Text(
text = npubDisplay, text = npubDisplay,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
@@ -27,6 +27,7 @@ import com.vitorpamplona.amethyst.ui.dal.DiscoverChatFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DiscoverCommunityFeedFilter import com.vitorpamplona.amethyst.ui.dal.DiscoverCommunityFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DiscoverLiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DiscoverLiveFeedFilter
import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.amethyst.ui.dal.FeedFilter
import com.vitorpamplona.amethyst.ui.dal.GeoHashFeedFilter
import com.vitorpamplona.amethyst.ui.dal.HashtagFeedFilter 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
@@ -125,6 +126,14 @@ class NostrHashtagFeedViewModel(val hashtag: String, val account: Account) : Fee
} }
} }
class NostrGeoHashFeedViewModel(val geohash: String, val account: Account) : FeedViewModel(GeoHashFeedFilter(geohash, account)) {
class Factory(val geohash: String, val account: Account) : ViewModelProvider.Factory {
override fun <NostrGeoHashFeedViewModel : ViewModel> create(modelClass: Class<NostrGeoHashFeedViewModel>): NostrGeoHashFeedViewModel {
return NostrGeoHashFeedViewModel(geohash, account) as NostrGeoHashFeedViewModel
}
}
}
class NostrCommunityFeedViewModel(val note: AddressableNote, val account: Account) : FeedViewModel(CommunityFeedFilter(note, account)) { class NostrCommunityFeedViewModel(val note: AddressableNote, val account: Account) : FeedViewModel(CommunityFeedFilter(note, account)) {
class Factory(val note: AddressableNote, val account: Account) : ViewModelProvider.Factory { class Factory(val note: AddressableNote, val account: Account) : ViewModelProvider.Factory {
override fun <NostrCommunityFeedViewModel : ViewModel> create(modelClass: Class<NostrCommunityFeedViewModel>): NostrCommunityFeedViewModel { override fun <NostrCommunityFeedViewModel : ViewModel> create(modelClass: Class<NostrCommunityFeedViewModel>): NostrCommunityFeedViewModel {
@@ -0,0 +1,203 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn
import android.widget.Toast
import androidx.compose.foundation.clickable
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.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
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 com.fonfon.kgeohash.toGeoHash
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.NostrGeohashDataSource
import com.vitorpamplona.amethyst.service.ReverseGeoLocationUtil
import com.vitorpamplona.amethyst.ui.screen.NostrGeoHashFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
import com.vitorpamplona.amethyst.ui.theme.HalfPadding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
fun GeoHashScreen(tag: String?, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
if (tag == null) return
PrepareViewModelsGeoHashScreen(tag, accountViewModel, nav)
}
@Composable
fun PrepareViewModelsGeoHashScreen(tag: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val followsFeedViewModel: NostrGeoHashFeedViewModel = viewModel(
key = tag + "GeoHashFeedViewModel",
factory = NostrGeoHashFeedViewModel.Factory(
tag,
accountViewModel.account
)
)
GeoHashScreen(tag, followsFeedViewModel, accountViewModel, nav)
}
@Composable
fun GeoHashScreen(tag: String, feedViewModel: NostrGeoHashFeedViewModel, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val lifeCycleOwner = LocalLifecycleOwner.current
NostrGeohashDataSource.loadHashtag(tag)
LaunchedEffect(tag) {
NostrGeohashDataSource.start()
feedViewModel.invalidateData()
}
DisposableEffect(accountViewModel) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
println("Hashtag Start")
NostrGeohashDataSource.loadHashtag(tag)
NostrGeohashDataSource.start()
feedViewModel.invalidateData()
}
if (event == Lifecycle.Event.ON_PAUSE) {
println("Hashtag Stop")
NostrGeohashDataSource.loadHashtag(null)
NostrGeohashDataSource.stop()
}
}
lifeCycleOwner.lifecycle.addObserver(observer)
onDispose {
lifeCycleOwner.lifecycle.removeObserver(observer)
NostrGeohashDataSource.loadHashtag(null)
NostrGeohashDataSource.stop()
}
}
Column(Modifier.fillMaxHeight()) {
Column(
modifier = Modifier.padding(vertical = 0.dp)
) {
GeoHashHeader(tag, accountViewModel)
RefresheableFeedView(
feedViewModel,
null,
accountViewModel = accountViewModel,
nav = nav
)
}
}
}
@Composable
fun GeoHashHeader(tag: String, account: AccountViewModel, onClick: () -> Unit = { }) {
Column(
Modifier.clickable { onClick() }
) {
Column(modifier = HalfPadding) {
Row(verticalAlignment = Alignment.CenterVertically) {
Column(
modifier = Modifier
.padding(start = 10.dp)
.weight(1f)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
val context = LocalContext.current
val cityName = remember(tag) {
ReverseGeoLocationUtil().execute(tag.toGeoHash().toLocation(), context)
}
Text(
"$cityName ($tag)",
fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1f)
)
HashtagActionOptions(tag, account)
}
}
}
}
Divider(
modifier = Modifier.padding(start = 12.dp, end = 12.dp),
thickness = 0.25.dp
)
}
}
@Composable
private fun HashtagActionOptions(
tag: String,
accountViewModel: AccountViewModel
) {
val scope = rememberCoroutineScope()
val context = LocalContext.current
val userState by accountViewModel.userProfile().live().follows.observeAsState()
val isFollowingTag by remember(userState) {
derivedStateOf {
userState?.user?.isFollowingGeohashCached(tag) ?: false
}
}
if (isFollowingTag) {
UnfollowButton {
if (!accountViewModel.isWriteable()) {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_unfollow),
Toast.LENGTH_SHORT
)
.show()
}
} else {
scope.launch(Dispatchers.IO) {
accountViewModel.account.unfollowGeohash(tag)
}
}
}
} else {
FollowButton {
if (!accountViewModel.isWriteable()) {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_follow),
Toast.LENGTH_SHORT
)
.show()
}
} else {
scope.launch(Dispatchers.IO) {
accountViewModel.account.followGeohash(tag)
}
}
}
}
}
@@ -18,7 +18,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -170,7 +169,7 @@ private fun HashtagActionOptions(
} }
} else { } else {
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
accountViewModel.account.unfollow(tag) accountViewModel.account.unfollowHashtag(tag)
} }
} }
} }
@@ -188,7 +187,7 @@ private fun HashtagActionOptions(
} }
} else { } else {
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
accountViewModel.account.follow(tag) accountViewModel.account.followHashtag(tag)
} }
} }
} }