- Adds Basic Support for Communities (172)
- Adds Community discovery screens - Adds Public Chat discovery - Adds Community Follow/Unfollow - Renders Community Posts in feed - Creates a summary of verified participants in Communities/Chats/Streams - Restructures Hashtag Screen to the new Screen Building structure - Remembers scroll position in Live, Community and Chat discovery
This commit is contained in:
@@ -43,6 +43,7 @@ private object PrefKeys {
|
|||||||
const val NOSTR_PRIVKEY = "nostr_privkey"
|
const val NOSTR_PRIVKEY = "nostr_privkey"
|
||||||
const val NOSTR_PUBKEY = "nostr_pubkey"
|
const val NOSTR_PUBKEY = "nostr_pubkey"
|
||||||
const val FOLLOWING_CHANNELS = "following_channels"
|
const val FOLLOWING_CHANNELS = "following_channels"
|
||||||
|
const val FOLLOWING_COMMUNITIES = "following_communities"
|
||||||
const val HIDDEN_USERS = "hidden_users"
|
const val HIDDEN_USERS = "hidden_users"
|
||||||
const val RELAYS = "relays"
|
const val RELAYS = "relays"
|
||||||
const val DONT_TRANSLATE_FROM = "dontTranslateFrom"
|
const val DONT_TRANSLATE_FROM = "dontTranslateFrom"
|
||||||
@@ -205,6 +206,7 @@ object LocalPreferences {
|
|||||||
account.loggedIn.privKey?.let { putString(PrefKeys.NOSTR_PRIVKEY, it.toHex()) }
|
account.loggedIn.privKey?.let { putString(PrefKeys.NOSTR_PRIVKEY, it.toHex()) }
|
||||||
account.loggedIn.pubKey.let { putString(PrefKeys.NOSTR_PUBKEY, it.toHex()) }
|
account.loggedIn.pubKey.let { putString(PrefKeys.NOSTR_PUBKEY, it.toHex()) }
|
||||||
putStringSet(PrefKeys.FOLLOWING_CHANNELS, account.followingChannels)
|
putStringSet(PrefKeys.FOLLOWING_CHANNELS, account.followingChannels)
|
||||||
|
putStringSet(PrefKeys.FOLLOWING_COMMUNITIES, account.followingCommunities)
|
||||||
putStringSet(PrefKeys.HIDDEN_USERS, account.hiddenUsers)
|
putStringSet(PrefKeys.HIDDEN_USERS, account.hiddenUsers)
|
||||||
putString(PrefKeys.RELAYS, gson.toJson(account.localRelays))
|
putString(PrefKeys.RELAYS, gson.toJson(account.localRelays))
|
||||||
putStringSet(PrefKeys.DONT_TRANSLATE_FROM, account.dontTranslateFrom)
|
putStringSet(PrefKeys.DONT_TRANSLATE_FROM, account.dontTranslateFrom)
|
||||||
@@ -247,6 +249,7 @@ object LocalPreferences {
|
|||||||
val pubKey = getString(PrefKeys.NOSTR_PUBKEY, null) ?: return null
|
val pubKey = getString(PrefKeys.NOSTR_PUBKEY, null) ?: return null
|
||||||
val privKey = getString(PrefKeys.NOSTR_PRIVKEY, null)
|
val privKey = getString(PrefKeys.NOSTR_PRIVKEY, null)
|
||||||
val followingChannels = getStringSet(PrefKeys.FOLLOWING_CHANNELS, null) ?: setOf()
|
val followingChannels = getStringSet(PrefKeys.FOLLOWING_CHANNELS, null) ?: setOf()
|
||||||
|
val followingCommunities = getStringSet(PrefKeys.FOLLOWING_COMMUNITIES, null) ?: setOf()
|
||||||
val hiddenUsers = getStringSet(PrefKeys.HIDDEN_USERS, emptySet()) ?: setOf()
|
val hiddenUsers = getStringSet(PrefKeys.HIDDEN_USERS, emptySet()) ?: setOf()
|
||||||
val localRelays = gson.fromJson(
|
val localRelays = gson.fromJson(
|
||||||
getString(PrefKeys.RELAYS, "[]"),
|
getString(PrefKeys.RELAYS, "[]"),
|
||||||
@@ -340,6 +343,7 @@ object LocalPreferences {
|
|||||||
val a = Account(
|
val a = Account(
|
||||||
loggedIn = Persona(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray()),
|
loggedIn = Persona(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray()),
|
||||||
followingChannels = followingChannels,
|
followingChannels = followingChannels,
|
||||||
|
followingCommunities = followingCommunities,
|
||||||
hiddenUsers = hiddenUsers,
|
hiddenUsers = hiddenUsers,
|
||||||
localRelays = localRelays,
|
localRelays = localRelays,
|
||||||
dontTranslateFrom = dontTranslateFrom,
|
dontTranslateFrom = dontTranslateFrom,
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ object ServiceManager {
|
|||||||
NostrHomeDataSource.start()
|
NostrHomeDataSource.start()
|
||||||
NostrAccountDataSource.start()
|
NostrAccountDataSource.start()
|
||||||
NostrChatroomListDataSource.start()
|
NostrChatroomListDataSource.start()
|
||||||
|
NostrDiscoveryDataSource.start()
|
||||||
|
|
||||||
// More Info Data Sources
|
// More Info Data Sources
|
||||||
NostrSingleEventDataSource.start()
|
NostrSingleEventDataSource.start()
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ val KIND3_FOLLOWS = " All Follows "
|
|||||||
class Account(
|
class Account(
|
||||||
val loggedIn: Persona,
|
val loggedIn: Persona,
|
||||||
var followingChannels: Set<String> = DefaultChannels,
|
var followingChannels: Set<String> = DefaultChannels,
|
||||||
|
var followingCommunities: Set<String> = setOf(),
|
||||||
var hiddenUsers: Set<String> = setOf(),
|
var hiddenUsers: Set<String> = setOf(),
|
||||||
var localRelays: Set<RelaySetupInfo> = Constants.defaultRelays.toSet(),
|
var localRelays: Set<RelaySetupInfo> = Constants.defaultRelays.toSet(),
|
||||||
var dontTranslateFrom: Set<String> = getLanguagesSpokenByUser(),
|
var dontTranslateFrom: Set<String> = getLanguagesSpokenByUser(),
|
||||||
@@ -107,6 +108,10 @@ class Account(
|
|||||||
return followingChannels.mapNotNull { LocalCache.checkGetOrCreateChannel(it) }
|
return followingChannels.mapNotNull { LocalCache.checkGetOrCreateChannel(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun followingCommunities(): List<AddressableNote> {
|
||||||
|
return followingCommunities.mapNotNull { LocalCache.checkGetOrCreateAddressableNote(it) }
|
||||||
|
}
|
||||||
|
|
||||||
fun isWriteable(): Boolean {
|
fun isWriteable(): Boolean {
|
||||||
return loggedIn.privKey != null
|
return loggedIn.privKey != null
|
||||||
}
|
}
|
||||||
@@ -857,6 +862,20 @@ class Account(
|
|||||||
saveable.invalidateData()
|
saveable.invalidateData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun joinCommunity(idHex: String) {
|
||||||
|
followingCommunities = followingCommunities + idHex
|
||||||
|
live.invalidateData()
|
||||||
|
|
||||||
|
saveable.invalidateData()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun leaveCommunity(idHex: String) {
|
||||||
|
followingCommunities = followingCommunities - idHex
|
||||||
|
live.invalidateData()
|
||||||
|
|
||||||
|
saveable.invalidateData()
|
||||||
|
}
|
||||||
|
|
||||||
fun hideUser(pubkeyHex: String) {
|
fun hideUser(pubkeyHex: String) {
|
||||||
hiddenUsers = hiddenUsers + pubkeyHex
|
hiddenUsers = hiddenUsers + pubkeyHex
|
||||||
live.invalidateData()
|
live.invalidateData()
|
||||||
@@ -1153,6 +1172,10 @@ class Account(
|
|||||||
return user.pubkeyHex in followingKeySet()
|
return user.pubkeyHex in followingKeySet()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isFollowing(user: HexKey): Boolean {
|
||||||
|
return user in followingKeySet()
|
||||||
|
}
|
||||||
|
|
||||||
fun isAcceptable(note: Note): Boolean {
|
fun isAcceptable(note: Note): Boolean {
|
||||||
return note.author?.let { isAcceptable(it) } ?: true && // if user hasn't hided this author
|
return note.author?.let { isAcceptable(it) } ?: true && // if user hasn't hided this author
|
||||||
isAcceptableDirect(note) &&
|
isAcceptableDirect(note) &&
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ object LocalCache {
|
|||||||
return users.get(key)
|
return users.get(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getAddressableNoteIfExists(key: String): AddressableNote? {
|
||||||
|
return addressables[key]
|
||||||
|
}
|
||||||
|
|
||||||
fun getNoteIfExists(key: String): Note? {
|
fun getNoteIfExists(key: String): Note? {
|
||||||
return addressables[key] ?: notes[key]
|
return addressables[key] ?: notes[key]
|
||||||
}
|
}
|
||||||
@@ -681,16 +685,20 @@ object LocalCache {
|
|||||||
// Log.d("TN", "New Boost (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}")
|
// Log.d("TN", "New Boost (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}")
|
||||||
|
|
||||||
val author = getOrCreateUser(event.pubKey)
|
val author = getOrCreateUser(event.pubKey)
|
||||||
val repliesTo = event.taggedAddresses().map { getOrCreateAddressableNote(it) }
|
|
||||||
|
|
||||||
note.loadEvent(event, author, repliesTo)
|
val communities = event.communities()
|
||||||
|
val eventsApproved = event.approvedEvents().mapNotNull { checkGetOrCreateNote(it) }
|
||||||
|
|
||||||
|
val repliesTo = communities.map { getOrCreateAddressableNote(it) }
|
||||||
|
|
||||||
|
note.loadEvent(event, author, eventsApproved)
|
||||||
|
|
||||||
// Prepares user's profile view.
|
// Prepares user's profile view.
|
||||||
author.addNote(note)
|
author.addNote(note)
|
||||||
|
|
||||||
// Counts the replies
|
// Counts the replies
|
||||||
repliesTo.forEach {
|
repliesTo.forEach {
|
||||||
it.addReply(note)
|
it.addBoost(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshObservers(note)
|
refreshObservers(note)
|
||||||
@@ -1181,7 +1189,7 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
println("PRUNE: ${toBeRemoved.size} messages removed from ${it.value.toBestDisplayName()}")
|
println("PRUNE: ${toBeRemoved.size} messages removed from ${it.value.toBestDisplayName()}. ${it.value.notes.size} kept")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -279,20 +279,55 @@ open class Note(val idHex: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun publicZapAuthors(): Set<HexKey> {
|
fun publicZapAuthors(): Set<User> {
|
||||||
|
// Zaps who the requester was the user
|
||||||
|
return zaps.mapNotNull {
|
||||||
|
it.key.author
|
||||||
|
}.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun publicZapAuthorHexes(): Set<HexKey> {
|
||||||
// Zaps who the requester was the user
|
// Zaps who the requester was the user
|
||||||
return zaps.mapNotNull {
|
return zaps.mapNotNull {
|
||||||
it.key.author?.pubkeyHex
|
it.key.author?.pubkeyHex
|
||||||
}.toSet()
|
}.toSet()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reactionAuthors(): Set<HexKey> {
|
fun reactionAuthors(): Set<User> {
|
||||||
|
// Zaps who the requester was the user
|
||||||
|
return reactions.values.map {
|
||||||
|
it.mapNotNull { it.author }
|
||||||
|
}.flatten().toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reactionAuthorHexes(): Set<HexKey> {
|
||||||
// Zaps who the requester was the user
|
// Zaps who the requester was the user
|
||||||
return reactions.values.map {
|
return reactions.values.map {
|
||||||
it.mapNotNull { it.author?.pubkeyHex }
|
it.mapNotNull { it.author?.pubkeyHex }
|
||||||
}.flatten().toSet()
|
}.flatten().toSet()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun replyAuthorHexes(): Set<HexKey> {
|
||||||
|
// Zaps who the requester was the user
|
||||||
|
return replies.mapNotNull {
|
||||||
|
it.author?.pubkeyHex
|
||||||
|
}.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun replyAuthors(): Set<User> {
|
||||||
|
// Zaps who the requester was the user
|
||||||
|
return replies.mapNotNull {
|
||||||
|
it.author
|
||||||
|
}.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun boostAuthors(): Set<User> {
|
||||||
|
// Zaps who the requester was the user
|
||||||
|
return boosts.mapNotNull {
|
||||||
|
it.author
|
||||||
|
}.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
fun isReactedBy(user: User): String? {
|
fun isReactedBy(user: User): String? {
|
||||||
return reactions.filter {
|
return reactions.filter {
|
||||||
it.value.any { it.author?.pubkeyHex == user.pubkeyHex }
|
it.value.any { it.author?.pubkeyHex == user.pubkeyHex }
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package com.vitorpamplona.amethyst.model
|
||||||
|
|
||||||
|
class ParticipantListBuilder {
|
||||||
|
private fun addFollowsThatDirectlyParticipateOnToSet(baseNote: Note, followingSet: Set<HexKey>?, set: MutableSet<User>) {
|
||||||
|
baseNote.author?.let { author ->
|
||||||
|
if (author !in set && (followingSet == null || author.pubkeyHex in followingSet)) set.add(author)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Breaks these searchers down to avoid the memory use of creating multiple lists
|
||||||
|
baseNote.replies.forEach { reply ->
|
||||||
|
reply.author?.let { author ->
|
||||||
|
if (author !in set && (followingSet == null || author.pubkeyHex in followingSet)) set.add(author)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
baseNote.boosts.forEach { boost ->
|
||||||
|
boost.author?.let { author ->
|
||||||
|
if (author !in set && (followingSet == null || author.pubkeyHex in followingSet)) set.add(author)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
baseNote.zaps.forEach { zapPair ->
|
||||||
|
zapPair.key.author?.let { author ->
|
||||||
|
if (author !in set && (followingSet == null || author.pubkeyHex in followingSet)) set.add(author)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
baseNote.reactions.forEach { reactionSet ->
|
||||||
|
reactionSet.value.forEach { reaction ->
|
||||||
|
reaction.author?.let { author ->
|
||||||
|
if (author !in set && (followingSet == null || author.pubkeyHex in followingSet)) set.add(author)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun followsThatParticipateOnDirect(baseNote: Note?, followingSet: Set<HexKey>?): Set<User> {
|
||||||
|
if (baseNote == null) return mutableSetOf()
|
||||||
|
|
||||||
|
val set = mutableSetOf<User>()
|
||||||
|
addFollowsThatDirectlyParticipateOnToSet(baseNote, followingSet, set)
|
||||||
|
return set
|
||||||
|
}
|
||||||
|
|
||||||
|
fun followsThatParticipateOn(baseNote: Note?, followingSet: Set<HexKey>?): Set<User> {
|
||||||
|
if (baseNote == null) return mutableSetOf()
|
||||||
|
|
||||||
|
val mySet = mutableSetOf<User>()
|
||||||
|
addFollowsThatDirectlyParticipateOnToSet(baseNote, followingSet, mySet)
|
||||||
|
|
||||||
|
baseNote.replies.forEach {
|
||||||
|
addFollowsThatDirectlyParticipateOnToSet(it, followingSet, mySet)
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalCache.getChannelIfExists(baseNote.idHex)?.notes?.values?.forEach {
|
||||||
|
addFollowsThatDirectlyParticipateOnToSet(it, followingSet, mySet)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mySet
|
||||||
|
}
|
||||||
|
|
||||||
|
fun countFollowsThatParticipateOn(baseNote: Note?, followingSet: Set<HexKey>?): Int {
|
||||||
|
if (baseNote == null) return 0
|
||||||
|
|
||||||
|
val list = followsThatParticipateOn(baseNote, followingSet)
|
||||||
|
|
||||||
|
return list.size
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
|
import com.vitorpamplona.amethyst.service.model.CommunityDefinitionEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.CommunityPostApprovalEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
|
||||||
|
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||||
|
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||||
|
|
||||||
|
object NostrCommunityDataSource : NostrDataSource("SingleCommunityFeed") {
|
||||||
|
private var communityToWatch: AddressableNote? = null
|
||||||
|
|
||||||
|
private fun createLoadCommunityFilter(): TypedFilter? {
|
||||||
|
val myCommunityToWatch = communityToWatch ?: return null
|
||||||
|
|
||||||
|
val community = myCommunityToWatch.event as? CommunityDefinitionEvent ?: return null
|
||||||
|
|
||||||
|
return TypedFilter(
|
||||||
|
types = COMMON_FEED_TYPES,
|
||||||
|
filter = JsonFilter(
|
||||||
|
authors = community.moderators().map { it.key }.plus(listOfNotNull(myCommunityToWatch.author?.pubkeyHex)),
|
||||||
|
tags = mapOf(
|
||||||
|
"a" to listOf(myCommunityToWatch.address.toTag())
|
||||||
|
),
|
||||||
|
kinds = listOf(CommunityPostApprovalEvent.kind),
|
||||||
|
limit = 500
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val loadCommunityChannel = requestNewChannel()
|
||||||
|
|
||||||
|
override fun updateChannelFilters() {
|
||||||
|
loadCommunityChannel.typedFilters = listOfNotNull(createLoadCommunityFilter()).ifEmpty { null }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadCommunity(note: AddressableNote?) {
|
||||||
|
communityToWatch = note
|
||||||
|
invalidateFilters()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ import com.vitorpamplona.amethyst.model.Account
|
|||||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.CommunityDefinitionEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.CommunityPostApprovalEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesChatMessageEvent
|
import com.vitorpamplona.amethyst.service.model.LiveActivitiesChatMessageEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent
|
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent
|
||||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||||
@@ -13,7 +15,7 @@ import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
|||||||
object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||||
lateinit var account: Account
|
lateinit var account: Account
|
||||||
|
|
||||||
fun createContextualFilter(): TypedFilter {
|
fun createLiveStreamFilter(): TypedFilter {
|
||||||
val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)
|
val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)
|
||||||
|
|
||||||
val followKeys = follows?.map {
|
val followKeys = follows?.map {
|
||||||
@@ -24,13 +26,47 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
|||||||
types = setOf(FeedType.GLOBAL),
|
types = setOf(FeedType.GLOBAL),
|
||||||
filter = JsonFilter(
|
filter = JsonFilter(
|
||||||
authors = followKeys,
|
authors = followKeys,
|
||||||
kinds = listOf(ChannelCreateEvent.kind, ChannelMetadataEvent.kind, ChannelMessageEvent.kind, LiveActivitiesChatMessageEvent.kind, LiveActivitiesEvent.kind),
|
kinds = listOf(LiveActivitiesChatMessageEvent.kind, LiveActivitiesEvent.kind),
|
||||||
limit = 1000
|
limit = 500
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createFollowTagsFilter(): TypedFilter? {
|
fun createPublicChatFilter(): TypedFilter {
|
||||||
|
val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)
|
||||||
|
|
||||||
|
val followKeys = follows?.map {
|
||||||
|
it.substring(0, 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
return TypedFilter(
|
||||||
|
types = setOf(FeedType.GLOBAL),
|
||||||
|
filter = JsonFilter(
|
||||||
|
authors = followKeys,
|
||||||
|
kinds = listOf(ChannelCreateEvent.kind, ChannelMetadataEvent.kind, ChannelMessageEvent.kind),
|
||||||
|
limit = 500
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createCommunitiesFilter(): TypedFilter {
|
||||||
|
val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)
|
||||||
|
|
||||||
|
val followKeys = follows?.map {
|
||||||
|
it.substring(0, 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
return TypedFilter(
|
||||||
|
types = setOf(FeedType.GLOBAL),
|
||||||
|
filter = JsonFilter(
|
||||||
|
authors = followKeys,
|
||||||
|
kinds = listOf(CommunityDefinitionEvent.kind, CommunityPostApprovalEvent.kind),
|
||||||
|
limit = 500
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createLiveStreamTagsFilter(): TypedFilter? {
|
||||||
val hashToLoad = account.selectedTagsFollowList(account.defaultDiscoveryFollowList)
|
val hashToLoad = account.selectedTagsFollowList(account.defaultDiscoveryFollowList)
|
||||||
|
|
||||||
if (hashToLoad.isNullOrEmpty()) return null
|
if (hashToLoad.isNullOrEmpty()) return null
|
||||||
@@ -38,13 +74,51 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
|||||||
return TypedFilter(
|
return TypedFilter(
|
||||||
types = setOf(FeedType.GLOBAL),
|
types = setOf(FeedType.GLOBAL),
|
||||||
filter = JsonFilter(
|
filter = JsonFilter(
|
||||||
kinds = listOf(ChannelCreateEvent.kind, ChannelMetadataEvent.kind, ChannelMessageEvent.kind, LiveActivitiesChatMessageEvent.kind, LiveActivitiesEvent.kind),
|
kinds = listOf(LiveActivitiesChatMessageEvent.kind, LiveActivitiesEvent.kind),
|
||||||
tags = mapOf(
|
tags = mapOf(
|
||||||
"t" to hashToLoad.map {
|
"t" to hashToLoad.map {
|
||||||
listOf(it, it.lowercase(), it.uppercase(), it.capitalize())
|
listOf(it, it.lowercase(), it.uppercase(), it.capitalize())
|
||||||
}.flatten()
|
}.flatten()
|
||||||
),
|
),
|
||||||
limit = 1000
|
limit = 500
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createPublicChatsTagsFilter(): TypedFilter? {
|
||||||
|
val hashToLoad = account.selectedTagsFollowList(account.defaultDiscoveryFollowList)
|
||||||
|
|
||||||
|
if (hashToLoad.isNullOrEmpty()) return null
|
||||||
|
|
||||||
|
return TypedFilter(
|
||||||
|
types = setOf(FeedType.GLOBAL),
|
||||||
|
filter = JsonFilter(
|
||||||
|
kinds = listOf(ChannelCreateEvent.kind, ChannelMetadataEvent.kind, ChannelMessageEvent.kind),
|
||||||
|
tags = mapOf(
|
||||||
|
"t" to hashToLoad.map {
|
||||||
|
listOf(it, it.lowercase(), it.uppercase(), it.capitalize())
|
||||||
|
}.flatten()
|
||||||
|
),
|
||||||
|
limit = 500
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createCommunitiesTagsFilter(): TypedFilter? {
|
||||||
|
val hashToLoad = account.selectedTagsFollowList(account.defaultDiscoveryFollowList)
|
||||||
|
|
||||||
|
if (hashToLoad.isNullOrEmpty()) return null
|
||||||
|
|
||||||
|
return TypedFilter(
|
||||||
|
types = setOf(FeedType.GLOBAL),
|
||||||
|
filter = JsonFilter(
|
||||||
|
kinds = listOf(CommunityDefinitionEvent.kind, CommunityPostApprovalEvent.kind),
|
||||||
|
tags = mapOf(
|
||||||
|
"t" to hashToLoad.map {
|
||||||
|
listOf(it, it.lowercase(), it.uppercase(), it.capitalize())
|
||||||
|
}.flatten()
|
||||||
|
),
|
||||||
|
limit = 500
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -52,6 +126,13 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
|||||||
val discoveryFeedChannel = requestNewChannel()
|
val discoveryFeedChannel = requestNewChannel()
|
||||||
|
|
||||||
override fun updateChannelFilters() {
|
override fun updateChannelFilters() {
|
||||||
discoveryFeedChannel.typedFilters = listOfNotNull(createContextualFilter(), createFollowTagsFilter()).ifEmpty { null }
|
discoveryFeedChannel.typedFilters = listOfNotNull(
|
||||||
|
createLiveStreamFilter(),
|
||||||
|
createPublicChatFilter(),
|
||||||
|
createCommunitiesFilter(),
|
||||||
|
createLiveStreamTagsFilter(),
|
||||||
|
createPublicChatsTagsFilter(),
|
||||||
|
createCommunitiesTagsFilter()
|
||||||
|
).ifEmpty { null }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ open class BaseTextNoteEvent(
|
|||||||
|
|
||||||
fun tagsWithoutCitations(): List<String> {
|
fun tagsWithoutCitations(): List<String> {
|
||||||
val repliesTo = replyTos()
|
val repliesTo = replyTos()
|
||||||
val tagAddresses = taggedAddresses().map { it.toTag() }
|
val tagAddresses = taggedAddresses().filter { it.kind != CommunityDefinitionEvent.kind }.map { it.toTag() }
|
||||||
if (repliesTo.isEmpty() && tagAddresses.isEmpty()) return emptyList()
|
if (repliesTo.isEmpty() && tagAddresses.isEmpty()) return emptyList()
|
||||||
|
|
||||||
val citations = findCitations()
|
val citations = findCitations()
|
||||||
|
|||||||
+7
-1
@@ -14,7 +14,13 @@ class ChannelHideMessageEvent(
|
|||||||
tags: List<List<String>>,
|
tags: List<List<String>>,
|
||||||
content: String,
|
content: String,
|
||||||
sig: HexKey
|
sig: HexKey
|
||||||
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
) : Event(id, pubKey, createdAt, kind, tags, content, sig), IsInPublicChatChannel {
|
||||||
|
override fun channel() = tags.firstOrNull {
|
||||||
|
it.size > 3 && it[0] == "e" && it[3] == "root"
|
||||||
|
}?.get(1) ?: tags.firstOrNull {
|
||||||
|
it.size > 1 && it[0] == "e"
|
||||||
|
}?.get(1)
|
||||||
|
|
||||||
fun eventsToHide() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
fun eventsToHide() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ class ChannelMessageEvent(
|
|||||||
tags: List<List<String>>,
|
tags: List<List<String>>,
|
||||||
content: String,
|
content: String,
|
||||||
sig: HexKey
|
sig: HexKey
|
||||||
) : BaseTextNoteEvent(id, pubKey, createdAt, kind, tags, content, sig) {
|
) : BaseTextNoteEvent(id, pubKey, createdAt, kind, tags, content, sig), IsInPublicChatChannel {
|
||||||
|
|
||||||
fun channel() = tags.firstOrNull {
|
override fun channel() = tags.firstOrNull {
|
||||||
it.size > 3 && it[0] == "e" && it[3] == "root"
|
it.size > 3 && it[0] == "e" && it[3] == "root"
|
||||||
}?.get(1) ?: tags.firstOrNull {
|
}?.get(1) ?: tags.firstOrNull {
|
||||||
it.size > 1 && it[0] == "e"
|
it.size > 1 && it[0] == "e"
|
||||||
@@ -65,3 +65,7 @@ class ChannelMessageEvent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IsInPublicChatChannel {
|
||||||
|
open fun channel(): String?
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,8 +15,9 @@ class ChannelMetadataEvent(
|
|||||||
tags: List<List<String>>,
|
tags: List<List<String>>,
|
||||||
content: String,
|
content: String,
|
||||||
sig: HexKey
|
sig: HexKey
|
||||||
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
) : Event(id, pubKey, createdAt, kind, tags, content, sig), IsInPublicChatChannel {
|
||||||
fun channel() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1)
|
|
||||||
|
override fun channel() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1)
|
||||||
fun channelInfo() =
|
fun channelInfo() =
|
||||||
try {
|
try {
|
||||||
MetadataEvent.gson.fromJson(content, ChannelCreateEvent.ChannelData::class.java)
|
MetadataEvent.gson.fromJson(content, ChannelCreateEvent.ChannelData::class.java)
|
||||||
|
|||||||
@@ -14,7 +14,12 @@ class ChannelMuteUserEvent(
|
|||||||
tags: List<List<String>>,
|
tags: List<List<String>>,
|
||||||
content: String,
|
content: String,
|
||||||
sig: HexKey
|
sig: HexKey
|
||||||
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
) : Event(id, pubKey, createdAt, kind, tags, content, sig), IsInPublicChatChannel {
|
||||||
|
override fun channel() = tags.firstOrNull {
|
||||||
|
it.size > 3 && it[0] == "e" && it[3] == "root"
|
||||||
|
}?.get(1) ?: tags.firstOrNull {
|
||||||
|
it.size > 1 && it[0] == "e"
|
||||||
|
}?.get(1)
|
||||||
|
|
||||||
fun usersToMute() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
fun usersToMute() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
||||||
|
|
||||||
|
|||||||
+16
@@ -26,6 +26,22 @@ class CommunityPostApprovalEvent(
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun communities() = tags.filter { it.size > 1 && it[0] == "a" }.mapNotNull {
|
||||||
|
val aTag = ATag.parse(it[1], it.getOrNull(2))
|
||||||
|
|
||||||
|
if (aTag?.kind == CommunityDefinitionEvent.kind) {
|
||||||
|
aTag
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun approvedEvents() = tags.filter {
|
||||||
|
it.size > 1 && (it[0] == "e" || (it[0] == "a" && ATag.parse(it[1], null)?.kind != CommunityDefinitionEvent.kind))
|
||||||
|
}.map {
|
||||||
|
it[1]
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val kind = 4550
|
const val kind = 4550
|
||||||
|
|
||||||
|
|||||||
@@ -71,10 +71,29 @@ open class Event(
|
|||||||
|
|
||||||
override fun isTaggedUser(idHex: String) = tags.any { it.size > 1 && it[0] == "p" && it[1] == idHex }
|
override fun isTaggedUser(idHex: String) = tags.any { it.size > 1 && it[0] == "p" && it[1] == idHex }
|
||||||
|
|
||||||
|
override fun isTaggedAddressableNote(idHex: String) = tags.any { it.size > 1 && it[0] == "a" && it[1] == idHex }
|
||||||
|
|
||||||
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 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 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 isTaggedAddressableKind(kind: Int): Boolean {
|
||||||
|
val kindStr = kind.toString()
|
||||||
|
return tags.any { it.size > 1 && it[0] == "a" && it[1].startsWith(kindStr) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTagOfAddressableKind(kind: Int): ATag? {
|
||||||
|
val kindStr = kind.toString()
|
||||||
|
val aTag = tags
|
||||||
|
.firstOrNull { it.size > 1 && it[0] == "a" && it[1].startsWith(kindStr) }
|
||||||
|
?.getOrNull(1)
|
||||||
|
?: return null
|
||||||
|
|
||||||
|
return ATag.parse(aTag, null)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getPoWRank(): Int {
|
override fun getPoWRank(): Int {
|
||||||
var rank = 0
|
var rank = 0
|
||||||
for (i in 0..id.length) {
|
for (i in 0..id.length) {
|
||||||
|
|||||||
@@ -27,10 +27,16 @@ interface EventInterface {
|
|||||||
fun hasValidSignature(): Boolean
|
fun hasValidSignature(): Boolean
|
||||||
|
|
||||||
fun isTaggedUser(idHex: String): Boolean
|
fun isTaggedUser(idHex: String): Boolean
|
||||||
|
fun isTaggedAddressableNote(idHex: String): Boolean
|
||||||
|
|
||||||
fun isTaggedHash(hashtag: String): Boolean
|
fun isTaggedHash(hashtag: String): Boolean
|
||||||
fun isTaggedHashes(hashtags: Set<String>): Boolean
|
fun isTaggedHashes(hashtags: Set<String>): Boolean
|
||||||
fun firstIsTaggedHashes(hashtags: Set<String>): String?
|
fun firstIsTaggedHashes(hashtags: Set<String>): String?
|
||||||
|
fun firstIsTaggedAddressableNote(addressableNotes: Set<String>): String?
|
||||||
|
|
||||||
|
fun isTaggedAddressableKind(kind: Int): Boolean
|
||||||
|
fun getTagOfAddressableKind(kind: Int): ATag?
|
||||||
|
|
||||||
fun hashtags(): List<String>
|
fun hashtags(): List<String>
|
||||||
|
|
||||||
fun getReward(): BigDecimal?
|
fun getReward(): BigDecimal?
|
||||||
|
|||||||
@@ -23,13 +23,13 @@ class LiveActivitiesEvent(
|
|||||||
fun summary() = tags.firstOrNull { it.size > 1 && it[0] == "summary" }?.get(1)
|
fun summary() = tags.firstOrNull { it.size > 1 && it[0] == "summary" }?.get(1)
|
||||||
fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1)
|
fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1)
|
||||||
fun streaming() = tags.firstOrNull { it.size > 1 && it[0] == "streaming" }?.get(1)
|
fun streaming() = tags.firstOrNull { it.size > 1 && it[0] == "streaming" }?.get(1)
|
||||||
fun starts() = tags.firstOrNull { it.size > 1 && it[0] == "starts" }?.get(1)
|
fun starts() = tags.firstOrNull { it.size > 1 && it[0] == "starts" }?.get(1)?.toLongOrNull()
|
||||||
fun ends() = tags.firstOrNull { it.size > 1 && it[0] == "ends" }?.get(1)
|
fun ends() = tags.firstOrNull { it.size > 1 && it[0] == "ends" }?.get(1)
|
||||||
fun status() = checkStatus(tags.firstOrNull { it.size > 1 && it[0] == "status" }?.get(1))
|
fun status() = checkStatus(tags.firstOrNull { it.size > 1 && it[0] == "status" }?.get(1))
|
||||||
fun currentParticipants() = tags.firstOrNull { it.size > 1 && it[0] == "current_participants" }?.get(1)
|
fun currentParticipants() = tags.firstOrNull { it.size > 1 && it[0] == "current_participants" }?.get(1)
|
||||||
fun totalParticipants() = tags.firstOrNull { it.size > 1 && it[0] == "total_participants" }?.get(1)
|
fun totalParticipants() = tags.firstOrNull { it.size > 1 && it[0] == "total_participants" }?.get(1)
|
||||||
|
|
||||||
fun participants() = tags.filter { it.size > 1 && it[0] == "p" }.map { Participant(it[1], it.getOrNull(2)) }
|
fun participants() = tags.filter { it.size > 1 && it[0] == "p" }.map { Participant(it[1], it.getOrNull(3)) }
|
||||||
|
|
||||||
fun checkStatus(eventStatus: String?): String? {
|
fun checkStatus(eventStatus: String?): String? {
|
||||||
return if (eventStatus == STATUS_LIVE && createdAt < Date().time / 1000 - (60 * 60 * 8)) { // 2 hours {
|
return if (eventStatus == STATUS_LIVE && createdAt < Date().time / 1000 - (60 * 60 * 8)) { // 2 hours {
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.dal
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.service.model.CommunityPostApprovalEvent
|
||||||
|
|
||||||
|
class CommunityFeedFilter(val note: AddressableNote, val account: Account) : AdditiveFeedFilter<Note>() {
|
||||||
|
override fun feedKey(): String {
|
||||||
|
return account.userProfile().pubkeyHex + "-" + note.idHex
|
||||||
|
}
|
||||||
|
|
||||||
|
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> {
|
||||||
|
return collection
|
||||||
|
.asSequence()
|
||||||
|
.filter { it.event is CommunityPostApprovalEvent } // Only Approvals
|
||||||
|
.filter { it.event?.isTaggedAddressableNote(note.idHex) == true } // Of the given community
|
||||||
|
.mapNotNull { it.replyTo }.flatten() // get approved posts
|
||||||
|
.filter { it.isNewThread() } // check if it is a new thread
|
||||||
|
.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun sort(collection: Set<Note>): List<Note> {
|
||||||
|
return collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.dal
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.model.ParticipantListBuilder
|
||||||
|
import com.vitorpamplona.amethyst.service.model.*
|
||||||
|
|
||||||
|
open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||||
|
override fun feedKey(): String {
|
||||||
|
return account.userProfile().pubkeyHex + "-" + account.defaultDiscoveryFollowList
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun feed(): List<Note> {
|
||||||
|
val allChannelNotes = LocalCache.channels.values.mapNotNull { LocalCache.getNoteIfExists(it.idHex) }
|
||||||
|
|
||||||
|
val notes = innerApplyFilter(allChannelNotes)
|
||||||
|
|
||||||
|
return sort(notes)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun applyFilter(collection: Set<Note>): Set<Note> {
|
||||||
|
return innerApplyFilter(collection)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||||
|
val now = System.currentTimeMillis() / 1000
|
||||||
|
val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS
|
||||||
|
|
||||||
|
val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
|
||||||
|
val followingTagSet = account.selectedTagsFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
|
||||||
|
|
||||||
|
val createEvents = collection.filter { it.event is ChannelCreateEvent }
|
||||||
|
val anyOtherChannelEvent = collection
|
||||||
|
.asSequence()
|
||||||
|
.filter { it.event is IsInPublicChatChannel }
|
||||||
|
.mapNotNull { (it.event as? ChannelMessageEvent)?.channel() }
|
||||||
|
.mapNotNull { LocalCache.checkGetOrCreateNote(it) }
|
||||||
|
.toSet()
|
||||||
|
|
||||||
|
val activities = (createEvents + anyOtherChannelEvent)
|
||||||
|
.asSequence()
|
||||||
|
.filter { it.event is ChannelCreateEvent }
|
||||||
|
.filter { isGlobal || it.author?.pubkeyHex in followingKeySet || it.event?.isTaggedHashes(followingTagSet) == true }
|
||||||
|
.filter { it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true }
|
||||||
|
.filter { (it.createdAt() ?: 0) <= now }
|
||||||
|
.toSet()
|
||||||
|
|
||||||
|
return activities
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun sort(collection: Set<Note>): List<Note> {
|
||||||
|
val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)
|
||||||
|
|
||||||
|
val counter = ParticipantListBuilder()
|
||||||
|
|
||||||
|
return collection.sortedWith(
|
||||||
|
compareBy(
|
||||||
|
{ counter.countFollowsThatParticipateOn(it, followingKeySet) },
|
||||||
|
{ it.createdAt() },
|
||||||
|
{ it.idHex }
|
||||||
|
)
|
||||||
|
).reversed()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.dal
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.model.ParticipantListBuilder
|
||||||
|
import com.vitorpamplona.amethyst.service.model.*
|
||||||
|
|
||||||
|
open class DiscoverCommunityFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||||
|
override fun feedKey(): String {
|
||||||
|
return account.userProfile().pubkeyHex + "-" + account.defaultDiscoveryFollowList
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun feed(): List<Note> {
|
||||||
|
val allNotes = LocalCache.addressables.values
|
||||||
|
|
||||||
|
val notes = innerApplyFilter(allNotes)
|
||||||
|
|
||||||
|
return sort(notes)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun applyFilter(collection: Set<Note>): Set<Note> {
|
||||||
|
return innerApplyFilter(collection)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||||
|
val now = System.currentTimeMillis() / 1000
|
||||||
|
val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS
|
||||||
|
|
||||||
|
val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
|
||||||
|
val followingTagSet = account.selectedTagsFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
|
||||||
|
|
||||||
|
val activities = collection
|
||||||
|
.asSequence()
|
||||||
|
.filter { it.event is CommunityDefinitionEvent }
|
||||||
|
.filter { isGlobal || it.author?.pubkeyHex in followingKeySet || it.event?.isTaggedHashes(followingTagSet) == true }
|
||||||
|
.filter { it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true }
|
||||||
|
.filter { (it.createdAt() ?: 0) <= now }
|
||||||
|
.toSet()
|
||||||
|
|
||||||
|
return activities
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun sort(collection: Set<Note>): List<Note> {
|
||||||
|
val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)
|
||||||
|
val counter = ParticipantListBuilder()
|
||||||
|
|
||||||
|
return collection.sortedWith(
|
||||||
|
compareBy(
|
||||||
|
{ counter.countFollowsThatParticipateOn(it, followingKeySet) },
|
||||||
|
{ it.createdAt() },
|
||||||
|
{ it.idHex }
|
||||||
|
)
|
||||||
|
).reversed()
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
-5
@@ -4,18 +4,20 @@ import com.vitorpamplona.amethyst.model.Account
|
|||||||
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.model.ParticipantListBuilder
|
||||||
import com.vitorpamplona.amethyst.service.model.*
|
import com.vitorpamplona.amethyst.service.model.*
|
||||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent.Companion.STATUS_ENDED
|
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent.Companion.STATUS_ENDED
|
||||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent.Companion.STATUS_LIVE
|
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent.Companion.STATUS_LIVE
|
||||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent.Companion.STATUS_PLANNED
|
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent.Companion.STATUS_PLANNED
|
||||||
|
|
||||||
open class DiscoverFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
open class DiscoverLiveFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||||
override fun feedKey(): String {
|
override fun feedKey(): String {
|
||||||
return account.userProfile().pubkeyHex + "-" + account.defaultDiscoveryFollowList
|
return account.userProfile().pubkeyHex + "-" + account.defaultDiscoveryFollowList
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun feed(): List<Note> {
|
override fun feed(): List<Note> {
|
||||||
val allChannelNotes = LocalCache.channels.values.mapNotNull { LocalCache.getNoteIfExists(it.idHex) }
|
val allChannelNotes =
|
||||||
|
LocalCache.channels.values.mapNotNull { LocalCache.getNoteIfExists(it.idHex) }
|
||||||
val allMessageNotes = LocalCache.channels.values.map { it.notes.values }.flatten()
|
val allMessageNotes = LocalCache.channels.values.map { it.notes.values }.flatten()
|
||||||
|
|
||||||
val notes = innerApplyFilter(allChannelNotes + allMessageNotes)
|
val notes = innerApplyFilter(allChannelNotes + allMessageNotes)
|
||||||
@@ -31,13 +33,19 @@ open class DiscoverFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
|
|||||||
val now = System.currentTimeMillis() / 1000
|
val now = System.currentTimeMillis() / 1000
|
||||||
val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS
|
val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS
|
||||||
|
|
||||||
val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
|
val followingKeySet =
|
||||||
val followingTagSet = account.selectedTagsFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
|
account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
|
||||||
|
val followingTagSet =
|
||||||
|
account.selectedTagsFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
|
||||||
|
|
||||||
val activities = collection
|
val activities = collection
|
||||||
.asSequence()
|
.asSequence()
|
||||||
.filter { it.event is LiveActivitiesEvent }
|
.filter { it.event is LiveActivitiesEvent }
|
||||||
.filter { isGlobal || it.author?.pubkeyHex in followingKeySet || it.event?.isTaggedHashes(followingTagSet) == true }
|
.filter {
|
||||||
|
isGlobal || it.author?.pubkeyHex in followingKeySet || it.event?.isTaggedHashes(
|
||||||
|
followingTagSet
|
||||||
|
) == true
|
||||||
|
}
|
||||||
.filter { it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true }
|
.filter { it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true }
|
||||||
.filter { (it.createdAt() ?: 0) <= now }
|
.filter { (it.createdAt() ?: 0) <= now }
|
||||||
.toSet()
|
.toSet()
|
||||||
@@ -46,9 +54,14 @@ open class DiscoverFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun sort(collection: Set<Note>): List<Note> {
|
override fun sort(collection: Set<Note>): List<Note> {
|
||||||
|
val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)
|
||||||
|
|
||||||
|
val counter = ParticipantListBuilder()
|
||||||
|
|
||||||
return collection.sortedWith(
|
return collection.sortedWith(
|
||||||
compareBy(
|
compareBy(
|
||||||
{ convertStatusToOrder((it.event as? LiveActivitiesEvent)?.status()) },
|
{ convertStatusToOrder((it.event as? LiveActivitiesEvent)?.status()) },
|
||||||
|
{ counter.countFollowsThatParticipateOn(it, followingKeySet) },
|
||||||
{ it.createdAt() },
|
{ it.createdAt() },
|
||||||
{ it.idHex }
|
{ it.idHex }
|
||||||
)
|
)
|
||||||
@@ -6,7 +6,7 @@ import com.vitorpamplona.amethyst.service.OnlineChecker
|
|||||||
import com.vitorpamplona.amethyst.service.model.*
|
import com.vitorpamplona.amethyst.service.model.*
|
||||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent.Companion.STATUS_LIVE
|
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent.Companion.STATUS_LIVE
|
||||||
|
|
||||||
class DiscoverLiveNowFeedFilter(account: Account) : DiscoverFeedFilter(account) {
|
class DiscoverLiveNowFeedFilter(account: Account) : DiscoverLiveFeedFilter(account) {
|
||||||
override fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
override fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||||
val allItems = super.innerApplyFilter(collection)
|
val allItems = super.innerApplyFilter(collection)
|
||||||
|
|
||||||
|
|||||||
@@ -8,19 +8,12 @@ import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
|||||||
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||||
|
|
||||||
object HashtagFeedFilter : AdditiveFeedFilter<Note>() {
|
class HashtagFeedFilter(val tag: String, val account: Account) : AdditiveFeedFilter<Note>() {
|
||||||
lateinit var account: Account
|
|
||||||
var tag: String? = null
|
|
||||||
|
|
||||||
override fun feedKey(): String {
|
override fun feedKey(): String {
|
||||||
return account.userProfile().pubkeyHex + "-" + tag
|
return account.userProfile().pubkeyHex + "-" + tag
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadHashtag(account: Account, tag: String?) {
|
|
||||||
this.account = account
|
|
||||||
this.tag = tag
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun feed(): List<Note> {
|
override fun feed(): List<Note> {
|
||||||
return sort(innerApplyFilter(LocalCache.notes.values))
|
return sort(innerApplyFilter(LocalCache.notes.values))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import com.vitorpamplona.amethyst.model.User
|
|||||||
|
|
||||||
class HiddenAccountsFeedFilter(val account: Account) : FeedFilter<User>() {
|
class HiddenAccountsFeedFilter(val account: Account) : FeedFilter<User>() {
|
||||||
override fun feedKey(): String {
|
override fun feedKey(): String {
|
||||||
return account.userProfile().pubkeyHex + "-" + HashtagFeedFilter.tag
|
return account.userProfile().pubkeyHex
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun feed(): List<User> {
|
override fun feed(): List<User> {
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ import androidx.navigation.compose.composable
|
|||||||
import com.vitorpamplona.amethyst.ui.note.UserReactionsViewModel
|
import com.vitorpamplona.amethyst.ui.note.UserReactionsViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListKnownFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListKnownFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListNewFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListNewFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverChatFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverCommunityFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverLiveFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel
|
||||||
@@ -22,6 +24,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.BookmarkListScreen
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelScreen
|
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.CommunityScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.DiscoverScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.DiscoverScreen
|
||||||
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
|
||||||
@@ -42,7 +45,9 @@ fun AppNavigation(
|
|||||||
knownFeedViewModel: NostrChatroomListKnownFeedViewModel,
|
knownFeedViewModel: NostrChatroomListKnownFeedViewModel,
|
||||||
newFeedViewModel: NostrChatroomListNewFeedViewModel,
|
newFeedViewModel: NostrChatroomListNewFeedViewModel,
|
||||||
videoFeedViewModel: NostrVideoFeedViewModel,
|
videoFeedViewModel: NostrVideoFeedViewModel,
|
||||||
discoveryFeedViewModel: NostrDiscoverFeedViewModel,
|
discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel,
|
||||||
|
discoveryCommunityFeedViewModel: NostrDiscoverCommunityFeedViewModel,
|
||||||
|
discoveryChatFeedViewModel: NostrDiscoverChatFeedViewModel,
|
||||||
notifFeedViewModel: NotificationViewModel,
|
notifFeedViewModel: NotificationViewModel,
|
||||||
userReactionsStatsModel: UserReactionsViewModel,
|
userReactionsStatsModel: UserReactionsViewModel,
|
||||||
|
|
||||||
@@ -109,7 +114,9 @@ fun AppNavigation(
|
|||||||
Route.Discover.let { route ->
|
Route.Discover.let { route ->
|
||||||
composable(route.route, route.arguments, content = {
|
composable(route.route, route.arguments, content = {
|
||||||
DiscoverScreen(
|
DiscoverScreen(
|
||||||
discoveryFeedViewModel = discoveryFeedViewModel,
|
discoveryLiveFeedViewModel = discoveryLiveFeedViewModel,
|
||||||
|
discoveryCommunityFeedViewModel = discoveryCommunityFeedViewModel,
|
||||||
|
discoveryChatFeedViewModel = discoveryChatFeedViewModel,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = nav
|
nav = nav
|
||||||
)
|
)
|
||||||
@@ -169,6 +176,16 @@ fun AppNavigation(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Route.Community.let { route ->
|
||||||
|
composable(route.route, route.arguments, content = {
|
||||||
|
CommunityScreen(
|
||||||
|
aTagHex = it.arguments?.getString("id"),
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
Route.Room.let { route ->
|
Route.Room.let { route ->
|
||||||
composable(route.route, route.arguments, content = {
|
composable(route.route, route.arguments, content = {
|
||||||
ChatroomScreen(
|
ChatroomScreen(
|
||||||
|
|||||||
@@ -582,7 +582,7 @@ fun IconRowRelays(relayViewModel: RelayPoolViewModel, onClick: () -> Unit) {
|
|||||||
|
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(start = 16.dp),
|
modifier = Modifier.padding(start = 16.dp),
|
||||||
text = stringResource(id = R.string.relays),
|
text = stringResource(id = R.string.relay_setup),
|
||||||
fontSize = 18.sp
|
fontSize = 18.sp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -104,6 +104,12 @@ sealed class Route(
|
|||||||
arguments = listOf(navArgument("id") { type = NavType.StringType }).toImmutableList()
|
arguments = listOf(navArgument("id") { type = NavType.StringType }).toImmutableList()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
object Community : Route(
|
||||||
|
route = "Community/{id}",
|
||||||
|
icon = R.drawable.ic_moments,
|
||||||
|
arguments = listOf(navArgument("id") { type = NavType.StringType }).toImmutableList()
|
||||||
|
)
|
||||||
|
|
||||||
object Room : Route(
|
object Room : Route(
|
||||||
route = "Room/{id}",
|
route = "Room/{id}",
|
||||||
icon = R.drawable.ic_moments,
|
icon = R.drawable.ic_moments,
|
||||||
@@ -195,7 +201,7 @@ object DiscoverLatestItem : LatestItem() {
|
|||||||
): Boolean {
|
): Boolean {
|
||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
|
|
||||||
val lastTime = account.loadLastRead(Route.Discover.base)
|
val lastTime = account.loadLastRead(Route.Discover.base + "Live")
|
||||||
|
|
||||||
val newestItem = updateNewestItem(newNotes, account, DiscoverLiveNowFeedFilter(account))
|
val newestItem = updateNewestItem(newNotes, account, DiscoverLiveNowFeedFilter(account))
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,20 @@ package com.vitorpamplona.amethyst.ui.note
|
|||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.Crossfade
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
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.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
@@ -23,20 +28,29 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment.Companion.BottomStart
|
import androidx.compose.ui.Alignment.Companion.BottomStart
|
||||||
|
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||||
import androidx.compose.ui.Alignment.Companion.TopEnd
|
import androidx.compose.ui.Alignment.Companion.TopEnd
|
||||||
import androidx.compose.ui.Modifier
|
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.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.distinctUntilChanged
|
import androidx.lifecycle.distinctUntilChanged
|
||||||
import androidx.lifecycle.map
|
import androidx.lifecycle.map
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
|
import com.vitorpamplona.amethyst.model.Channel
|
||||||
|
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.model.ParticipantListBuilder
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.OnlineChecker
|
import com.vitorpamplona.amethyst.service.OnlineChecker
|
||||||
|
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.CommunityDefinitionEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent
|
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent.Companion.STATUS_ENDED
|
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent.Companion.STATUS_ENDED
|
||||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent.Companion.STATUS_LIVE
|
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent.Companion.STATUS_LIVE
|
||||||
@@ -48,9 +62,13 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.EndedFlag
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LiveFlag
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LiveFlag
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.OfflineFlag
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.OfflineFlag
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.persistentSetOf
|
import kotlinx.collections.immutable.persistentSetOf
|
||||||
@@ -300,7 +318,7 @@ fun InnerChannelCardWithReactions(
|
|||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (String) -> Unit
|
nav: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
Column() {
|
Column(StdPadding) {
|
||||||
RenderNoteRow(
|
RenderNoteRow(
|
||||||
baseNote,
|
baseNote,
|
||||||
accountViewModel,
|
accountViewModel,
|
||||||
@@ -319,6 +337,12 @@ private fun RenderNoteRow(
|
|||||||
is LiveActivitiesEvent -> {
|
is LiveActivitiesEvent -> {
|
||||||
RenderLiveActivityThumb(baseNote, accountViewModel, nav)
|
RenderLiveActivityThumb(baseNote, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
|
is CommunityDefinitionEvent -> {
|
||||||
|
RenderCommunitiesThumb(baseNote, accountViewModel, nav)
|
||||||
|
}
|
||||||
|
is ChannelCreateEvent -> {
|
||||||
|
RenderChannelThumb(baseNote, accountViewModel, nav)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,6 +363,7 @@ fun RenderLiveActivityThumb(baseNote: Note, accountViewModel: AccountViewModel,
|
|||||||
val content = remember(eventUpdates) { noteEvent.summary() }
|
val content = remember(eventUpdates) { noteEvent.summary() }
|
||||||
val participants = remember(eventUpdates) { noteEvent.participants() }
|
val participants = remember(eventUpdates) { noteEvent.participants() }
|
||||||
val status = remember(eventUpdates) { noteEvent.status() }
|
val status = remember(eventUpdates) { noteEvent.status() }
|
||||||
|
val starts = remember(eventUpdates) { noteEvent.starts() }
|
||||||
|
|
||||||
var isOnline by remember { mutableStateOf(false) }
|
var isOnline by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
@@ -359,29 +384,31 @@ fun RenderLiveActivityThumb(baseNote: Note, accountViewModel: AccountViewModel,
|
|||||||
|
|
||||||
LaunchedEffect(key1 = eventUpdates) {
|
LaunchedEffect(key1 = eventUpdates) {
|
||||||
launch(Dispatchers.IO) {
|
launch(Dispatchers.IO) {
|
||||||
val channel = LocalCache.getChannelIfExists(baseNote.idHex)
|
val hosts = participants.mapNotNull { part ->
|
||||||
val repliesByFollows = channel?.notes?.mapNotNull { it.value.author?.pubkeyHex }?.toSet() ?: emptySet()
|
if (part.key != baseNote.author?.pubkeyHex) {
|
||||||
|
LocalCache.checkGetOrCreateUser(part.key)
|
||||||
val zappers = baseNote.publicZapAuthors()
|
} else {
|
||||||
val likeAuthors = baseNote.reactionAuthors()
|
null
|
||||||
|
|
||||||
val allParticipants = (repliesByFollows + zappers + likeAuthors).filter { accountViewModel.isFollowing(it) }
|
|
||||||
|
|
||||||
val newParticipantUsers = (
|
|
||||||
participants.mapNotNull { part ->
|
|
||||||
if (part.key != baseNote.author?.pubkeyHex) {
|
|
||||||
LocalCache.checkGetOrCreateUser(part.key)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
} + allParticipants.mapNotNull {
|
|
||||||
if (it != baseNote.author?.pubkeyHex) {
|
|
||||||
LocalCache.checkGetOrCreateUser(it)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
).toImmutableList()
|
}
|
||||||
|
|
||||||
|
val hostsAuthor = hosts + (
|
||||||
|
baseNote.author?.let {
|
||||||
|
listOf(it)
|
||||||
|
} ?: emptyList<User>()
|
||||||
|
)
|
||||||
|
|
||||||
|
val followingKeySet = accountViewModel.account.selectedUsersFollowList(accountViewModel.account.defaultDiscoveryFollowList)
|
||||||
|
val allParticipants = ParticipantListBuilder().followsThatParticipateOn(baseNote, followingKeySet).minus(hostsAuthor)
|
||||||
|
|
||||||
|
val newParticipantUsers = if (followingKeySet == null) {
|
||||||
|
val allFollows = accountViewModel.account.selectedUsersFollowList(KIND3_FOLLOWS)
|
||||||
|
val followingParticipants = ParticipantListBuilder().followsThatParticipateOn(baseNote, allFollows).minus(hostsAuthor)
|
||||||
|
|
||||||
|
(hosts + followingParticipants + (allParticipants - followingParticipants)).toImmutableList()
|
||||||
|
} else {
|
||||||
|
(hosts + allParticipants).toImmutableList()
|
||||||
|
}
|
||||||
|
|
||||||
if (!equalImmutableLists(newParticipantUsers, participantUsers)) {
|
if (!equalImmutableLists(newParticipantUsers, participantUsers)) {
|
||||||
participantUsers = newParticipantUsers
|
participantUsers = newParticipantUsers
|
||||||
@@ -429,7 +456,7 @@ fun RenderLiveActivityThumb(baseNote: Note, accountViewModel: AccountViewModel,
|
|||||||
EndedFlag()
|
EndedFlag()
|
||||||
}
|
}
|
||||||
STATUS_PLANNED -> {
|
STATUS_PLANNED -> {
|
||||||
ScheduledFlag()
|
ScheduledFlag(starts)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
EndedFlag()
|
EndedFlag()
|
||||||
@@ -438,13 +465,13 @@ fun RenderLiveActivityThumb(baseNote: Note, accountViewModel: AccountViewModel,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(Modifier.padding(10.dp).align(BottomStart)) {
|
Box(
|
||||||
|
Modifier
|
||||||
|
.padding(10.dp)
|
||||||
|
.align(BottomStart)
|
||||||
|
) {
|
||||||
if (participantUsers.isNotEmpty()) {
|
if (participantUsers.isNotEmpty()) {
|
||||||
FlowRow() {
|
Gallery(participantUsers, accountViewModel)
|
||||||
participantUsers.forEach {
|
|
||||||
ClickableUserPicture(it, Size35dp, accountViewModel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -463,6 +490,244 @@ fun RenderLiveActivityThumb(baseNote: Note, accountViewModel: AccountViewModel,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
fun RenderCommunitiesThumb(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||||
|
val noteEvent = baseNote.event as? CommunityDefinitionEvent ?: return
|
||||||
|
|
||||||
|
val eventUpdates by baseNote.live().metadata.observeAsState()
|
||||||
|
|
||||||
|
val name = remember(eventUpdates) { noteEvent.dTag() }
|
||||||
|
val description = remember(eventUpdates) { noteEvent.description() }
|
||||||
|
val cover by remember(eventUpdates) {
|
||||||
|
derivedStateOf {
|
||||||
|
noteEvent.image()?.ifBlank { null }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val moderators = remember(eventUpdates) { noteEvent.moderators() }
|
||||||
|
|
||||||
|
var participantUsers by remember {
|
||||||
|
mutableStateOf<ImmutableList<User>>(
|
||||||
|
persistentListOf()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = eventUpdates) {
|
||||||
|
launch(Dispatchers.IO) {
|
||||||
|
val hosts = moderators.mapNotNull { part ->
|
||||||
|
if (part.key != baseNote.author?.pubkeyHex) {
|
||||||
|
LocalCache.checkGetOrCreateUser(part.key)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val followingKeySet = accountViewModel.account.selectedUsersFollowList(accountViewModel.account.defaultDiscoveryFollowList)
|
||||||
|
val allParticipants = ParticipantListBuilder().followsThatParticipateOn(baseNote, followingKeySet).minus(hosts)
|
||||||
|
|
||||||
|
val newParticipantUsers = if (followingKeySet == null) {
|
||||||
|
val allFollows = accountViewModel.account.selectedUsersFollowList(KIND3_FOLLOWS)
|
||||||
|
val followingParticipants = ParticipantListBuilder().followsThatParticipateOn(baseNote, allFollows).minus(hosts)
|
||||||
|
|
||||||
|
(hosts + followingParticipants + (allParticipants - followingParticipants)).toImmutableList()
|
||||||
|
} else {
|
||||||
|
(hosts + allParticipants).toImmutableList()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!equalImmutableLists(newParticipantUsers, participantUsers)) {
|
||||||
|
participantUsers = newParticipantUsers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(Modifier.fillMaxWidth()) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(0.3f)
|
||||||
|
.aspectRatio(ratio = 1f)
|
||||||
|
) {
|
||||||
|
cover?.let {
|
||||||
|
Box(contentAlignment = BottomStart) {
|
||||||
|
AsyncImage(
|
||||||
|
model = it,
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clip(QuoteBorder)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} ?: run {
|
||||||
|
baseNote.author?.let {
|
||||||
|
DisplayAuthorBanner(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = DoubleHorzSpacer)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Row() {
|
||||||
|
Text(
|
||||||
|
text = name,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
description?.let {
|
||||||
|
Spacer(modifier = StdVertSpacer)
|
||||||
|
Row() {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
color = MaterialTheme.colors.placeholderText,
|
||||||
|
maxLines = 3,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (participantUsers.isNotEmpty()) {
|
||||||
|
Spacer(modifier = StdVertSpacer)
|
||||||
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Gallery(participantUsers, accountViewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RenderChannelThumb(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||||
|
val noteEvent = baseNote.event as? ChannelCreateEvent ?: return
|
||||||
|
|
||||||
|
LoadChannel(baseChannelHex = baseNote.idHex) {
|
||||||
|
RenderChannelThumb(baseNote = baseNote, channel = it, accountViewModel, nav)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RenderChannelThumb(baseNote: Note, channel: Channel, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||||
|
val channelUpdates by channel.live.observeAsState()
|
||||||
|
|
||||||
|
val name = remember(channelUpdates) { channel.toBestDisplayName() }
|
||||||
|
val description = remember(channelUpdates) { channel.summary() }
|
||||||
|
val cover by remember(channelUpdates) {
|
||||||
|
derivedStateOf {
|
||||||
|
channel.profilePicture()?.ifBlank { null }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var participantUsers by remember(baseNote) {
|
||||||
|
mutableStateOf<ImmutableList<User>>(
|
||||||
|
persistentListOf()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = channelUpdates) {
|
||||||
|
launch(Dispatchers.IO) {
|
||||||
|
val followingKeySet = accountViewModel.account.selectedUsersFollowList(accountViewModel.account.defaultDiscoveryFollowList)
|
||||||
|
val allParticipants = ParticipantListBuilder().followsThatParticipateOn(baseNote, followingKeySet).toImmutableList()
|
||||||
|
|
||||||
|
val newParticipantUsers = if (followingKeySet == null) {
|
||||||
|
val allFollows = accountViewModel.account.selectedUsersFollowList(KIND3_FOLLOWS)
|
||||||
|
val followingParticipants = ParticipantListBuilder().followsThatParticipateOn(baseNote, allFollows).toList()
|
||||||
|
|
||||||
|
(followingParticipants + (allParticipants - followingParticipants)).toImmutableList()
|
||||||
|
} else {
|
||||||
|
allParticipants.toImmutableList()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!equalImmutableLists(newParticipantUsers, participantUsers)) {
|
||||||
|
participantUsers = newParticipantUsers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(Modifier.fillMaxWidth()) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(0.3f)
|
||||||
|
.aspectRatio(ratio = 1f)
|
||||||
|
) {
|
||||||
|
cover?.let {
|
||||||
|
Box(contentAlignment = BottomStart) {
|
||||||
|
AsyncImage(
|
||||||
|
model = it,
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clip(QuoteBorder)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} ?: run {
|
||||||
|
baseNote.author?.let {
|
||||||
|
DisplayAuthorBanner(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = DoubleHorzSpacer)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth().fillMaxHeight()
|
||||||
|
) {
|
||||||
|
Row() {
|
||||||
|
Text(
|
||||||
|
text = name,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
description?.let {
|
||||||
|
Spacer(modifier = StdVertSpacer)
|
||||||
|
Row() {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
color = MaterialTheme.colors.placeholderText,
|
||||||
|
maxLines = 3,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (participantUsers.isNotEmpty()) {
|
||||||
|
Spacer(modifier = StdVertSpacer)
|
||||||
|
Row() {
|
||||||
|
Gallery(participantUsers, accountViewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
fun Gallery(users: List<User>, accountViewModel: AccountViewModel) {
|
||||||
|
FlowRow(verticalAlignment = CenterVertically) {
|
||||||
|
users.take(6).forEach {
|
||||||
|
ClickableUserPicture(it, Size35dp, accountViewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (users.size > 6) {
|
||||||
|
Text(
|
||||||
|
text = remember(users) { " + " + (showCount(users.size - 6)).toString() },
|
||||||
|
fontSize = 13.sp,
|
||||||
|
color = MaterialTheme.colors.onSurface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DisplayAuthorBanner(author: User) {
|
fun DisplayAuthorBanner(author: User) {
|
||||||
val picture by author.live().metadata.map {
|
val picture by author.live().metadata.map {
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ import com.vitorpamplona.amethyst.model.Note
|
|||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.model.UserMetadata
|
import com.vitorpamplona.amethyst.model.UserMetadata
|
||||||
import com.vitorpamplona.amethyst.service.OnlineChecker
|
import com.vitorpamplona.amethyst.service.OnlineChecker
|
||||||
|
import com.vitorpamplona.amethyst.service.model.ATag
|
||||||
import com.vitorpamplona.amethyst.service.model.AppDefinitionEvent
|
import com.vitorpamplona.amethyst.service.model.AppDefinitionEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.AudioTrackEvent
|
import com.vitorpamplona.amethyst.service.model.AudioTrackEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent
|
import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent
|
||||||
@@ -102,6 +103,7 @@ import com.vitorpamplona.amethyst.service.model.BaseTextNoteEvent
|
|||||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.CommunityDefinitionEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.FileHeaderEvent
|
import com.vitorpamplona.amethyst.service.model.FileHeaderEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.FileStorageHeaderEvent
|
import com.vitorpamplona.amethyst.service.model.FileStorageHeaderEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
|
import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
|
||||||
@@ -147,6 +149,8 @@ import com.vitorpamplona.amethyst.ui.components.imageExtensions
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists
|
import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.JoinCommunityButton
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LeaveCommunityButton
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LiveFlag
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LiveFlag
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag
|
||||||
@@ -156,6 +160,7 @@ import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||||
import com.vitorpamplona.amethyst.ui.theme.HalfPadding
|
import com.vitorpamplona.amethyst.ui.theme.HalfPadding
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
|
||||||
import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer
|
import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||||
import com.vitorpamplona.amethyst.ui.theme.ShowMoreRelaysButtonBoxModifer
|
import com.vitorpamplona.amethyst.ui.theme.ShowMoreRelaysButtonBoxModifer
|
||||||
@@ -170,6 +175,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdStartPadding
|
import com.vitorpamplona.amethyst.ui.theme.StdStartPadding
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.UserNameMaxRowHeight
|
import com.vitorpamplona.amethyst.ui.theme.UserNameMaxRowHeight
|
||||||
@@ -432,6 +438,12 @@ fun NormalNote(
|
|||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = nav
|
nav = nav
|
||||||
)
|
)
|
||||||
|
is CommunityDefinitionEvent -> CommunityHeader(
|
||||||
|
baseNote = baseNote,
|
||||||
|
showBottomDiviser = true,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav
|
||||||
|
)
|
||||||
is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote)
|
is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote)
|
||||||
is FileHeaderEvent -> FileHeaderDisplay(baseNote)
|
is FileHeaderEvent -> FileHeaderDisplay(baseNote)
|
||||||
is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote)
|
is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote)
|
||||||
@@ -456,6 +468,113 @@ fun NormalNote(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CommunityHeader(
|
||||||
|
baseNote: Note,
|
||||||
|
showBottomDiviser: Boolean,
|
||||||
|
modifier: Modifier = StdPadding,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: (String) -> Unit
|
||||||
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
scope.launch {
|
||||||
|
nav("Community/${baseNote.idHex}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
val channelState by baseNote.live().metadata.observeAsState()
|
||||||
|
val noteEvent = remember(channelState) { channelState?.note?.event as? CommunityDefinitionEvent } ?: return
|
||||||
|
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
noteEvent.image()?.let {
|
||||||
|
RobohashAsyncImageProxy(
|
||||||
|
robot = baseNote.idHex,
|
||||||
|
model = it,
|
||||||
|
contentDescription = stringResource(R.string.profile_image),
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier.padding(start = 10.dp)
|
||||||
|
.width(Size35dp)
|
||||||
|
.height(Size35dp)
|
||||||
|
.clip(shape = CircleShape)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 10.dp)
|
||||||
|
.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = remember(channelState) { noteEvent.dTag() },
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val summary = remember(channelState) {
|
||||||
|
noteEvent.description()?.ifBlank { null }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (summary != null) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = summary,
|
||||||
|
color = MaterialTheme.colors.placeholderText,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
fontSize = 12.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(Size35dp)
|
||||||
|
.padding(start = 5.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
CommunityActionOptions(baseNote, accountViewModel, nav)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showBottomDiviser) {
|
||||||
|
Divider(
|
||||||
|
thickness = 0.25.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CommunityActionOptions(
|
||||||
|
note: Note,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: (String) -> Unit
|
||||||
|
) {
|
||||||
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||||
|
val isFollowing by remember(accountState) {
|
||||||
|
derivedStateOf {
|
||||||
|
accountState?.account?.followingCommunities?.contains(note.idHex) ?: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFollowing) {
|
||||||
|
LeaveCommunityButton(accountViewModel, note, nav)
|
||||||
|
} else {
|
||||||
|
JoinCommunityButton(accountViewModel, note, nav)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun CheckNewAndRenderNote(
|
private fun CheckNewAndRenderNote(
|
||||||
baseNote: Note,
|
baseNote: Note,
|
||||||
@@ -821,6 +940,8 @@ fun routeFor(note: Note, loggedIn: User): String? {
|
|||||||
}
|
}
|
||||||
} else if (noteEvent is PrivateDmEvent) {
|
} else if (noteEvent is PrivateDmEvent) {
|
||||||
return "Room/${noteEvent.talkingWith(loggedIn.pubkeyHex)}"
|
return "Room/${noteEvent.talkingWith(loggedIn.pubkeyHex)}"
|
||||||
|
} else if (noteEvent is CommunityDefinitionEvent) {
|
||||||
|
return "Community/${note.idHex}"
|
||||||
} else {
|
} else {
|
||||||
return "Note/${note.idHex}"
|
return "Note/${note.idHex}"
|
||||||
}
|
}
|
||||||
@@ -849,7 +970,7 @@ fun RenderTextEvent(
|
|||||||
val subject = (note.event as? TextNoteEvent)?.subject()?.ifEmpty { null }
|
val subject = (note.event as? TextNoteEvent)?.subject()?.ifEmpty { null }
|
||||||
val body = accountViewModel.decrypt(note)
|
val body = accountViewModel.decrypt(note)
|
||||||
|
|
||||||
if (subject != null) {
|
if (!subject.isNullOrBlank() && body?.split("\n")?.get(0)?.contains(subject) == false) {
|
||||||
"## $subject\n$body"
|
"## $subject\n$body"
|
||||||
} else {
|
} else {
|
||||||
body
|
body
|
||||||
@@ -1716,7 +1837,7 @@ private fun ReplyRow(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showReply) {
|
if (showReply) {
|
||||||
val replyingDirectlyTo = remember { note.replyTo?.lastOrNull() }
|
val replyingDirectlyTo = remember { note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.kind } }
|
||||||
if (replyingDirectlyTo != null && unPackReply) {
|
if (replyingDirectlyTo != null && unPackReply) {
|
||||||
ReplyNoteComposition(replyingDirectlyTo, backgroundColor, accountViewModel, nav)
|
ReplyNoteComposition(replyingDirectlyTo, backgroundColor, accountViewModel, nav)
|
||||||
Spacer(modifier = StdVertSpacer)
|
Spacer(modifier = StdVertSpacer)
|
||||||
@@ -1818,12 +1939,18 @@ fun FirstUserInfoRow(
|
|||||||
nav: (String) -> Unit
|
nav: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
Row(verticalAlignment = CenterVertically, modifier = remember { UserNameRowHeight }) {
|
Row(verticalAlignment = CenterVertically, modifier = remember { UserNameRowHeight }) {
|
||||||
val isRepost by remember {
|
val isRepost by remember(baseNote) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent
|
baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val isCommunityPost by remember(baseNote) {
|
||||||
|
derivedStateOf {
|
||||||
|
baseNote.event?.isTaggedAddressableKind(CommunityDefinitionEvent.kind) == true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (showAuthorPicture) {
|
if (showAuthorPicture) {
|
||||||
NoteAuthorPicture(baseNote, nav, accountViewModel, Size25dp)
|
NoteAuthorPicture(baseNote, nav, accountViewModel, Size25dp)
|
||||||
Spacer(HalfPadding)
|
Spacer(HalfPadding)
|
||||||
@@ -1834,6 +1961,8 @@ fun FirstUserInfoRow(
|
|||||||
|
|
||||||
if (isRepost) {
|
if (isRepost) {
|
||||||
BoostedMark()
|
BoostedMark()
|
||||||
|
} else if (isCommunityPost) {
|
||||||
|
DisplayFollowingCommunityInPost(baseNote, accountViewModel, nav)
|
||||||
} else {
|
} else {
|
||||||
DisplayFollowingHashtagsInPost(baseNote, accountViewModel, nav)
|
DisplayFollowingHashtagsInPost(baseNote, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
@@ -2142,6 +2271,19 @@ private fun LoadAndDisplayUser(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DisplayFollowingCommunityInPost(
|
||||||
|
baseNote: Note,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: (String) -> Unit
|
||||||
|
) {
|
||||||
|
Column(HalfStartPadding) {
|
||||||
|
Row(verticalAlignment = CenterVertically) {
|
||||||
|
DisplayCommunity(baseNote, nav)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DisplayFollowingHashtagsInPost(
|
fun DisplayFollowingHashtagsInPost(
|
||||||
baseNote: Note,
|
baseNote: Note,
|
||||||
@@ -2192,6 +2334,37 @@ private fun DisplayTagList(firstTag: String, nav: (String) -> Unit) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DisplayCommunity(note: Note, nav: (String) -> Unit) {
|
||||||
|
val communityTag = remember(note) {
|
||||||
|
note.event?.getTagOfAddressableKind(CommunityDefinitionEvent.kind)
|
||||||
|
} ?: return
|
||||||
|
|
||||||
|
val displayTag = remember(note) { AnnotatedString(getCommunityShortName(communityTag)) }
|
||||||
|
val route = remember(note) { "Community/${communityTag.toTag()}" }
|
||||||
|
|
||||||
|
ClickableText(
|
||||||
|
text = displayTag,
|
||||||
|
onClick = { nav(route) },
|
||||||
|
style = LocalTextStyle.current.copy(
|
||||||
|
color = MaterialTheme.colors.primary.copy(
|
||||||
|
alpha = 0.52f
|
||||||
|
)
|
||||||
|
),
|
||||||
|
maxLines = 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCommunityShortName(communityTag: ATag): String {
|
||||||
|
val name = if (communityTag.dTag.length > 10) {
|
||||||
|
communityTag.dTag.take(10) + "..."
|
||||||
|
} else {
|
||||||
|
communityTag.dTag.take(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
return "/c/$name"
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun DisplayUncitedHashtags(
|
fun DisplayUncitedHashtags(
|
||||||
@@ -2663,6 +2836,7 @@ fun RenderLiveActivityEventInner(baseNote: Note, accountViewModel: AccountViewMo
|
|||||||
val content = remember(eventUpdates) { noteEvent.summary() }
|
val content = remember(eventUpdates) { noteEvent.summary() }
|
||||||
val participants = remember(eventUpdates) { noteEvent.participants() }
|
val participants = remember(eventUpdates) { noteEvent.participants() }
|
||||||
val status = remember(eventUpdates) { noteEvent.status() }
|
val status = remember(eventUpdates) { noteEvent.status() }
|
||||||
|
val starts = remember(eventUpdates) { noteEvent.starts() }
|
||||||
|
|
||||||
var isOnline by remember { mutableStateOf(false) }
|
var isOnline by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
@@ -2698,7 +2872,7 @@ fun RenderLiveActivityEventInner(baseNote: Note, accountViewModel: AccountViewMo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
STATUS_PLANNED -> {
|
STATUS_PLANNED -> {
|
||||||
ScheduledFlag()
|
ScheduledFlag(starts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ fun timeAgo(mills: Long?, context: Context): String {
|
|||||||
.replace(" hr. ago", context.getString(R.string.h))
|
.replace(" hr. ago", context.getString(R.string.h))
|
||||||
.replace(" min. ago", context.getString(R.string.m))
|
.replace(" min. ago", context.getString(R.string.m))
|
||||||
.replace(" days ago", context.getString(R.string.d))
|
.replace(" days ago", context.getString(R.string.d))
|
||||||
|
.replace("Yesterday", "1" + context.getString(R.string.d))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun timeAgoShort(mills: Long?, context: Context): String {
|
fun timeAgoShort(mills: Long?, context: Context): String {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
import com.vitorpamplona.amethyst.model.Channel
|
import com.vitorpamplona.amethyst.model.Channel
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
@@ -21,7 +22,10 @@ import com.vitorpamplona.amethyst.ui.dal.ChannelFeedFilter
|
|||||||
import com.vitorpamplona.amethyst.ui.dal.ChatroomFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.ChatroomFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.ChatroomListKnownFeedFilter
|
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.DiscoverFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.CommunityFeedFilter
|
||||||
|
import com.vitorpamplona.amethyst.ui.dal.DiscoverChatFeedFilter
|
||||||
|
import com.vitorpamplona.amethyst.ui.dal.DiscoverCommunityFeedFilter
|
||||||
|
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.HashtagFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.HashtagFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.HomeConversationsFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.HomeConversationsFeedFilter
|
||||||
@@ -66,10 +70,26 @@ class NostrVideoFeedViewModel(val account: Account) : FeedViewModel(VideoFeedFil
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NostrDiscoverFeedViewModel(val account: Account) : FeedViewModel(DiscoverFeedFilter(account)) {
|
class NostrDiscoverLiveFeedViewModel(val account: Account) : FeedViewModel(DiscoverLiveFeedFilter(account)) {
|
||||||
class Factory(val account: Account) : ViewModelProvider.Factory {
|
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||||
override fun <NostrDiscoverFeedViewModel : ViewModel> create(modelClass: Class<NostrDiscoverFeedViewModel>): NostrDiscoverFeedViewModel {
|
override fun <NostrDiscoverLiveFeedViewModel : ViewModel> create(modelClass: Class<NostrDiscoverLiveFeedViewModel>): NostrDiscoverLiveFeedViewModel {
|
||||||
return NostrDiscoverFeedViewModel(account) as NostrDiscoverFeedViewModel
|
return NostrDiscoverLiveFeedViewModel(account) as NostrDiscoverLiveFeedViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NostrDiscoverCommunityFeedViewModel(val account: Account) : FeedViewModel(DiscoverCommunityFeedFilter(account)) {
|
||||||
|
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||||
|
override fun <NostrDiscoverCommunityFeedViewModel : ViewModel> create(modelClass: Class<NostrDiscoverCommunityFeedViewModel>): NostrDiscoverCommunityFeedViewModel {
|
||||||
|
return NostrDiscoverCommunityFeedViewModel(account) as NostrDiscoverCommunityFeedViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NostrDiscoverChatFeedViewModel(val account: Account) : FeedViewModel(DiscoverChatFeedFilter(account)) {
|
||||||
|
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||||
|
override fun <NostrDiscoverChatFeedViewModel : ViewModel> create(modelClass: Class<NostrDiscoverChatFeedViewModel>): NostrDiscoverChatFeedViewModel {
|
||||||
|
return NostrDiscoverChatFeedViewModel(account) as NostrDiscoverChatFeedViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,7 +117,21 @@ class NostrUserProfileConversationsFeedViewModel(val user: User, val account: Ac
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NostrHashtagFeedViewModel : FeedViewModel(HashtagFeedFilter)
|
class NostrHashtagFeedViewModel(val hashtag: String, val account: Account) : FeedViewModel(HashtagFeedFilter(hashtag, account)) {
|
||||||
|
class Factory(val hashtag: String, val account: Account) : ViewModelProvider.Factory {
|
||||||
|
override fun <NostrHashtagFeedViewModel : ViewModel> create(modelClass: Class<NostrHashtagFeedViewModel>): NostrHashtagFeedViewModel {
|
||||||
|
return NostrHashtagFeedViewModel(hashtag, account) as NostrHashtagFeedViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NostrCommunityFeedViewModel(val note: AddressableNote, val account: Account) : FeedViewModel(CommunityFeedFilter(note, account)) {
|
||||||
|
class Factory(val note: AddressableNote, val account: Account) : ViewModelProvider.Factory {
|
||||||
|
override fun <NostrCommunityFeedViewModel : ViewModel> create(modelClass: Class<NostrCommunityFeedViewModel>): NostrCommunityFeedViewModel {
|
||||||
|
return NostrCommunityFeedViewModel(note, account) as NostrCommunityFeedViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class NostrUserProfileReportFeedViewModel(val user: User) : FeedViewModel(UserProfileReportsFeedFilter(user)) {
|
class NostrUserProfileReportFeedViewModel(val user: User) : FeedViewModel(UserProfileReportsFeedFilter(user)) {
|
||||||
class Factory(val user: User) : ViewModelProvider.Factory {
|
class Factory(val user: User) : ViewModelProvider.Factory {
|
||||||
|
|||||||
@@ -20,10 +20,15 @@ object ScrollStateKeys {
|
|||||||
const val DISCOVER_SCREEN = "Discover"
|
const val DISCOVER_SCREEN = "Discover"
|
||||||
val HOME_FOLLOWS = Route.Home.base + "Follows"
|
val HOME_FOLLOWS = Route.Home.base + "Follows"
|
||||||
val HOME_REPLIES = Route.Home.base + "FollowsReplies"
|
val HOME_REPLIES = Route.Home.base + "FollowsReplies"
|
||||||
|
|
||||||
|
val DISCOVER_LIVE = Route.Home.base + "Live"
|
||||||
|
val DISCOVER_COMMUNITY = Route.Home.base + "Communities"
|
||||||
|
val DISCOVER_CHATS = Route.Home.base + "Chats"
|
||||||
}
|
}
|
||||||
|
|
||||||
object PagerStateKeys {
|
object PagerStateKeys {
|
||||||
const val HOME_SCREEN = "Home"
|
const val HOME_SCREEN = "PagerHome"
|
||||||
|
const val DISCOVER_SCREEN = "PagerDiscover"
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -59,6 +60,7 @@ import com.vitorpamplona.amethyst.service.model.AudioTrackEvent
|
|||||||
import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
|
import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.CommunityDefinitionEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.FileHeaderEvent
|
import com.vitorpamplona.amethyst.service.model.FileHeaderEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.FileStorageHeaderEvent
|
import com.vitorpamplona.amethyst.service.model.FileStorageHeaderEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
|
import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
|
||||||
@@ -295,7 +297,17 @@ fun NoteMaster(
|
|||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
NoteUsernameDisplay(baseNote, Modifier.weight(1f))
|
NoteUsernameDisplay(baseNote, Modifier.weight(1f))
|
||||||
|
|
||||||
DisplayFollowingHashtagsInPost(baseNote, accountViewModel, nav)
|
val isCommunityPost by remember(baseNote) {
|
||||||
|
derivedStateOf {
|
||||||
|
baseNote.event?.isTaggedAddressableKind(CommunityDefinitionEvent.kind) == true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCommunityPost) {
|
||||||
|
DisplayFollowingCommunityInPost(baseNote, accountViewModel, nav)
|
||||||
|
} else {
|
||||||
|
DisplayFollowingHashtagsInPost(baseNote, accountViewModel, nav)
|
||||||
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
timeAgo(note.createdAt(), context = context),
|
timeAgo(note.createdAt(), context = context),
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose
|
|||||||
import com.vitorpamplona.amethyst.ui.note.LikeReaction
|
import com.vitorpamplona.amethyst.ui.note.LikeReaction
|
||||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||||
import com.vitorpamplona.amethyst.ui.note.ZapReaction
|
import com.vitorpamplona.amethyst.ui.note.ZapReaction
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.timeAgo
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrChannelFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrChannelFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.RefreshingChatroomFeedView
|
import com.vitorpamplona.amethyst.ui.screen.RefreshingChatroomFeedView
|
||||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||||
@@ -703,9 +704,9 @@ private fun ChannelActionOptions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isFollowing) {
|
if (isFollowing) {
|
||||||
LeaveButton(accountViewModel, channel, nav)
|
LeaveChatButton(accountViewModel, channel, nav)
|
||||||
} else {
|
} else {
|
||||||
JoinButton(accountViewModel, channel, nav)
|
JoinChatButton(accountViewModel, channel, nav)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -784,9 +785,12 @@ fun OfflineFlag() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ScheduledFlag() {
|
fun ScheduledFlag(starts: Long?) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val startsIn = starts?.let { timeAgo(it, context) }
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = R.string.live_stream_planned_tag),
|
text = startsIn ?: stringResource(id = R.string.live_stream_planned_tag),
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
modifier = remember {
|
modifier = remember {
|
||||||
@@ -864,7 +868,7 @@ private fun EditButton(accountViewModel: AccountViewModel, channel: PublicChatCh
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun JoinButton(accountViewModel: AccountViewModel, channel: Channel, nav: (String) -> Unit) {
|
fun JoinChatButton(accountViewModel: AccountViewModel, channel: Channel, nav: (String) -> Unit) {
|
||||||
Button(
|
Button(
|
||||||
modifier = Modifier.padding(horizontal = 3.dp),
|
modifier = Modifier.padding(horizontal = 3.dp),
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -882,7 +886,7 @@ private fun JoinButton(accountViewModel: AccountViewModel, channel: Channel, nav
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun LeaveButton(accountViewModel: AccountViewModel, channel: Channel, nav: (String) -> Unit) {
|
fun LeaveChatButton(accountViewModel: AccountViewModel, channel: Channel, nav: (String) -> Unit) {
|
||||||
Button(
|
Button(
|
||||||
modifier = Modifier.padding(horizontal = 3.dp),
|
modifier = Modifier.padding(horizontal = 3.dp),
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -899,3 +903,39 @@ private fun LeaveButton(accountViewModel: AccountViewModel, channel: Channel, na
|
|||||||
Text(text = stringResource(R.string.leave), color = Color.White)
|
Text(text = stringResource(R.string.leave), color = Color.White)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun JoinCommunityButton(accountViewModel: AccountViewModel, note: Note, nav: (String) -> Unit) {
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.padding(horizontal = 3.dp),
|
||||||
|
onClick = {
|
||||||
|
accountViewModel.account.joinCommunity(note.idHex)
|
||||||
|
},
|
||||||
|
shape = ButtonBorder,
|
||||||
|
colors = ButtonDefaults
|
||||||
|
.buttonColors(
|
||||||
|
backgroundColor = MaterialTheme.colors.primary
|
||||||
|
),
|
||||||
|
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.join), color = Color.White)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LeaveCommunityButton(accountViewModel: AccountViewModel, note: Note, nav: (String) -> Unit) {
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.padding(horizontal = 3.dp),
|
||||||
|
onClick = {
|
||||||
|
accountViewModel.account.leaveCommunity((note.idHex))
|
||||||
|
},
|
||||||
|
shape = ButtonBorder,
|
||||||
|
colors = ButtonDefaults
|
||||||
|
.buttonColors(
|
||||||
|
backgroundColor = MaterialTheme.colors.primary
|
||||||
|
),
|
||||||
|
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.leave), color = Color.White)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.service.NostrCommunityDataSource
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.CommunityHeader
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.NostrCommunityFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CommunityScreen(aTagHex: String?, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||||
|
if (aTagHex == null) return
|
||||||
|
|
||||||
|
var noteDefBase by remember { mutableStateOf<AddressableNote?>(LocalCache.getAddressableNoteIfExists(aTagHex)) }
|
||||||
|
|
||||||
|
if (noteDefBase == null) {
|
||||||
|
LaunchedEffect(aTagHex) {
|
||||||
|
// waits to resolve.
|
||||||
|
launch(Dispatchers.IO) {
|
||||||
|
val newNote = LocalCache.checkGetOrCreateAddressableNote(aTagHex)
|
||||||
|
if (newNote != noteDefBase) {
|
||||||
|
noteDefBase = newNote
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
noteDefBase?.let {
|
||||||
|
PrepareViewModelsCommunityScreen(
|
||||||
|
note = it,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PrepareViewModelsCommunityScreen(note: AddressableNote, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||||
|
val followsFeedViewModel: NostrCommunityFeedViewModel = viewModel(
|
||||||
|
key = note.idHex + "CommunityFeedViewModel",
|
||||||
|
factory = NostrCommunityFeedViewModel.Factory(
|
||||||
|
note,
|
||||||
|
accountViewModel.account
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
CommunityScreen(note, followsFeedViewModel, accountViewModel, nav)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CommunityScreen(note: AddressableNote, feedViewModel: NostrCommunityFeedViewModel, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||||
|
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||||
|
|
||||||
|
NostrCommunityDataSource.loadCommunity(note)
|
||||||
|
|
||||||
|
LaunchedEffect(note) {
|
||||||
|
feedViewModel.invalidateData()
|
||||||
|
}
|
||||||
|
|
||||||
|
DisposableEffect(accountViewModel) {
|
||||||
|
val observer = LifecycleEventObserver { _, event ->
|
||||||
|
if (event == Lifecycle.Event.ON_RESUME) {
|
||||||
|
println("Community Start")
|
||||||
|
NostrCommunityDataSource.start()
|
||||||
|
feedViewModel.invalidateData()
|
||||||
|
}
|
||||||
|
if (event == Lifecycle.Event.ON_PAUSE) {
|
||||||
|
println("Community Stop")
|
||||||
|
NostrCommunityDataSource.loadCommunity(null)
|
||||||
|
NostrCommunityDataSource.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lifeCycleOwner.lifecycle.addObserver(observer)
|
||||||
|
onDispose {
|
||||||
|
lifeCycleOwner.lifecycle.removeObserver(observer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(Modifier.fillMaxHeight()) {
|
||||||
|
CommunityHeader(baseNote = note, showBottomDiviser = true, accountViewModel = accountViewModel, nav = nav)
|
||||||
|
RefresheableFeedView(
|
||||||
|
feedViewModel,
|
||||||
|
null,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,25 +5,36 @@ import androidx.compose.animation.core.tween
|
|||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.PagerState
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Tab
|
||||||
|
import androidx.compose.material.TabRow
|
||||||
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.service.NostrDiscoveryDataSource
|
import com.vitorpamplona.amethyst.service.NostrDiscoveryDataSource
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||||
import com.vitorpamplona.amethyst.ui.note.ChannelCardCompose
|
import com.vitorpamplona.amethyst.ui.note.ChannelCardCompose
|
||||||
@@ -32,20 +43,37 @@ import com.vitorpamplona.amethyst.ui.screen.FeedError
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.FeedState
|
import com.vitorpamplona.amethyst.ui.screen.FeedState
|
||||||
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.LoadingFeed
|
import com.vitorpamplona.amethyst.ui.screen.LoadingFeed
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverChatFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverCommunityFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverLiveFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.PagerStateKeys
|
||||||
import com.vitorpamplona.amethyst.ui.screen.RefresheableView
|
import com.vitorpamplona.amethyst.ui.screen.RefresheableView
|
||||||
import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState
|
import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState
|
||||||
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
|
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.rememberForeverPagerState
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun DiscoverScreen(
|
fun DiscoverScreen(
|
||||||
discoveryFeedViewModel: NostrDiscoverFeedViewModel,
|
discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel,
|
||||||
|
discoveryCommunityFeedViewModel: NostrDiscoverCommunityFeedViewModel,
|
||||||
|
discoveryChatFeedViewModel: NostrDiscoverChatFeedViewModel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (String) -> Unit
|
nav: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||||
|
|
||||||
WatchAccountForDiscoveryScreen(discoveryViewModel = discoveryFeedViewModel, accountViewModel = accountViewModel)
|
val pagerState = rememberForeverPagerState(key = PagerStateKeys.DISCOVER_SCREEN)
|
||||||
|
|
||||||
|
WatchAccountForDiscoveryScreen(
|
||||||
|
discoveryLiveFeedViewModel = discoveryLiveFeedViewModel,
|
||||||
|
discoveryCommunityFeedViewModel = discoveryCommunityFeedViewModel,
|
||||||
|
discoveryChatFeedViewModel = discoveryChatFeedViewModel,
|
||||||
|
accountViewModel = accountViewModel
|
||||||
|
)
|
||||||
|
|
||||||
DisposableEffect(accountViewModel) {
|
DisposableEffect(accountViewModel) {
|
||||||
val observer = LifecycleEventObserver { _, event ->
|
val observer = LifecycleEventObserver { _, event ->
|
||||||
@@ -53,10 +81,6 @@ fun DiscoverScreen(
|
|||||||
println("Discovery Start")
|
println("Discovery Start")
|
||||||
NostrDiscoveryDataSource.start()
|
NostrDiscoveryDataSource.start()
|
||||||
}
|
}
|
||||||
if (event == Lifecycle.Event.ON_PAUSE) {
|
|
||||||
println("Discovery Stop")
|
|
||||||
NostrDiscoveryDataSource.stop()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lifeCycleOwner.lifecycle.addObserver(observer)
|
lifeCycleOwner.lifecycle.addObserver(observer)
|
||||||
@@ -65,14 +89,56 @@ fun DiscoverScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val tabs by remember(discoveryLiveFeedViewModel, discoveryCommunityFeedViewModel, discoveryChatFeedViewModel) {
|
||||||
|
mutableStateOf(
|
||||||
|
listOf(
|
||||||
|
TabItem(R.string.discover_live, discoveryLiveFeedViewModel, Route.Discover.base + "Live", ScrollStateKeys.DISCOVER_LIVE),
|
||||||
|
TabItem(R.string.discover_community, discoveryCommunityFeedViewModel, Route.Discover.base + "Community", ScrollStateKeys.DISCOVER_COMMUNITY),
|
||||||
|
TabItem(R.string.discover_chat, discoveryChatFeedViewModel, Route.Discover.base + "Chats", ScrollStateKeys.DISCOVER_CHATS)
|
||||||
|
).toImmutableList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Column(Modifier.fillMaxHeight()) {
|
Column(Modifier.fillMaxHeight()) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(vertical = 0.dp)
|
modifier = Modifier.padding(vertical = 0.dp)
|
||||||
) {
|
) {
|
||||||
RefresheableView(discoveryFeedViewModel, true) {
|
DiscoverPages(pagerState, tabs, accountViewModel, nav)
|
||||||
SaveableFeedState(discoveryFeedViewModel, scrollStateKey = ScrollStateKeys.DISCOVER_SCREEN) { listState ->
|
}
|
||||||
RenderDiscoverFeed(discoveryFeedViewModel, Route.Discover.base, accountViewModel, listState, nav)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
private fun DiscoverPages(
|
||||||
|
pagerState: PagerState,
|
||||||
|
tabs: ImmutableList<TabItem>,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: (String) -> Unit
|
||||||
|
) {
|
||||||
|
TabRow(
|
||||||
|
backgroundColor = MaterialTheme.colors.background,
|
||||||
|
selectedTabIndex = pagerState.currentPage
|
||||||
|
) {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
tabs.forEachIndexed { index, tab ->
|
||||||
|
Tab(
|
||||||
|
selected = pagerState.currentPage == index,
|
||||||
|
text = {
|
||||||
|
Text(text = stringResource(tab.resource))
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
coroutineScope.launch { pagerState.animateScrollToPage(index) }
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalPager(pageCount = 3, state = pagerState) { page ->
|
||||||
|
RefresheableView(tabs[page].viewModel, true) {
|
||||||
|
SaveableFeedState(tabs[page].viewModel, scrollStateKey = tabs[page].scrollStateKey) { listState ->
|
||||||
|
RenderDiscoverFeed(tabs[page].viewModel, tabs[page].routeForLastRead, accountViewModel, listState, nav)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,12 +189,19 @@ private fun RenderDiscoverFeed(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun WatchAccountForDiscoveryScreen(discoveryViewModel: NostrDiscoverFeedViewModel, accountViewModel: AccountViewModel) {
|
fun WatchAccountForDiscoveryScreen(
|
||||||
|
discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel,
|
||||||
|
discoveryCommunityFeedViewModel: NostrDiscoverCommunityFeedViewModel,
|
||||||
|
discoveryChatFeedViewModel: NostrDiscoverChatFeedViewModel,
|
||||||
|
accountViewModel: AccountViewModel
|
||||||
|
) {
|
||||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||||
|
|
||||||
LaunchedEffect(accountViewModel, accountState?.account?.defaultDiscoveryFollowList) {
|
LaunchedEffect(accountViewModel, accountState?.account?.defaultDiscoveryFollowList) {
|
||||||
NostrDiscoveryDataSource.resetFilters()
|
NostrDiscoveryDataSource.resetFilters()
|
||||||
discoveryViewModel.checkKeysInvalidateDataAndSendToTop()
|
discoveryLiveFeedViewModel.checkKeysInvalidateDataAndSendToTop()
|
||||||
|
discoveryCommunityFeedViewModel.checkKeysInvalidateDataAndSendToTop()
|
||||||
|
discoveryChatFeedViewModel.checkKeysInvalidateDataAndSendToTop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,16 +223,18 @@ private fun DiscoverFeedLoaded(
|
|||||||
) {
|
) {
|
||||||
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item ->
|
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item ->
|
||||||
val defaultModifier = remember {
|
val defaultModifier = remember {
|
||||||
Modifier.padding(10.dp).animateItemPlacement()
|
Modifier.fillMaxWidth().animateItemPlacement()
|
||||||
}
|
}
|
||||||
|
|
||||||
ChannelCardCompose(
|
Row(defaultModifier) {
|
||||||
baseNote = item,
|
ChannelCardCompose(
|
||||||
routeForLastRead = routeForLastRead,
|
baseNote = item,
|
||||||
modifier = defaultModifier,
|
routeForLastRead = routeForLastRead,
|
||||||
accountViewModel = accountViewModel,
|
modifier = Modifier,
|
||||||
nav = nav
|
accountViewModel = accountViewModel,
|
||||||
)
|
nav = nav
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ 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
|
||||||
@@ -29,61 +30,77 @@ import androidx.lifecycle.LifecycleEventObserver
|
|||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.service.NostrHashtagDataSource
|
import com.vitorpamplona.amethyst.service.NostrHashtagDataSource
|
||||||
import com.vitorpamplona.amethyst.ui.dal.HashtagFeedFilter
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrHashtagFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrHashtagFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
|
import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.HalfPadding
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HashtagScreen(tag: String?, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
fun HashtagScreen(tag: String?, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||||
|
if (tag == null) return
|
||||||
|
|
||||||
|
PrepareViewModelsHashtagScreen(tag, accountViewModel, nav)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PrepareViewModelsHashtagScreen(tag: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||||
|
val followsFeedViewModel: NostrHashtagFeedViewModel = viewModel(
|
||||||
|
key = tag + "HashtagFeedViewModel",
|
||||||
|
factory = NostrHashtagFeedViewModel.Factory(
|
||||||
|
tag,
|
||||||
|
accountViewModel.account
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
HashtagScreen(tag, followsFeedViewModel, accountViewModel, nav)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HashtagScreen(tag: String, feedViewModel: NostrHashtagFeedViewModel, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||||
|
|
||||||
if (tag != null) {
|
NostrHashtagDataSource.loadHashtag(tag)
|
||||||
HashtagFeedFilter.loadHashtag(accountViewModel.account, tag)
|
|
||||||
val feedViewModel: NostrHashtagFeedViewModel = viewModel()
|
|
||||||
|
|
||||||
LaunchedEffect(tag) {
|
LaunchedEffect(tag) {
|
||||||
HashtagFeedFilter.loadHashtag(accountViewModel.account, tag)
|
NostrHashtagDataSource.start()
|
||||||
NostrHashtagDataSource.loadHashtag(tag)
|
feedViewModel.invalidateData()
|
||||||
feedViewModel.invalidateData()
|
}
|
||||||
}
|
|
||||||
|
|
||||||
DisposableEffect(accountViewModel) {
|
DisposableEffect(accountViewModel) {
|
||||||
val observer = LifecycleEventObserver { _, event ->
|
val observer = LifecycleEventObserver { _, event ->
|
||||||
if (event == Lifecycle.Event.ON_RESUME) {
|
if (event == Lifecycle.Event.ON_RESUME) {
|
||||||
println("Hashtag Start")
|
println("Hashtag Start")
|
||||||
HashtagFeedFilter.loadHashtag(accountViewModel.account, tag)
|
NostrHashtagDataSource.loadHashtag(tag)
|
||||||
NostrHashtagDataSource.loadHashtag(tag)
|
NostrHashtagDataSource.start()
|
||||||
NostrHashtagDataSource.start()
|
feedViewModel.invalidateData()
|
||||||
feedViewModel.invalidateData()
|
|
||||||
}
|
|
||||||
if (event == Lifecycle.Event.ON_PAUSE) {
|
|
||||||
println("Hashtag Stop")
|
|
||||||
HashtagFeedFilter.loadHashtag(accountViewModel.account, null)
|
|
||||||
NostrHashtagDataSource.loadHashtag(null)
|
|
||||||
NostrHashtagDataSource.stop()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (event == Lifecycle.Event.ON_PAUSE) {
|
||||||
lifeCycleOwner.lifecycle.addObserver(observer)
|
println("Hashtag Stop")
|
||||||
onDispose {
|
NostrHashtagDataSource.loadHashtag(null)
|
||||||
lifeCycleOwner.lifecycle.removeObserver(observer)
|
NostrHashtagDataSource.stop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(Modifier.fillMaxHeight()) {
|
lifeCycleOwner.lifecycle.addObserver(observer)
|
||||||
Column(
|
onDispose {
|
||||||
modifier = Modifier.padding(vertical = 0.dp)
|
lifeCycleOwner.lifecycle.removeObserver(observer)
|
||||||
) {
|
NostrHashtagDataSource.loadHashtag(null)
|
||||||
HashtagHeader(tag, accountViewModel)
|
NostrHashtagDataSource.stop()
|
||||||
RefresheableFeedView(
|
}
|
||||||
feedViewModel,
|
}
|
||||||
null,
|
|
||||||
accountViewModel = accountViewModel,
|
Column(Modifier.fillMaxHeight()) {
|
||||||
nav = nav
|
Column(
|
||||||
)
|
modifier = Modifier.padding(vertical = 0.dp)
|
||||||
}
|
) {
|
||||||
|
HashtagHeader(tag, accountViewModel)
|
||||||
|
RefresheableFeedView(
|
||||||
|
feedViewModel,
|
||||||
|
null,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,7 +110,7 @@ fun HashtagHeader(tag: String, account: AccountViewModel, onClick: () -> Unit =
|
|||||||
Column(
|
Column(
|
||||||
Modifier.clickable { onClick() }
|
Modifier.clickable { onClick() }
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(12.dp)) {
|
Column(modifier = HalfPadding) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -43,7 +43,9 @@ import com.vitorpamplona.amethyst.ui.screen.AccountState
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListKnownFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListKnownFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListNewFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListNewFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverChatFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverCommunityFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverLiveFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel
|
||||||
@@ -93,9 +95,19 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
|
|||||||
factory = NostrVideoFeedViewModel.Factory(accountViewModel.account)
|
factory = NostrVideoFeedViewModel.Factory(accountViewModel.account)
|
||||||
)
|
)
|
||||||
|
|
||||||
val discoveryFeedViewModel: NostrDiscoverFeedViewModel = viewModel(
|
val discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel = viewModel(
|
||||||
key = accountViewModel.userProfile().pubkeyHex + "NostrDiscoveryFeedViewModel",
|
key = accountViewModel.userProfile().pubkeyHex + "NostrDiscoveryLiveFeedViewModel",
|
||||||
factory = NostrDiscoverFeedViewModel.Factory(accountViewModel.account)
|
factory = NostrDiscoverLiveFeedViewModel.Factory(accountViewModel.account)
|
||||||
|
)
|
||||||
|
|
||||||
|
val discoveryCommunityFeedViewModel: NostrDiscoverCommunityFeedViewModel = viewModel(
|
||||||
|
key = accountViewModel.userProfile().pubkeyHex + "NostrDiscoveryCommunityFeedViewModel",
|
||||||
|
factory = NostrDiscoverCommunityFeedViewModel.Factory(accountViewModel.account)
|
||||||
|
)
|
||||||
|
|
||||||
|
val discoveryChatFeedViewModel: NostrDiscoverChatFeedViewModel = viewModel(
|
||||||
|
key = accountViewModel.userProfile().pubkeyHex + "NostrDiscoveryChatFeedViewModel",
|
||||||
|
factory = NostrDiscoverChatFeedViewModel.Factory(accountViewModel.account)
|
||||||
)
|
)
|
||||||
|
|
||||||
val notifFeedViewModel: NotificationViewModel = viewModel(
|
val notifFeedViewModel: NotificationViewModel = viewModel(
|
||||||
@@ -137,7 +149,9 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
|
|||||||
videoFeedViewModel.sendToTop()
|
videoFeedViewModel.sendToTop()
|
||||||
}
|
}
|
||||||
Route.Discover.base -> {
|
Route.Discover.base -> {
|
||||||
discoveryFeedViewModel.sendToTop()
|
discoveryLiveFeedViewModel.sendToTop()
|
||||||
|
discoveryCommunityFeedViewModel.sendToTop()
|
||||||
|
discoveryChatFeedViewModel.sendToTop()
|
||||||
}
|
}
|
||||||
Route.Notification.base -> {
|
Route.Notification.base -> {
|
||||||
notifFeedViewModel.invalidateDataAndSendToTop()
|
notifFeedViewModel.invalidateDataAndSendToTop()
|
||||||
@@ -186,7 +200,9 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
|
|||||||
knownFeedViewModel = knownFeedViewModel,
|
knownFeedViewModel = knownFeedViewModel,
|
||||||
newFeedViewModel = newFeedViewModel,
|
newFeedViewModel = newFeedViewModel,
|
||||||
videoFeedViewModel = videoFeedViewModel,
|
videoFeedViewModel = videoFeedViewModel,
|
||||||
discoveryFeedViewModel = discoveryFeedViewModel,
|
discoveryLiveFeedViewModel = discoveryLiveFeedViewModel,
|
||||||
|
discoveryCommunityFeedViewModel = discoveryCommunityFeedViewModel,
|
||||||
|
discoveryChatFeedViewModel = discoveryChatFeedViewModel,
|
||||||
notifFeedViewModel = notifFeedViewModel,
|
notifFeedViewModel = notifFeedViewModel,
|
||||||
userReactionsStatsModel = userReactionsStatsModel,
|
userReactionsStatsModel = userReactionsStatsModel,
|
||||||
navController = navController,
|
navController = navController,
|
||||||
|
|||||||
@@ -472,4 +472,8 @@
|
|||||||
<string name="followed_tags">Followed Tags</string>
|
<string name="followed_tags">Followed Tags</string>
|
||||||
|
|
||||||
<string name="relay_setup">Relays</string>
|
<string name="relay_setup">Relays</string>
|
||||||
|
|
||||||
|
<string name="discover_live">Live</string>
|
||||||
|
<string name="discover_community">Communities</string>
|
||||||
|
<string name="discover_chat">Chats Groups</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user