Further separates channels by kind in the UI
This commit is contained in:
@@ -2254,7 +2254,7 @@ class Account(
|
||||
delay(1000 * 60 * 1)
|
||||
// waits 5 minutes before migrating the list.
|
||||
val contactList = userProfile().latestContactList
|
||||
val oldChannels = contactList?.taggedEventIds()?.toSet()?.mapNotNull { cache.getChannelIfExists(it) as? PublicChatChannel }
|
||||
val oldChannels = contactList?.taggedEventIds()?.toSet()?.mapNotNull { cache.getPublicChatChannelIfExists(it) as? PublicChatChannel }
|
||||
|
||||
if (oldChannels != null && oldChannels.isNotEmpty()) {
|
||||
Log.d("DB UPGRADE", "Migrating List with ${oldChannels.size} old channels ")
|
||||
|
||||
@@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortDisplay
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
@@ -34,23 +34,17 @@ import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.toNEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelDataNorm
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
import com.vitorpamplona.quartz.utils.Hex
|
||||
import com.vitorpamplona.quartz.utils.LargeCache
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@Stable
|
||||
class EphemeralChatChannel(
|
||||
val roomId: RoomId,
|
||||
) : Channel(roomId.toKey()) {
|
||||
override fun idNote() = roomId.toDisplayKey()
|
||||
|
||||
override fun idDisplayNote() = idNote().toShortenHex()
|
||||
|
||||
) : Channel() {
|
||||
override fun relays() = setOf(roomId.relayUrl)
|
||||
|
||||
override fun toBestDisplayName() = roomId.toDisplayKey()
|
||||
@@ -64,8 +58,8 @@ class EphemeralChatChannel(
|
||||
|
||||
@Stable
|
||||
class PublicChatChannel(
|
||||
idHex: String,
|
||||
) : Channel(idHex) {
|
||||
val idHex: String,
|
||||
) : Channel() {
|
||||
var event: ChannelCreateEvent? = null
|
||||
var infoTags = EmptyTagList
|
||||
var info = ChannelDataNorm(null, null, null, null)
|
||||
@@ -110,7 +104,7 @@ class PublicChatChannel(
|
||||
super.updateChannelInfo(creator, updatedAt)
|
||||
}
|
||||
|
||||
override fun toBestDisplayName(): String = info.name ?: super.toBestDisplayName()
|
||||
override fun toBestDisplayName(): String = info.name ?: toNEvent().toShortDisplay()
|
||||
|
||||
override fun summary(): String? = info.about
|
||||
|
||||
@@ -119,19 +113,18 @@ class PublicChatChannel(
|
||||
return info.picture ?: super.profilePicture()
|
||||
}
|
||||
|
||||
override fun anyNameStartsWith(prefix: String): Boolean = listOfNotNull(info.name, info.about).any { it.contains(prefix, true) }
|
||||
override fun anyNameStartsWith(prefix: String): Boolean =
|
||||
idHex.startsWith(prefix) ||
|
||||
info.name?.contains(prefix, true) == true ||
|
||||
info.about?.contains(prefix, true) == true
|
||||
}
|
||||
|
||||
@Stable
|
||||
class LiveActivitiesChannel(
|
||||
val address: Address,
|
||||
) : Channel(address.toValue()) {
|
||||
) : Channel() {
|
||||
var info: LiveActivitiesEvent? = null
|
||||
|
||||
override fun idNote() = toNAddr()
|
||||
|
||||
override fun idDisplayNote() = idNote().toShortenHex()
|
||||
|
||||
fun address() = address
|
||||
|
||||
override fun relays() = info?.allRelayUrls()?.toSet() ?: super.relays()
|
||||
@@ -149,7 +142,7 @@ class LiveActivitiesChannel(
|
||||
super.updateChannelInfo(creator, updatedAt)
|
||||
}
|
||||
|
||||
override fun toBestDisplayName(): String = info?.title() ?: super.toBestDisplayName()
|
||||
override fun toBestDisplayName(): String = info?.title() ?: toNAddr().toShortDisplay()
|
||||
|
||||
override fun summary(): String? = info?.summary()
|
||||
|
||||
@@ -171,9 +164,7 @@ data class Counter(
|
||||
)
|
||||
|
||||
@Stable
|
||||
abstract class Channel(
|
||||
val idHex: String,
|
||||
) {
|
||||
abstract class Channel {
|
||||
var creator: User? = null
|
||||
var updatedMetadataAt: Long = 0
|
||||
val notes = LargeCache<HexKey, Note>()
|
||||
@@ -181,11 +172,7 @@ abstract class Channel(
|
||||
|
||||
private var relays = mapOf<NormalizedRelayUrl, Counter>()
|
||||
|
||||
open fun idNote() = Hex.decode(idHex).toNEvent()
|
||||
|
||||
open fun idDisplayNote() = idNote().toShortenHex()
|
||||
|
||||
open fun toBestDisplayName(): String = idDisplayNote()
|
||||
abstract fun toBestDisplayName(): String
|
||||
|
||||
open fun summary(): String? = null
|
||||
|
||||
@@ -248,10 +235,6 @@ abstract class Channel(
|
||||
notes.remove(note.idHex)
|
||||
}
|
||||
|
||||
fun removeNote(noteHex: String) {
|
||||
notes.remove(noteHex)
|
||||
}
|
||||
|
||||
abstract fun anyNameStartsWith(prefix: String): Boolean
|
||||
|
||||
fun pruneOldMessages(): Set<Note> {
|
||||
|
||||
@@ -89,6 +89,7 @@ import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
|
||||
import com.vitorpamplona.quartz.nip19Bech32.decodeEventIdAsHexOrNull
|
||||
import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.Entity
|
||||
@@ -320,11 +321,11 @@ object LocalCache : ILocalCache {
|
||||
|
||||
fun getNoteIfExists(key: ETag): Note? = notes.get(key.eventId)
|
||||
|
||||
fun getChannelIfExists(key: String): PublicChatChannel? = publicChatChannels.get(key)
|
||||
fun getPublicChatChannelIfExists(key: String): PublicChatChannel? = publicChatChannels.get(key)
|
||||
|
||||
fun getChannelIfExists(key: RoomId): EphemeralChatChannel? = ephemeralChannels.get(key)
|
||||
fun getEphemeralChatChannelIfExists(key: RoomId): EphemeralChatChannel? = ephemeralChannels.get(key)
|
||||
|
||||
fun getChannelIfExists(key: Address): LiveActivitiesChannel? = liveChatChannels.get(key)
|
||||
fun getLiveActivityChannelIfExists(key: Address): LiveActivitiesChannel? = liveChatChannels.get(key)
|
||||
|
||||
fun getNoteIfExists(event: Event): Note? =
|
||||
if (event is AddressableEvent) {
|
||||
@@ -1345,10 +1346,10 @@ object LocalCache : ILocalCache {
|
||||
masterNote.removeReport(deleteNote)
|
||||
}
|
||||
|
||||
deleteNote.channelHex()?.let { getChannelIfExists(it)?.removeNote(deleteNote) }
|
||||
deleteNote.channelHex()?.let { getPublicChatChannelIfExists(it)?.removeNote(deleteNote) }
|
||||
|
||||
(deletedEvent as? LiveActivitiesChatMessageEvent)?.activity()?.let {
|
||||
getChannelIfExists(it.toTag())?.removeNote(deleteNote)
|
||||
getPublicChatChannelIfExists(it.toTag())?.removeNote(deleteNote)
|
||||
}
|
||||
|
||||
(deletedEvent as? TorrentCommentEvent)?.torrentIds()?.let {
|
||||
@@ -2318,30 +2319,44 @@ object LocalCache : ILocalCache {
|
||||
}
|
||||
}
|
||||
|
||||
fun findChannelsStartingWith(text: String): List<Channel> {
|
||||
checkNotInMainThread()
|
||||
|
||||
fun findPublicChatChannelsStartingWith(text: String): List<PublicChatChannel> {
|
||||
if (text.isBlank()) return emptyList()
|
||||
|
||||
val key = decodeEventIdAsHexOrNull(text)
|
||||
if (key != null && getChannelIfExists(key) != null) {
|
||||
return listOfNotNull(getChannelIfExists(key))
|
||||
if (key != null) {
|
||||
getPublicChatChannelIfExists(key)?.let {
|
||||
return listOf(it)
|
||||
}
|
||||
}
|
||||
|
||||
return publicChatChannels.filter { _, channel ->
|
||||
channel.anyNameStartsWith(text) ||
|
||||
channel.idHex.startsWith(text, true) ||
|
||||
channel.idNote().startsWith(text, true)
|
||||
} +
|
||||
ephemeralChannels.filter { _, channel ->
|
||||
channel.anyNameStartsWith(text) ||
|
||||
channel.idHex.startsWith(text, true) ||
|
||||
channel.idNote().startsWith(text, true)
|
||||
} +
|
||||
liveChatChannels.filter { _, channel ->
|
||||
channel.anyNameStartsWith(text) ||
|
||||
channel.idHex.startsWith(text, true) ||
|
||||
channel.idNote().startsWith(text, true)
|
||||
channel.anyNameStartsWith(text)
|
||||
}
|
||||
}
|
||||
|
||||
fun findEphemeralChatChannelsStartingWith(text: String): List<EphemeralChatChannel> {
|
||||
if (text.isBlank()) return emptyList()
|
||||
|
||||
return ephemeralChannels.filter { _, channel ->
|
||||
channel.anyNameStartsWith(text)
|
||||
}
|
||||
}
|
||||
|
||||
fun findLiveActivityChannelsStartingWith(text: String): List<LiveActivitiesChannel> {
|
||||
if (text.isBlank()) return emptyList()
|
||||
|
||||
try {
|
||||
val parsed = Nip19Parser.uriToRoute(text)?.entity
|
||||
if (parsed is NAddress && parsed.kind == LiveActivitiesEvent.KIND) {
|
||||
return listOf(getOrCreateLiveChannel(parsed.address()))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
}
|
||||
|
||||
return liveChatChannels.filter { _, channel ->
|
||||
channel.anyNameStartsWith(text)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji
|
||||
import com.vitorpamplona.amethyst.service.replace
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortDisplay
|
||||
import com.vitorpamplona.quartz.experimental.bounties.addedRewardValue
|
||||
import com.vitorpamplona.quartz.experimental.bounties.hasAdditionalReward
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent
|
||||
@@ -91,7 +91,7 @@ class AddressableNote(
|
||||
|
||||
override fun toNEvent() = toNAddr()
|
||||
|
||||
override fun idDisplayNote() = idNote().toShortenHex()
|
||||
override fun idDisplayNote() = idNote().toShortDisplay()
|
||||
|
||||
override fun address() = address
|
||||
|
||||
@@ -212,7 +212,7 @@ open class Note(
|
||||
|
||||
fun toNostrUri(): String = "nostr:${toNEvent()}"
|
||||
|
||||
open fun idDisplayNote() = idNote().toShortenHex()
|
||||
open fun idDisplayNote() = idNote().toShortDisplay()
|
||||
|
||||
fun channelHex(): HexKey? =
|
||||
if (
|
||||
|
||||
@@ -96,7 +96,7 @@ class ParticipantListBuilder {
|
||||
it.replyTo?.forEach { addFollowsThatDirectlyParticipateOnToSet(it, followingSet, mySet) }
|
||||
}
|
||||
|
||||
LocalCache.getChannelIfExists(baseNote.idHex)?.notes?.forEach { key, it ->
|
||||
LocalCache.getPublicChatChannelIfExists(baseNote.idHex)?.notes?.forEach { key, it ->
|
||||
addFollowsThatDirectlyParticipateOnToSet(it, followingSet, mySet)
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.model
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortDisplay
|
||||
import com.vitorpamplona.quartz.lightning.Lud06
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
@@ -78,7 +78,7 @@ class User(
|
||||
|
||||
fun pubkeyNpub() = pubkey().toNpub()
|
||||
|
||||
fun pubkeyDisplayHex() = pubkeyNpub().toShortenHex()
|
||||
fun pubkeyDisplayHex() = pubkeyNpub().toShortDisplay()
|
||||
|
||||
fun dmInboxRelayList() = (LocalCache.getAddressableNoteIfExists(ChatMessageRelayListEvent.createAddressTag(pubkeyHex))?.event as? ChatMessageRelayListEvent)
|
||||
|
||||
|
||||
+7
-7
@@ -20,7 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service.relayClient.eoseManagers
|
||||
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSEFollowList
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSEByKey
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.ammolite.relays.datasources.Subscription
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
@@ -37,16 +37,16 @@ import kotlin.collections.distinctBy
|
||||
* This class keeps EOSEs for each SubID.id() for as long as possible and
|
||||
* shares all EOSEs among all users.
|
||||
*/
|
||||
abstract class PerUniqueIdEoseManager<T>(
|
||||
abstract class PerUniqueIdEoseManager<T, U : Any>(
|
||||
client: NostrClient,
|
||||
allKeys: () -> Set<T>,
|
||||
val invalidateAfterEose: Boolean = false,
|
||||
) : BaseEoseManager<T>(client, allKeys) {
|
||||
// long term EOSE cache
|
||||
private val latestEOSEs = EOSEFollowList()
|
||||
private val latestEOSEs = EOSEByKey<U>()
|
||||
|
||||
// map between each query Id and each subscription id
|
||||
private val userSubscriptionMap = mutableMapOf<String, String>()
|
||||
private val userSubscriptionMap = mutableMapOf<U, String>()
|
||||
|
||||
fun since(key: T) = latestEOSEs.since(id(key))
|
||||
|
||||
@@ -67,7 +67,7 @@ abstract class PerUniqueIdEoseManager<T>(
|
||||
}
|
||||
|
||||
open fun endSub(
|
||||
key: String,
|
||||
key: U,
|
||||
subId: String,
|
||||
) {
|
||||
dismissSubscription(subId)
|
||||
@@ -87,7 +87,7 @@ abstract class PerUniqueIdEoseManager<T>(
|
||||
override fun updateSubscriptions(keys: Set<T>) {
|
||||
val uniqueSubscribedAccounts = keys.distinctBy { id(it) }
|
||||
|
||||
val updated = mutableSetOf<String>()
|
||||
val updated = mutableSetOf<U>()
|
||||
|
||||
uniqueSubscribedAccounts.forEach {
|
||||
val mainKey = id(it)
|
||||
@@ -107,5 +107,5 @@ abstract class PerUniqueIdEoseManager<T>(
|
||||
since: SincePerRelayMap?,
|
||||
): List<RelayBasedFilter>?
|
||||
|
||||
abstract fun id(key: T): String
|
||||
abstract fun id(key: T): U
|
||||
}
|
||||
|
||||
+4
-4
@@ -21,7 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.service.relayClient.eoseManagers
|
||||
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSEAccountKey
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.ammolite.relays.datasources.Subscription
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
@@ -41,12 +41,12 @@ import kotlin.collections.distinctBy
|
||||
* does NOT share EOSEs with other users. Changing the list will not make the
|
||||
* app reuse the EOSE because it assumes the filter is going to be different
|
||||
*/
|
||||
abstract class PerUserAndFollowListEoseManager<T>(
|
||||
abstract class PerUserAndFollowListEoseManager<T, U : Any>(
|
||||
client: NostrClient,
|
||||
allKeys: () -> Set<T>,
|
||||
val invalidateAfterEose: Boolean = false,
|
||||
) : BaseEoseManager<T>(client, allKeys) {
|
||||
private val latestEOSEs = EOSEAccount()
|
||||
private val latestEOSEs = EOSEAccountKey<U>()
|
||||
private val userSubscriptionMap = mutableMapOf<User, String>()
|
||||
|
||||
fun since(key: T) = latestEOSEs.since(user(key), list(key))
|
||||
@@ -108,5 +108,5 @@ abstract class PerUserAndFollowListEoseManager<T>(
|
||||
|
||||
abstract fun user(key: T): User
|
||||
|
||||
abstract fun list(key: T): String
|
||||
abstract fun list(key: T): U
|
||||
}
|
||||
|
||||
+1
-1
@@ -76,5 +76,5 @@ class ChannelMetadataAndLiveActivityWatcherSubAssembler(
|
||||
}
|
||||
}
|
||||
|
||||
override fun distinct(key: ChannelFinderQueryState) = key.channel.idHex
|
||||
override fun distinct(key: ChannelFinderQueryState) = key.channel
|
||||
}
|
||||
|
||||
+1
-1
@@ -40,5 +40,5 @@ class ChannelLoaderSubAssembler(
|
||||
) : SingleSubNoEoseCacheEoseManager<ChannelFinderQueryState>(client, allKeys, invalidateAfterEose = true) {
|
||||
override fun updateFilter(keys: List<ChannelFinderQueryState>): List<RelayBasedFilter>? = filterMissingChannelsById(keys)
|
||||
|
||||
override fun distinct(key: ChannelFinderQueryState) = key.channel.idHex
|
||||
override fun distinct(key: ChannelFinderQueryState) = key.channel
|
||||
}
|
||||
|
||||
+3
-2
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState
|
||||
@@ -30,7 +31,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
class ChannelMetadataWatcherSubAssembler(
|
||||
client: NostrClient,
|
||||
allKeys: () -> Set<ChannelFinderQueryState>,
|
||||
) : PerUniqueIdEoseManager<ChannelFinderQueryState>(client, allKeys) {
|
||||
) : PerUniqueIdEoseManager<ChannelFinderQueryState, Channel>(client, allKeys) {
|
||||
override fun updateFilter(
|
||||
key: ChannelFinderQueryState,
|
||||
since: SincePerRelayMap?,
|
||||
@@ -46,5 +47,5 @@ class ChannelMetadataWatcherSubAssembler(
|
||||
/**
|
||||
* Only one key per channel.
|
||||
*/
|
||||
override fun id(key: ChannelFinderQueryState) = key.channel.idHex
|
||||
override fun id(key: ChannelFinderQueryState) = key.channel
|
||||
}
|
||||
|
||||
+3
-2
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip53LiveActivities
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState
|
||||
@@ -34,7 +35,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
class LiveActivityWatcherSubAssembly(
|
||||
client: NostrClient,
|
||||
allKeys: () -> Set<ChannelFinderQueryState>,
|
||||
) : PerUniqueIdEoseManager<ChannelFinderQueryState>(client, allKeys) {
|
||||
) : PerUniqueIdEoseManager<ChannelFinderQueryState, Channel>(client, allKeys) {
|
||||
override fun updateFilter(
|
||||
key: ChannelFinderQueryState,
|
||||
since: SincePerRelayMap?,
|
||||
@@ -50,5 +51,5 @@ class LiveActivityWatcherSubAssembly(
|
||||
/**
|
||||
* Only one key per channel.
|
||||
*/
|
||||
override fun id(key: ChannelFinderQueryState) = key.channel.idHex
|
||||
override fun id(key: ChannelFinderQueryState) = key.channel
|
||||
}
|
||||
|
||||
+2
-2
@@ -46,7 +46,7 @@ class SearchWatcherSubAssembler(
|
||||
val cache: LocalCache,
|
||||
client: NostrClient,
|
||||
allKeys: () -> Set<SearchQueryState>,
|
||||
) : PerUniqueIdEoseManager<SearchQueryState>(client, allKeys) {
|
||||
) : PerUniqueIdEoseManager<SearchQueryState, Int>(client, allKeys) {
|
||||
override fun updateFilter(
|
||||
key: SearchQueryState,
|
||||
since: SincePerRelayMap?,
|
||||
@@ -95,5 +95,5 @@ class SearchWatcherSubAssembler(
|
||||
return directFilters + searchFilters
|
||||
}
|
||||
|
||||
override fun id(key: SearchQueryState) = key.searchQuery.hashCode().toString()
|
||||
override fun id(key: SearchQueryState) = key.searchQuery.hashCode()
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.service.relays
|
||||
|
||||
import androidx.collection.LruCache
|
||||
import com.vitorpamplona.amethyst.model.LocalCache.users
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.ammolite.relays.filters.MutableTime
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
@@ -57,11 +58,15 @@ class EOSERelayList {
|
||||
|
||||
class EOSEFollowList(
|
||||
cacheSize: Int = 200,
|
||||
) : EOSEByKey<String>(cacheSize)
|
||||
|
||||
open class EOSEByKey<U : Any>(
|
||||
cacheSize: Int = 200,
|
||||
) {
|
||||
var followList: LruCache<String, EOSERelayList> = LruCache<String, EOSERelayList>(cacheSize)
|
||||
var followList: LruCache<U, EOSERelayList> = LruCache<U, EOSERelayList>(cacheSize)
|
||||
|
||||
fun addOrUpdate(
|
||||
listCode: String,
|
||||
listCode: U,
|
||||
relayUrl: NormalizedRelayUrl,
|
||||
time: Long,
|
||||
) {
|
||||
@@ -75,10 +80,10 @@ class EOSEFollowList(
|
||||
}
|
||||
}
|
||||
|
||||
fun since(listCode: String) = followList[listCode]?.relayList
|
||||
fun since(listCode: U) = followList[listCode]?.relayList
|
||||
|
||||
fun newEose(
|
||||
listCode: String,
|
||||
listCode: U,
|
||||
relayUrl: NormalizedRelayUrl,
|
||||
time: Long,
|
||||
) = addOrUpdate(listCode, relayUrl, time)
|
||||
@@ -86,18 +91,22 @@ class EOSEFollowList(
|
||||
|
||||
class EOSEAccount(
|
||||
cacheSize: Int = 20,
|
||||
) : EOSEAccountKey<String>(cacheSize)
|
||||
|
||||
open class EOSEAccountKey<U : Any>(
|
||||
cacheSize: Int = 20,
|
||||
) {
|
||||
var users: LruCache<User, EOSEFollowList> = LruCache<User, EOSEFollowList>(cacheSize)
|
||||
var users: LruCache<User, EOSEByKey<U>> = LruCache<User, EOSEByKey<U>>(cacheSize)
|
||||
|
||||
fun addOrUpdate(
|
||||
user: User,
|
||||
listCode: String,
|
||||
listCode: U,
|
||||
relayUrl: NormalizedRelayUrl,
|
||||
time: Long,
|
||||
) {
|
||||
val followList = users[user]
|
||||
if (followList == null) {
|
||||
val newList = EOSEFollowList()
|
||||
val newList = EOSEByKey<U>()
|
||||
users.put(user, newList)
|
||||
newList.addOrUpdate(listCode, relayUrl, time)
|
||||
} else {
|
||||
@@ -111,12 +120,12 @@ class EOSEAccount(
|
||||
|
||||
fun since(
|
||||
key: User,
|
||||
listCode: String,
|
||||
listCode: U,
|
||||
) = users[key]?.followList?.get(listCode)?.relayList
|
||||
|
||||
fun newEose(
|
||||
user: User,
|
||||
listCode: String,
|
||||
listCode: U,
|
||||
relayUrl: NormalizedRelayUrl,
|
||||
time: Long,
|
||||
) = addOrUpdate(user, listCode, relayUrl, time)
|
||||
|
||||
@@ -40,11 +40,12 @@ import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.DEFAULT_MUTED_SETTING
|
||||
import com.vitorpamplona.amethyst.service.playback.pip.BackgroundMedia
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.prepareSharedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
|
||||
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed
|
||||
@@ -52,12 +53,7 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NNote
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -165,30 +161,33 @@ fun uriToRoute(
|
||||
is NProfile -> Route.Profile(nip19.hex)
|
||||
is NNote -> Route.Note(nip19.hex)
|
||||
is NEvent -> {
|
||||
if (nip19.kind == PrivateDmEvent.KIND) {
|
||||
nip19.author?.let { Route.RoomByAuthor(it) }
|
||||
} else if (
|
||||
nip19.kind == ChannelMessageEvent.KIND ||
|
||||
nip19.kind == ChannelCreateEvent.KIND ||
|
||||
nip19.kind == ChannelMetadataEvent.KIND
|
||||
) {
|
||||
Route.Channel(nip19.hex)
|
||||
} else {
|
||||
Route.EventRedirect(nip19.hex)
|
||||
}
|
||||
routeFor(
|
||||
note = LocalCache.getOrCreateNote(nip19.hex),
|
||||
loggedIn = account.userProfile(),
|
||||
) ?: Route.EventRedirect(nip19.hex)
|
||||
}
|
||||
|
||||
is NAddress -> {
|
||||
if (nip19.kind == CommunityDefinitionEvent.KIND) {
|
||||
Route.Community(nip19.kind, nip19.author, nip19.dTag)
|
||||
} else if (nip19.kind == LiveActivitiesEvent.KIND) {
|
||||
Route.Channel(nip19.aTag())
|
||||
} else {
|
||||
Route.EventRedirect(nip19.aTag())
|
||||
}
|
||||
routeFor(
|
||||
note = LocalCache.getOrCreateAddressableNote(nip19.address()),
|
||||
loggedIn = account.userProfile(),
|
||||
) ?: Route.EventRedirect(nip19.aTag())
|
||||
}
|
||||
|
||||
is NEmbed -> Route.EventRedirect(nip19.event.id)
|
||||
is NEmbed -> {
|
||||
val noteEvent = nip19.event
|
||||
if (noteEvent is AddressableEvent) {
|
||||
routeFor(
|
||||
note = LocalCache.getOrCreateAddressableNote(noteEvent.address()),
|
||||
loggedIn = account.userProfile(),
|
||||
) ?: Route.EventRedirect(noteEvent.addressTag())
|
||||
} else {
|
||||
routeFor(
|
||||
note = LocalCache.getOrCreateNote(nip19.event.id),
|
||||
loggedIn = account.userProfile(),
|
||||
) ?: Route.EventRedirect(nip19.event.id)
|
||||
}
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
@@ -42,7 +42,6 @@ class NewMessageTagger(
|
||||
var message: String,
|
||||
var pTags: List<User>? = null,
|
||||
var eTags: List<Note>? = null,
|
||||
var channelHex: String? = null,
|
||||
var dao: Dao,
|
||||
) {
|
||||
val directMentions = mutableSetOf<HexKey>()
|
||||
|
||||
@@ -30,7 +30,6 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -57,30 +56,27 @@ import androidx.compose.ui.unit.sp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadChannel
|
||||
import com.vitorpamplona.amethyst.ui.note.njumpLink
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NNote
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NSec
|
||||
import com.vitorpamplona.quartz.nip19Bech32.toNIP19
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
@@ -95,19 +91,13 @@ fun ClickableRoute(
|
||||
when (val entity = nip19.entity) {
|
||||
is NPub -> DisplayUser(entity.hex, nip19.nip19raw, nip19.additionalChars, accountViewModel, nav)
|
||||
is NProfile -> DisplayUser(entity.hex, nip19.nip19raw, nip19.additionalChars, accountViewModel, nav)
|
||||
is com.vitorpamplona.quartz.nip19Bech32.entities.NNote -> DisplayEvent(entity.hex, null, nip19.nip19raw, nip19.additionalChars, accountViewModel, nav)
|
||||
is NEvent -> DisplayEvent(entity.hex, entity.kind, nip19.nip19raw, nip19.additionalChars, accountViewModel, nav)
|
||||
is NNote -> DisplayEvent(entity.hex, nip19.nip19raw, nip19.additionalChars, accountViewModel, nav)
|
||||
is NEvent -> DisplayEvent(entity.hex, nip19.nip19raw, nip19.additionalChars, accountViewModel, nav)
|
||||
is NEmbed -> LoadAndDisplayEvent(entity.event, nip19.additionalChars, accountViewModel, nav)
|
||||
is NAddress -> DisplayAddress(entity, nip19.nip19raw, nip19.additionalChars, accountViewModel, nav)
|
||||
is NRelay -> {
|
||||
Text(word)
|
||||
}
|
||||
is NSec -> {
|
||||
Text(word)
|
||||
}
|
||||
else -> {
|
||||
Text(word)
|
||||
}
|
||||
is NRelay -> Text(word)
|
||||
is NSec -> Text(word)
|
||||
else -> Text(word)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +128,7 @@ private fun LoadAndDisplayEvent(
|
||||
) {
|
||||
LoadOrCreateNote(event, accountViewModel) {
|
||||
if (it != null) {
|
||||
DisplayNoteLink(it, event.id, event.kind, additionalChars, accountViewModel, nav)
|
||||
DisplayNoteLink(it, event.id, additionalChars, accountViewModel, nav)
|
||||
} else {
|
||||
val externalLink = event.toNIP19()
|
||||
val uri = LocalUriHandler.current
|
||||
@@ -158,7 +148,6 @@ private fun LoadAndDisplayEvent(
|
||||
@Composable
|
||||
fun DisplayEvent(
|
||||
hex: HexKey,
|
||||
kind: Int?,
|
||||
nip19: String,
|
||||
additionalChars: String?,
|
||||
accountViewModel: AccountViewModel,
|
||||
@@ -166,7 +155,7 @@ fun DisplayEvent(
|
||||
) {
|
||||
LoadNote(hex, accountViewModel) {
|
||||
if (it != null) {
|
||||
DisplayNoteLink(it, hex, kind, additionalChars, accountViewModel, nav)
|
||||
DisplayNoteLink(it, hex, additionalChars, accountViewModel, nav)
|
||||
} else {
|
||||
val externalLink = njumpLink(nip19)
|
||||
val uri = LocalUriHandler.current
|
||||
@@ -187,55 +176,21 @@ fun DisplayEvent(
|
||||
private fun DisplayNoteLink(
|
||||
it: Note,
|
||||
hex: HexKey,
|
||||
kind: Int?,
|
||||
addedCharts: String?,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val noteState by observeNote(it, accountViewModel)
|
||||
val note = remember(noteState) { noteState?.note } ?: return
|
||||
val noteIdDisplayNote = remember(noteState) { "@${noteState.note.idDisplayNote()}" }
|
||||
|
||||
val channelHex = remember(noteState) { note.channelHex() }
|
||||
val noteIdDisplayNote = remember(noteState) { "@${note.idDisplayNote()}" }
|
||||
val route = routeFor(it, accountViewModel.userProfile()) ?: Route.EventRedirect(hex)
|
||||
|
||||
if (note.event is ChannelCreateEvent || kind == ChannelCreateEvent.KIND) {
|
||||
CreateClickableText(
|
||||
clickablePart = noteIdDisplayNote,
|
||||
suffix = addedCharts,
|
||||
route = remember(noteState) { Route.Channel(hex) },
|
||||
route = route,
|
||||
nav = nav,
|
||||
)
|
||||
} else if (note.event is PrivateDmEvent || kind == PrivateDmEvent.KIND) {
|
||||
CreateClickableText(
|
||||
clickablePart = noteIdDisplayNote,
|
||||
suffix = addedCharts,
|
||||
route =
|
||||
remember(noteState) { (note.author?.pubkeyHex ?: hex).let { Route.RoomByAuthor(it) } },
|
||||
nav = nav,
|
||||
)
|
||||
} else if (channelHex != null) {
|
||||
LoadChannel(baseChannelHex = channelHex, accountViewModel) { baseChannel ->
|
||||
val channelState by observeChannel(baseChannel, accountViewModel)
|
||||
val channelDisplayName by
|
||||
remember(channelState) {
|
||||
derivedStateOf { channelState?.channel?.toBestDisplayName() ?: noteIdDisplayNote }
|
||||
}
|
||||
|
||||
CreateClickableText(
|
||||
clickablePart = channelDisplayName,
|
||||
suffix = addedCharts,
|
||||
route = remember(noteState) { Route.Channel(baseChannel.idHex) },
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
CreateClickableText(
|
||||
clickablePart = noteIdDisplayNote,
|
||||
suffix = addedCharts,
|
||||
route = remember(noteState) { Route.EventRedirect(hex) },
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -258,7 +213,7 @@ private fun DisplayAddress(
|
||||
val noteState by observeNote(it, accountViewModel)
|
||||
|
||||
val route = remember(noteState) { Route.Note(nip19.aTag()) }
|
||||
val displayName = remember(noteState) { "@${noteState?.note?.idDisplayNote()}" }
|
||||
val displayName = remember(noteState) { "@${noteState.note.idDisplayNote()}" }
|
||||
|
||||
CreateClickableText(
|
||||
clickablePart = displayName,
|
||||
|
||||
@@ -94,7 +94,7 @@ import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.invoice.MayBeInvoicePreview
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortDisplay
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
@@ -727,7 +727,7 @@ fun TagLink(
|
||||
) {
|
||||
LoadNote(baseNoteHex = word.hex, accountViewModel) {
|
||||
if (it == null) {
|
||||
Text(text = remember { word.segmentText.toShortenHex() })
|
||||
Text(text = remember { word.segmentText.toShortDisplay() })
|
||||
} else {
|
||||
Row {
|
||||
DisplayNoteFromTag(
|
||||
@@ -766,7 +766,7 @@ private fun DisplayNoteFromTag(
|
||||
)
|
||||
} else {
|
||||
ClickableTextPrimary(
|
||||
text = "@${baseNote.idNote().toShortenHex()}",
|
||||
text = "@${baseNote.idNote().toShortDisplay()}",
|
||||
onClick = { routeFor(baseNote, accountViewModel.userProfile())?.let { nav.nav(it) } },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ abstract class FeedFilter<T> {
|
||||
open fun limit() = 1000
|
||||
|
||||
/** Returns a string that serves as the key to invalidate the list if it changes. */
|
||||
abstract fun feedKey(): String
|
||||
abstract fun feedKey(): Any
|
||||
|
||||
open fun showHiddenKey(): Boolean = false
|
||||
|
||||
|
||||
+8
-8
@@ -24,7 +24,7 @@ import android.util.Log
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.EphemeralChatChannel
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.dal.AdditiveComplexFeedFilter
|
||||
@@ -42,7 +42,7 @@ import kotlin.collections.distinctBy
|
||||
|
||||
@Stable
|
||||
class ChannelFeedContentState(
|
||||
val localFilter: AdditiveComplexFeedFilter<Channel, Note>,
|
||||
val localFilter: AdditiveComplexFeedFilter<EphemeralChatChannel, Note>,
|
||||
val viewModelScope: CoroutineScope,
|
||||
) : InvalidatableContent {
|
||||
private val _feedContent = MutableStateFlow<ChannelFeedState>(ChannelFeedState.Loading)
|
||||
@@ -53,7 +53,7 @@ class ChannelFeedContentState(
|
||||
val scrollToTop = _scrollToTop.asStateFlow()
|
||||
var scrolltoTopPending = false
|
||||
|
||||
private var lastFeedKey: String? = null
|
||||
private var lastFeedKey: Any? = null
|
||||
|
||||
override val isRefreshing: MutableState<Boolean> = mutableStateOf(false)
|
||||
|
||||
@@ -78,7 +78,7 @@ class ChannelFeedContentState(
|
||||
isRefreshing.value = true
|
||||
try {
|
||||
lastFeedKey = localFilter.feedKey()
|
||||
val notes = localFilter.loadTop().distinctBy { it.idHex }.toImmutableList()
|
||||
val notes = localFilter.loadTop().distinctBy { it }.toImmutableList()
|
||||
|
||||
val oldNotesState = _feedContent.value
|
||||
if (oldNotesState is ChannelFeedState.Loaded) {
|
||||
@@ -93,15 +93,15 @@ class ChannelFeedContentState(
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFeed(notes: ImmutableList<Channel>) {
|
||||
private fun updateFeed(notes: ImmutableList<EphemeralChatChannel>) {
|
||||
val currentState = _feedContent.value
|
||||
if (notes.isEmpty()) {
|
||||
_feedContent.tryEmit(ChannelFeedState.Empty)
|
||||
} else if (currentState is ChannelFeedState.Loaded) {
|
||||
currentState.feed.tryEmit(LoadedFeedState<Channel>(notes, localFilter.showHiddenKey()))
|
||||
currentState.feed.tryEmit(LoadedFeedState<EphemeralChatChannel>(notes, localFilter.showHiddenKey()))
|
||||
} else {
|
||||
_feedContent.tryEmit(
|
||||
ChannelFeedState.Loaded(MutableStateFlow(LoadedFeedState<Channel>(notes, localFilter.showHiddenKey()))),
|
||||
ChannelFeedState.Loaded(MutableStateFlow(LoadedFeedState<EphemeralChatChannel>(notes, localFilter.showHiddenKey()))),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -123,7 +123,7 @@ class ChannelFeedContentState(
|
||||
val newList =
|
||||
localFilter
|
||||
.updateListWith(emptyList(), newItems)
|
||||
.distinctBy { it.idHex }
|
||||
.distinctBy { it }
|
||||
.toImmutableList()
|
||||
if (newList.isNotEmpty()) {
|
||||
updateFeed(newList)
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.ui.feeds
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.EphemeralChatChannel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@Stable
|
||||
@@ -29,7 +29,7 @@ sealed class ChannelFeedState {
|
||||
object Loading : ChannelFeedState()
|
||||
|
||||
class Loaded(
|
||||
val feed: MutableStateFlow<LoadedFeedState<Channel>>,
|
||||
val feed: MutableStateFlow<LoadedFeedState<EphemeralChatChannel>>,
|
||||
) : ChannelFeedState()
|
||||
|
||||
object Empty : ChannelFeedState()
|
||||
|
||||
@@ -53,7 +53,7 @@ class FeedContentState(
|
||||
val scrollToTop = _scrollToTop.asStateFlow()
|
||||
var scrolltoTopPending = false
|
||||
|
||||
private var lastFeedKey: String? = null
|
||||
private var lastFeedKey: Any? = null
|
||||
|
||||
override val isRefreshing: MutableState<Boolean> = mutableStateOf(false)
|
||||
|
||||
|
||||
+2
-2
@@ -64,7 +64,7 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUse
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortDisplay
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedOff.AddAccountDialog
|
||||
@@ -242,7 +242,7 @@ private fun AccountName(
|
||||
}
|
||||
|
||||
Text(
|
||||
text = remember(user) { acc.npub.toShortenHex() },
|
||||
text = remember(user) { acc.npub.toShortDisplay() },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -59,10 +59,11 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarks.BookmarkListScree
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomByAuthorScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.NewGroupDMScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ChannelScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.EphemeralChatScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.EphemeralChatScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.metadata.NewEphemeralChatScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.PublicChatChannelScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.metadata.ChannelMetadataScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveActivityChannelScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.MessagesScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.CommunityScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.DiscoverScreen
|
||||
@@ -138,9 +139,12 @@ fun AppNavigation(
|
||||
composableFromEndArgs<Route.Geohash> { GeoHashScreen(it, accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.RelayInfo> { RelayInformationScreen(it.url, accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.Community> { CommunityScreen(Address(it.kind, it.pubKeyHex, it.dTag), accountViewModel, nav) }
|
||||
|
||||
composableFromEndArgs<Route.Room> { ChatroomScreen(it.id.toString(), it.message, it.replyId, it.draftId, accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.RoomByAuthor> { ChatroomByAuthorScreen(it.id, null, accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.Channel> { ChannelScreen(it.id, accountViewModel, nav) }
|
||||
|
||||
composableFromEndArgs<Route.PublicChatChannel> { PublicChatChannelScreen(it.id, accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.LiveActivityChannel> { LiveActivityChannelScreen(Address(it.kind, it.pubKeyHex, it.dTag), accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.EphemeralChat> {
|
||||
RelayUrlNormalizer.normalizeOrNull(it.relayUrl)?.let { relay ->
|
||||
EphemeralChatScreen(RoomId(it.id, relay), accountViewModel, nav)
|
||||
@@ -388,7 +392,7 @@ private fun isSameRoute(
|
||||
if (newRoute is Route.EventRedirect) {
|
||||
return when (currentRoute) {
|
||||
is Route.Note -> newRoute.id == currentRoute.id
|
||||
is Route.Channel -> newRoute.id == currentRoute.id
|
||||
is Route.PublicChatChannel -> newRoute.id == currentRoute.id
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,11 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.navigation
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.EphemeralChatChannel
|
||||
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
|
||||
@@ -39,6 +41,8 @@ import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.location.isGeohashedScoped
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.topics.isHashtagScoped
|
||||
@@ -48,7 +52,7 @@ fun routeFor(
|
||||
note: Note,
|
||||
loggedIn: User,
|
||||
): Route? {
|
||||
val noteEvent = note.event ?: return Route.Note(note.idHex)
|
||||
val noteEvent = note.event ?: return Route.EventRedirect(note.idHex)
|
||||
|
||||
return routeFor(noteEvent, loggedIn)
|
||||
}
|
||||
@@ -62,15 +66,15 @@ fun routeFor(
|
||||
|
||||
if (innerEvent is IsInPublicChatChannel) {
|
||||
innerEvent.channelId()?.let {
|
||||
return Route.Channel(it)
|
||||
return Route.PublicChatChannel(it)
|
||||
}
|
||||
} else if (innerEvent is LiveActivitiesEvent) {
|
||||
innerEvent.aTag().toTag().let {
|
||||
return Route.Channel(it)
|
||||
innerEvent.address().let {
|
||||
return Route.LiveActivityChannel(it.kind, it.pubKeyHex, it.dTag)
|
||||
}
|
||||
} else if (innerEvent is LiveActivitiesChatMessageEvent) {
|
||||
innerEvent.activity()?.toTag()?.let {
|
||||
return Route.Channel(it)
|
||||
innerEvent.activityAddress()?.let {
|
||||
return Route.LiveActivityChannel(it.kind, it.pubKeyHex, it.dTag)
|
||||
}
|
||||
} else if (innerEvent is ChatroomKeyable) {
|
||||
val room = innerEvent.chatroomKey(loggedIn.pubkeyHex)
|
||||
@@ -85,17 +89,17 @@ fun routeFor(
|
||||
return Route.ContentDiscovery(noteEvent.id)
|
||||
} else if (noteEvent is IsInPublicChatChannel) {
|
||||
noteEvent.channelId()?.let {
|
||||
return Route.Channel(it)
|
||||
return Route.PublicChatChannel(it)
|
||||
}
|
||||
} else if (noteEvent is ChannelCreateEvent) {
|
||||
return Route.Channel(noteEvent.id)
|
||||
return Route.PublicChatChannel(noteEvent.id)
|
||||
} else if (noteEvent is LiveActivitiesEvent) {
|
||||
noteEvent.aTag().toTag().let {
|
||||
return Route.Channel(it)
|
||||
noteEvent.address().let {
|
||||
return Route.LiveActivityChannel(it.kind, it.pubKeyHex, it.dTag)
|
||||
}
|
||||
} else if (noteEvent is LiveActivitiesChatMessageEvent) {
|
||||
noteEvent.activity()?.toTag()?.let {
|
||||
return Route.Channel(it)
|
||||
noteEvent.activityAddress()?.let {
|
||||
return Route.LiveActivityChannel(it.kind, it.pubKeyHex, it.dTag)
|
||||
}
|
||||
} else if (noteEvent is ChatroomKeyable) {
|
||||
val room = noteEvent.chatroomKey(loggedIn.pubkeyHex)
|
||||
@@ -103,6 +107,14 @@ fun routeFor(
|
||||
return Route.Room(room.hashCode())
|
||||
} else if (noteEvent is CommunityDefinitionEvent) {
|
||||
return Route.Community(noteEvent.kind, noteEvent.pubKey, noteEvent.dTag())
|
||||
} else if (noteEvent is GiftWrapEvent) {
|
||||
noteEvent.innerEventId?.let {
|
||||
return routeFor(LocalCache.getOrCreateNote(it), loggedIn)
|
||||
}
|
||||
} else if (noteEvent is SealedRumorEvent) {
|
||||
noteEvent.innerEventId?.let {
|
||||
return routeFor(LocalCache.getOrCreateNote(it), loggedIn)
|
||||
}
|
||||
} else if (noteEvent is AddressableEvent) {
|
||||
return Route.Note(noteEvent.aTag().toTag())
|
||||
} else {
|
||||
@@ -169,12 +181,11 @@ fun routeToMessage(
|
||||
accountViewModel: AccountViewModel,
|
||||
): Route = routeToMessage(user.pubkeyHex, draftMessage, replyId, draftId, accountViewModel)
|
||||
|
||||
fun routeFor(note: Channel): Route =
|
||||
if (note is EphemeralChatChannel) {
|
||||
Route.EphemeralChat(note.roomId.id, note.roomId.relayUrl.url)
|
||||
} else {
|
||||
Route.Channel(note.idHex)
|
||||
}
|
||||
fun routeFor(note: EphemeralChatChannel): Route = Route.EphemeralChat(note.roomId.id, note.roomId.relayUrl.url)
|
||||
|
||||
fun routeFor(note: PublicChatChannel): Route = Route.PublicChatChannel(note.idHex)
|
||||
|
||||
fun routeFor(note: LiveActivitiesChannel): Route = Route.LiveActivityChannel(note.address.kind, note.address.pubKeyHex, note.address.dTag)
|
||||
|
||||
fun routeFor(roomId: RoomId): Route = Route.EphemeralChat(roomId.id, roomId.relayUrl.url)
|
||||
|
||||
|
||||
@@ -85,7 +85,17 @@ sealed class Route {
|
||||
val dTag: String,
|
||||
) : Route()
|
||||
|
||||
@Serializable data class Channel(
|
||||
@Serializable data class PublicChatChannel(
|
||||
val id: String,
|
||||
) : Route()
|
||||
|
||||
@Serializable data class LiveActivityChannel(
|
||||
val kind: Int,
|
||||
val pubKeyHex: HexKey,
|
||||
val dTag: String,
|
||||
) : Route()
|
||||
|
||||
@Serializable data class EphemeralChatChannel(
|
||||
val id: String,
|
||||
) : Route()
|
||||
|
||||
@@ -204,7 +214,8 @@ fun getRouteWithArguments(navController: NavHostController): Route? {
|
||||
dest.hasRoute<Route.RelayInfo>() -> entry.toRoute<Route.RelayInfo>()
|
||||
|
||||
dest.hasRoute<Route.RoomByAuthor>() -> entry.toRoute<Route.RoomByAuthor>()
|
||||
dest.hasRoute<Route.Channel>() -> entry.toRoute<Route.Channel>()
|
||||
dest.hasRoute<Route.PublicChatChannel>() -> entry.toRoute<Route.PublicChatChannel>()
|
||||
dest.hasRoute<Route.LiveActivityChannel>() -> entry.toRoute<Route.LiveActivityChannel>()
|
||||
dest.hasRoute<Route.ChannelMetadataEdit>() -> entry.toRoute<Route.ChannelMetadataEdit>()
|
||||
dest.hasRoute<Route.EphemeralChat>() -> entry.toRoute<Route.EphemeralChat>()
|
||||
dest.hasRoute<Route.NewEphemeralChat>() -> entry.toRoute<Route.NewEphemeralChat>()
|
||||
|
||||
@@ -31,19 +31,17 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.EphemeralChatChannel
|
||||
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteOts
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserStatuses
|
||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
@@ -161,36 +159,28 @@ fun LoadOts(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoadChannel(
|
||||
baseChannelHex: String,
|
||||
fun LoadPublicChatChannel(
|
||||
id: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
content: @Composable (Channel) -> Unit,
|
||||
content: @Composable (PublicChatChannel) -> Unit,
|
||||
) {
|
||||
var channel by
|
||||
remember(baseChannelHex) {
|
||||
mutableStateOf<Channel?>(accountViewModel.getChannelIfExists(baseChannelHex))
|
||||
val channel =
|
||||
produceStateIfNotNull(accountViewModel.getPublicChatChannelIfExists(id), id) {
|
||||
value = accountViewModel.checkGetOrCreatePublicChatChannel(id)
|
||||
}
|
||||
|
||||
if (channel == null) {
|
||||
LaunchedEffect(key1 = baseChannelHex) {
|
||||
accountViewModel.checkGetOrCreateChannel(baseChannelHex) { newChannel ->
|
||||
launch(Dispatchers.Main) { channel = newChannel }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
channel?.let { content(it) }
|
||||
channel.value?.let { content(it) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoadChannel(
|
||||
id: RoomId,
|
||||
fun LoadLiveActivityChannel(
|
||||
id: Address,
|
||||
accountViewModel: AccountViewModel,
|
||||
content: @Composable (EphemeralChatChannel) -> Unit,
|
||||
content: @Composable (LiveActivitiesChannel) -> Unit,
|
||||
) {
|
||||
val channel =
|
||||
produceStateIfNotNull(accountViewModel.getChannelIfExists(id) as? EphemeralChatChannel, id) {
|
||||
value = accountViewModel.checkGetOrCreateChannel(id) as? EphemeralChatChannel
|
||||
produceStateIfNotNull(accountViewModel.getLiveActivityChannelIfExists(id), id) {
|
||||
value = accountViewModel.checkGetOrCreateLiveActivityChannel(id)
|
||||
}
|
||||
|
||||
channel.value?.let { content(it) }
|
||||
|
||||
@@ -52,9 +52,9 @@ import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.compose.produceCachedStateAsync
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannelPicture
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEdits
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent
|
||||
@@ -125,7 +125,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderTorrentComment
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderWikiContent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.VideoDisplay
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.RenderChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.RenderPublicChatChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font12SP
|
||||
@@ -276,9 +276,8 @@ fun AcceptableNote(
|
||||
is ChannelCreateEvent,
|
||||
is ChannelMetadataEvent,
|
||||
->
|
||||
RenderChannelHeader(
|
||||
RenderPublicChatChannelHeader(
|
||||
channelNote = baseNote,
|
||||
showVideo = !makeItShort,
|
||||
sendToChannel = true,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
@@ -316,9 +315,8 @@ fun AcceptableNote(
|
||||
is ChannelCreateEvent,
|
||||
is ChannelMetadataEvent,
|
||||
->
|
||||
RenderChannelHeader(
|
||||
RenderPublicChatChannelHeader(
|
||||
channelNote = baseNote,
|
||||
showVideo = !makeItShort,
|
||||
sendToChannel = true,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
@@ -1186,7 +1184,7 @@ private fun RenderAuthorImages(
|
||||
if (baseNote.event is ChannelMessageEvent) {
|
||||
val baseChannelHex = remember(baseNote) { baseNote.channelHex() }
|
||||
if (baseChannelHex != null) {
|
||||
LoadChannel(baseChannelHex, accountViewModel) { channel ->
|
||||
LoadPublicChatChannel(baseChannelHex, accountViewModel) { channel ->
|
||||
ChannelNotePicture(
|
||||
channel,
|
||||
accountViewModel,
|
||||
@@ -1198,7 +1196,7 @@ private fun RenderAuthorImages(
|
||||
|
||||
@Composable
|
||||
private fun ChannelNotePicture(
|
||||
baseChannel: Channel,
|
||||
baseChannel: PublicChatChannel,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val model by observeChannelPicture(baseChannel, accountViewModel)
|
||||
|
||||
@@ -23,11 +23,11 @@ package com.vitorpamplona.amethyst.ui.note
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
|
||||
fun ByteArray.toShortenHex(): String = toHexKey().toShortenHex()
|
||||
fun ByteArray.toHexShortDisplay(): String = toHexKey().toShortDisplay()
|
||||
|
||||
fun String.toShortenHex(): String {
|
||||
fun String.toShortDisplay(): String {
|
||||
if (length <= 16) return this
|
||||
return replaceRange(8, length - 8, ":")
|
||||
}
|
||||
|
||||
fun HexKey.toDisplayHexKey(): String = this.toShortenHex()
|
||||
fun HexKey.toDisplayHexKey(): String = this.toShortDisplay()
|
||||
|
||||
+1
-1
@@ -656,7 +656,7 @@ open class CommentPostViewModel :
|
||||
override fun updateZapFromText() {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
val tagger =
|
||||
NewMessageTagger(message.text, emptyList(), emptyList(), null, accountViewModel!!)
|
||||
NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel!!)
|
||||
tagger.run()
|
||||
tagger.pTags?.forEach { taggedUser ->
|
||||
if (!forwardZapTo.value.items.any { it.key == taggedUser }) {
|
||||
|
||||
@@ -33,7 +33,7 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.PublicChatChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
||||
@@ -60,9 +60,8 @@ fun RenderChannelMessage(
|
||||
}
|
||||
|
||||
showChannelInfo?.let {
|
||||
ChannelHeader(
|
||||
PublicChatChannelHeader(
|
||||
channelHex = it,
|
||||
showVideo = false,
|
||||
sendToChannel = true,
|
||||
modifier = MaterialTheme.colorScheme.replyModifier.padding(10.dp),
|
||||
accountViewModel = accountViewModel,
|
||||
|
||||
@@ -139,7 +139,7 @@ fun RenderLiveActivityEventInner(
|
||||
val subject = remember(eventUpdates) { noteEvent.title() }
|
||||
val content = remember(eventUpdates) { noteEvent.summary() }
|
||||
val participants = remember(eventUpdates) { noteEvent.participants() }
|
||||
val status = remember(eventUpdates) { noteEvent.status() }
|
||||
val status = remember(eventUpdates) { noteEvent.statusEnum() }
|
||||
val starts = remember(eventUpdates) { noteEvent.starts() }
|
||||
|
||||
Row(
|
||||
@@ -163,19 +163,22 @@ fun RenderLiveActivityEventInner(
|
||||
|
||||
CrossfadeIfEnabled(targetState = status, label = "RenderLiveActivityEventInner", accountViewModel = accountViewModel) {
|
||||
when (it) {
|
||||
StatusTag.STATUS.LIVE.code -> {
|
||||
StatusTag.STATUS.LIVE -> {
|
||||
media?.let { CrossfadeCheckIfVideoIsOnline(it, accountViewModel) { LiveFlag() } }
|
||||
}
|
||||
|
||||
StatusTag.STATUS.PLANNED.code -> {
|
||||
StatusTag.STATUS.PLANNED -> {
|
||||
ScheduledFlag(starts)
|
||||
}
|
||||
|
||||
StatusTag.STATUS.ENDED -> {}
|
||||
null -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
media?.let { media ->
|
||||
if (status == StatusTag.STATUS.LIVE.code) {
|
||||
if (status == StatusTag.STATUS.LIVE) {
|
||||
CheckIfVideoIsOnline(media, accountViewModel) { isOnline ->
|
||||
if (isOnline) {
|
||||
Row(
|
||||
@@ -210,7 +213,7 @@ fun RenderLiveActivityEventInner(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (status == StatusTag.STATUS.ENDED.code || (status == StatusTag.STATUS.PLANNED.code && (starts ?: 0) < TimeUtils.eightHoursAgo())) {
|
||||
if (status == StatusTag.STATUS.ENDED || (status == StatusTag.STATUS.PLANNED && (starts ?: 0) < TimeUtils.eightHoursAgo())) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier =
|
||||
|
||||
+8
-4
@@ -32,8 +32,9 @@ import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadLiveActivityChannel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveActivitiesChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
||||
@@ -53,21 +54,24 @@ fun RenderLiveActivityChatMessage(
|
||||
val showChannelInfo =
|
||||
remember(noteEvent) {
|
||||
if (noteEvent is LiveActivitiesChatMessageEvent) {
|
||||
noteEvent.activity()?.toTag()
|
||||
noteEvent.activityAddress()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
showChannelInfo?.let {
|
||||
ChannelHeader(
|
||||
channelHex = it,
|
||||
LoadLiveActivityChannel(it, accountViewModel) {
|
||||
LiveActivitiesChannelHeader(
|
||||
baseChannel = it,
|
||||
showVideo = false,
|
||||
showFlag = true,
|
||||
sendToChannel = true,
|
||||
modifier = MaterialTheme.colorScheme.replyModifier.padding(10.dp),
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = StdVertSpacer)
|
||||
}
|
||||
|
||||
|
||||
+11
-6
@@ -48,6 +48,7 @@ import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.EphemeralChatChannel
|
||||
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||
@@ -1116,20 +1117,24 @@ class AccountViewModel(
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun checkGetOrCreateChannel(key: HexKey): Channel? = LocalCache.checkGetOrCreateChannel(key)
|
||||
suspend fun checkGetOrCreatePublicChatChannel(key: HexKey): PublicChatChannel? = LocalCache.getOrCreatePublicChatChannel(key)
|
||||
|
||||
suspend fun checkGetOrCreateChannel(key: RoomId): Channel? = LocalCache.getOrCreateEphemeralChannel(key)
|
||||
suspend fun checkGetOrCreateLiveActivityChannel(key: Address): LiveActivitiesChannel? = LocalCache.getOrCreateLiveChannel(key)
|
||||
|
||||
suspend fun checkGetOrCreateEphemeralChatChannel(key: RoomId): EphemeralChatChannel? = LocalCache.getOrCreateEphemeralChannel(key)
|
||||
|
||||
fun checkGetOrCreateChannel(
|
||||
key: HexKey,
|
||||
onResult: (Channel?) -> Unit,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) { onResult(checkGetOrCreateChannel(key)) }
|
||||
viewModelScope.launch(Dispatchers.IO) { onResult(checkGetOrCreatePublicChatChannel(key)) }
|
||||
}
|
||||
|
||||
fun getChannelIfExists(hex: HexKey): Channel? = LocalCache.getChannelIfExists(hex)
|
||||
fun getPublicChatChannelIfExists(hex: HexKey) = LocalCache.getPublicChatChannelIfExists(hex)
|
||||
|
||||
fun getChannelIfExists(key: RoomId): Channel? = LocalCache.getChannelIfExists(key.toKey())
|
||||
fun getEphemeralChatChannelIfExists(key: RoomId) = LocalCache.getEphemeralChatChannelIfExists(key)
|
||||
|
||||
fun getLiveActivityChannelIfExists(key: Address) = LocalCache.getLiveActivityChannelIfExists(key)
|
||||
|
||||
fun <T : PubKeyReferenceTag> loadParticipants(
|
||||
participants: List<T>,
|
||||
@@ -1226,7 +1231,7 @@ class AccountViewModel(
|
||||
route?.let {
|
||||
if (route is Route.Room) {
|
||||
account.markAsRead("Room/${route.id}", date)
|
||||
} else if (route is Route.Channel) {
|
||||
} else if (route is Route.PublicChatChannel) {
|
||||
account.markAsRead("Channel/${route.id}", date)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-7
@@ -42,7 +42,6 @@ import androidx.compose.ui.Alignment.Companion.CenterStart
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.logTime
|
||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
@@ -60,7 +59,6 @@ import com.vitorpamplona.amethyst.ui.note.ZapReaction
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.DisplayZapSplits
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DisplayLocation
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DisplayPoW
|
||||
import com.vitorpamplona.amethyst.ui.note.externalLinkForNote
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.layouts.ChatBubbleLayout
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderChangeChannelMetadataNote
|
||||
@@ -98,9 +96,6 @@ fun ChatroomMessageCompose(
|
||||
nav: INav,
|
||||
onWantsToReply: (Note) -> Unit,
|
||||
onWantsToEditDraft: (Note) -> Unit,
|
||||
) {
|
||||
logTime(
|
||||
debugMessage = { "ChatroomMessageCompose " + externalLinkForNote(baseNote) },
|
||||
) {
|
||||
WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel) {
|
||||
WatchBlockAndReport(
|
||||
@@ -124,7 +119,6 @@ fun ChatroomMessageCompose(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NormalChatNote(
|
||||
@@ -176,7 +170,7 @@ fun NormalChatNote(
|
||||
parentBackgroundColor = parentBackgroundColor,
|
||||
onClick = {
|
||||
if (note.event is ChannelCreateEvent) {
|
||||
nav.nav(Route.Channel(note.idHex))
|
||||
nav.nav(Route.PublicChatChannel(note.idHex))
|
||||
true
|
||||
} else {
|
||||
false
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
class ChatroomFilterSubAssembler(
|
||||
client: NostrClient,
|
||||
allKeys: () -> Set<ChatroomQueryState>,
|
||||
) : PerUserAndFollowListEoseManager<ChatroomQueryState>(client, allKeys) {
|
||||
) : PerUserAndFollowListEoseManager<ChatroomQueryState, String>(client, allKeys) {
|
||||
override fun updateFilter(
|
||||
key: ChatroomQueryState,
|
||||
since: SincePerRelayMap?,
|
||||
|
||||
+2
-2
@@ -578,7 +578,7 @@ class ChatNewMessageViewModel :
|
||||
fun updateRoomFromUsersInput() {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
delay(300)
|
||||
val toUsersTagger = NewMessageTagger(toUsers.text, null, null, null, accountViewModel!!)
|
||||
val toUsersTagger = NewMessageTagger(toUsers.text, null, null, accountViewModel!!)
|
||||
toUsersTagger.run()
|
||||
|
||||
val users = toUsersTagger.pTags?.mapTo(mutableSetOf()) { it.pubkeyHex }
|
||||
@@ -701,7 +701,7 @@ class ChatNewMessageViewModel :
|
||||
|
||||
override fun updateZapFromText() {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), null, accountViewModel!!)
|
||||
val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel!!)
|
||||
tagger.run()
|
||||
tagger.pTags?.forEach { taggedUser ->
|
||||
if (!forwardZapTo.value.items.any { it.key == taggedUser }) {
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ class ChannelFeedFilter(
|
||||
val channel: Channel,
|
||||
val account: Account,
|
||||
) : AdditiveFeedFilter<Note>() {
|
||||
override fun feedKey(): String = channel.idHex
|
||||
override fun feedKey() = channel
|
||||
|
||||
// returns the last Note of each user.
|
||||
override fun feed(): List<Note> =
|
||||
|
||||
+3
-2
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.EphemeralChatChannel
|
||||
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||
@@ -32,7 +33,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
class ChannelFromUserFilterSubAssembler(
|
||||
client: NostrClient,
|
||||
allKeys: () -> Set<ChannelQueryState>,
|
||||
) : PerUserAndFollowListEoseManager<ChannelQueryState>(client, allKeys) {
|
||||
) : PerUserAndFollowListEoseManager<ChannelQueryState, Channel>(client, allKeys) {
|
||||
override fun updateFilter(
|
||||
key: ChannelQueryState,
|
||||
since: SincePerRelayMap?,
|
||||
@@ -48,5 +49,5 @@ class ChannelFromUserFilterSubAssembler(
|
||||
|
||||
override fun user(key: ChannelQueryState) = key.account.userProfile()
|
||||
|
||||
override fun list(key: ChannelQueryState) = key.channel.idHex
|
||||
override fun list(key: ChannelQueryState) = key.channel
|
||||
}
|
||||
|
||||
+3
-2
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.EphemeralChatChannel
|
||||
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||
@@ -32,7 +33,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
class ChannelPublicFilterSubAssembler(
|
||||
client: NostrClient,
|
||||
allKeys: () -> Set<ChannelQueryState>,
|
||||
) : PerUniqueIdEoseManager<ChannelQueryState>(client, allKeys) {
|
||||
) : PerUniqueIdEoseManager<ChannelQueryState, Channel>(client, allKeys) {
|
||||
override fun updateFilter(
|
||||
key: ChannelQueryState,
|
||||
since: SincePerRelayMap?,
|
||||
@@ -44,5 +45,5 @@ class ChannelPublicFilterSubAssembler(
|
||||
else -> null
|
||||
}
|
||||
|
||||
override fun id(key: ChannelQueryState) = key.channel.idHex
|
||||
override fun id(key: ChannelQueryState) = key.channel
|
||||
}
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ fun filterMessagesToLiveActivities(
|
||||
filter =
|
||||
Filter(
|
||||
kinds = listOf(LiveActivitiesChatMessageEvent.KIND),
|
||||
tags = mapOf("a" to listOfNotNull(channel.idHex)),
|
||||
tags = mapOf("a" to listOfNotNull(channel.address.toValue())),
|
||||
limit = 200,
|
||||
since = since?.get(it)?.time,
|
||||
),
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ fun filterMyMessagesToLiveActivities(
|
||||
filter =
|
||||
Filter(
|
||||
kinds = listOf(LiveActivitiesChatMessageEvent.KIND),
|
||||
tags = mapOf("a" to listOfNotNull(channel.idHex)),
|
||||
tags = mapOf("a" to listOfNotNull(channel.address.toValue())),
|
||||
authors = listOf(pubKey),
|
||||
limit = 50,
|
||||
since = since?.get(it)?.time,
|
||||
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.model.EphemeralChatChannel
|
||||
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dal.ChannelFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelFilterAssemblerSubscription
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.ChannelNewMessageViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.EditFieldRow
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun EphemeralChatChannelView(
|
||||
channelId: RoomId?,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
if (channelId == null) return
|
||||
|
||||
LoadEphemeralChatChannel(channelId, accountViewModel) { ephem ->
|
||||
PrepareChannelViewModels(
|
||||
baseChannel = ephem,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PrepareChannelViewModels(
|
||||
baseChannel: EphemeralChatChannel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val feedViewModel: ChannelFeedViewModel =
|
||||
viewModel(
|
||||
key = baseChannel.roomId.toKey() + "ChannelFeedViewModel",
|
||||
factory =
|
||||
ChannelFeedViewModel.Factory(
|
||||
baseChannel,
|
||||
accountViewModel.account,
|
||||
),
|
||||
)
|
||||
|
||||
val channelScreenModel: ChannelNewMessageViewModel = viewModel()
|
||||
channelScreenModel.init(accountViewModel)
|
||||
channelScreenModel.load(baseChannel)
|
||||
|
||||
ChannelView(
|
||||
channel = baseChannel,
|
||||
feedViewModel = feedViewModel,
|
||||
newPostModel = channelScreenModel,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChannelView(
|
||||
channel: EphemeralChatChannel,
|
||||
feedViewModel: ChannelFeedViewModel,
|
||||
newPostModel: ChannelNewMessageViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
WatchLifecycleAndUpdateModel(feedViewModel)
|
||||
ChannelFilterAssemblerSubscription(channel, accountViewModel.dataSources().channel, accountViewModel)
|
||||
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
Column(
|
||||
modifier =
|
||||
remember {
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(vertical = 0.dp)
|
||||
.weight(1f, true)
|
||||
},
|
||||
) {
|
||||
RefreshingChatroomFeedView(
|
||||
viewModel = feedViewModel,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
routeForLastRead = "Channel/${channel.roomId.toKey()}",
|
||||
avoidDraft = newPostModel.draftTag,
|
||||
onWantsToReply = newPostModel::reply,
|
||||
onWantsToEditDraft = newPostModel::editFromDraft,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
// LAST ROW
|
||||
EditFieldRow(
|
||||
newPostModel,
|
||||
accountViewModel,
|
||||
onSendNewMessage = {
|
||||
scope.launch {
|
||||
feedViewModel.sendToTop()
|
||||
}
|
||||
},
|
||||
nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
+3
-36
@@ -18,51 +18,18 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.vitorpamplona.amethyst.model.EphemeralChatChannel
|
||||
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadChannel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.EphemeralChatTopBar
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.PublicChatTopBar
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.header.LiveActivityTopBar
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
|
||||
|
||||
@Composable
|
||||
fun ChannelScreen(
|
||||
channelId: String?,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
if (channelId == null) return
|
||||
|
||||
DisappearingScaffold(
|
||||
isInvertedLayout = true,
|
||||
topBar = {
|
||||
LoadChannel(channelId, accountViewModel) {
|
||||
when (it) {
|
||||
is EphemeralChatChannel -> EphemeralChatTopBar(it, accountViewModel, nav)
|
||||
is PublicChatChannel -> PublicChatTopBar(it, accountViewModel, nav)
|
||||
is LiveActivitiesChannel -> LiveActivityTopBar(it, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
) {
|
||||
Column(Modifier.padding(it)) {
|
||||
ChannelView(channelId, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EphemeralChatScreen(
|
||||
channelId: RoomId,
|
||||
@@ -72,14 +39,14 @@ fun EphemeralChatScreen(
|
||||
DisappearingScaffold(
|
||||
isInvertedLayout = true,
|
||||
topBar = {
|
||||
LoadChannel(channelId, accountViewModel) {
|
||||
LoadEphemeralChatChannel(channelId, accountViewModel) {
|
||||
EphemeralChatTopBar(it, accountViewModel, nav)
|
||||
}
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
) {
|
||||
Column(Modifier.padding(it)) {
|
||||
ChannelView(channelId, accountViewModel, nav)
|
||||
EphemeralChatChannelView(channelId, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.vitorpamplona.amethyst.model.EphemeralChatChannel
|
||||
import com.vitorpamplona.amethyst.ui.note.produceStateIfNotNull
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
|
||||
|
||||
@Composable
|
||||
fun LoadEphemeralChatChannel(
|
||||
id: RoomId,
|
||||
accountViewModel: AccountViewModel,
|
||||
content: @Composable (EphemeralChatChannel) -> Unit,
|
||||
) {
|
||||
val channel =
|
||||
produceStateIfNotNull(accountViewModel.getEphemeralChatChannelIfExists(id), id) {
|
||||
value = accountViewModel.checkGetOrCreateEphemeralChatChannel(id)
|
||||
}
|
||||
|
||||
channel.value?.let { content(it) }
|
||||
}
|
||||
+1
-1
@@ -123,7 +123,7 @@ private fun DrawRelayIcon(
|
||||
val relayInfo by loadRelayInfo(channel.roomId.relayUrl, accountViewModel)
|
||||
|
||||
RobohashFallbackAsyncImage(
|
||||
robot = channel.idHex,
|
||||
robot = channel.roomId.toKey(),
|
||||
model = relayInfo?.icon,
|
||||
contentDescription = stringRes(R.string.profile_image),
|
||||
contentScale = ContentScale.Crop,
|
||||
|
||||
+7
-30
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -30,48 +30,28 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadChannel
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dal.ChannelFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelFilterAssemblerSubscription
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.ShowVideoStreaming
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.ChannelNewMessageViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.EditFieldRow
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun ChannelView(
|
||||
fun PublicChatChannelView(
|
||||
channelId: String?,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
if (channelId == null) return
|
||||
|
||||
LoadChannel(channelId, accountViewModel) {
|
||||
PrepareChannelViewModels(
|
||||
baseChannel = it,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChannelView(
|
||||
channelId: RoomId?,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
if (channelId == null) return
|
||||
|
||||
LoadChannel(channelId, accountViewModel) {
|
||||
LoadPublicChatChannel(channelId, accountViewModel) {
|
||||
PrepareChannelViewModels(
|
||||
baseChannel = it,
|
||||
accountViewModel = accountViewModel,
|
||||
@@ -82,7 +62,7 @@ fun ChannelView(
|
||||
|
||||
@Composable
|
||||
fun PrepareChannelViewModels(
|
||||
baseChannel: Channel,
|
||||
baseChannel: PublicChatChannel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
@@ -111,7 +91,7 @@ fun PrepareChannelViewModels(
|
||||
|
||||
@Composable
|
||||
fun ChannelView(
|
||||
channel: Channel,
|
||||
channel: PublicChatChannel,
|
||||
feedViewModel: ChannelFeedViewModel,
|
||||
newPostModel: ChannelNewMessageViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
@@ -130,9 +110,6 @@ fun ChannelView(
|
||||
.weight(1f, true)
|
||||
},
|
||||
) {
|
||||
if (channel is LiveActivitiesChannel) {
|
||||
ShowVideoStreaming(channel, accountViewModel)
|
||||
}
|
||||
RefreshingChatroomFeedView(
|
||||
viewModel = feedViewModel,
|
||||
accountViewModel = accountViewModel,
|
||||
+6
-37
@@ -18,38 +18,31 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.vitorpamplona.amethyst.model.EphemeralChatChannel
|
||||
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadChannel
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.EphemeralChatChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.PublicChatChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveActivitiesChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.innerPostModifier
|
||||
|
||||
@Composable
|
||||
fun RenderChannelHeader(
|
||||
fun RenderPublicChatChannelHeader(
|
||||
channelNote: Note,
|
||||
showVideo: Boolean,
|
||||
sendToChannel: Boolean,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
channelNote.channelHex()?.let {
|
||||
ChannelHeader(
|
||||
PublicChatChannelHeader(
|
||||
channelHex = it,
|
||||
showVideo = showVideo,
|
||||
sendToChannel = sendToChannel,
|
||||
modifier = MaterialTheme.colorScheme.innerPostModifier.padding(Size10dp),
|
||||
accountViewModel = accountViewModel,
|
||||
@@ -59,28 +52,14 @@ fun RenderChannelHeader(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChannelHeader(
|
||||
fun PublicChatChannelHeader(
|
||||
channelHex: String,
|
||||
showVideo: Boolean,
|
||||
showFlag: Boolean = true,
|
||||
sendToChannel: Boolean = false,
|
||||
modifier: Modifier = StdPadding,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
LoadChannel(channelHex, accountViewModel) {
|
||||
when (it) {
|
||||
is LiveActivitiesChannel ->
|
||||
LiveActivitiesChannelHeader(
|
||||
it,
|
||||
showVideo,
|
||||
showFlag,
|
||||
sendToChannel,
|
||||
modifier,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
is PublicChatChannel ->
|
||||
LoadPublicChatChannel(channelHex, accountViewModel) {
|
||||
PublicChatChannelHeader(
|
||||
it,
|
||||
sendToChannel,
|
||||
@@ -88,15 +67,5 @@ fun ChannelHeader(
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
|
||||
is EphemeralChatChannel ->
|
||||
EphemeralChatChannelHeader(
|
||||
it,
|
||||
sendToChannel,
|
||||
modifier,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.PublicChatTopBar
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
|
||||
@Composable
|
||||
fun PublicChatChannelScreen(
|
||||
channelId: HexKey?,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
if (channelId == null) return
|
||||
|
||||
DisappearingScaffold(
|
||||
isInvertedLayout = true,
|
||||
topBar = {
|
||||
LoadPublicChatChannel(channelId, accountViewModel) {
|
||||
PublicChatTopBar(it, accountViewModel, nav)
|
||||
}
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
) {
|
||||
Column(Modifier.padding(it)) {
|
||||
PublicChatChannelView(channelId, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -58,7 +58,7 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.SelectSingleFromGallery
|
||||
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadChannel
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel
|
||||
import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.note.buttons.SaveButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
@@ -86,7 +86,7 @@ fun ChannelMetadataScreen(
|
||||
if (channelId == null) {
|
||||
ChannelMetadataScreen(null as PublicChatChannel?, accountViewModel, nav)
|
||||
} else {
|
||||
LoadChannel(channelId, accountViewModel) {
|
||||
LoadPublicChatChannel(channelId, accountViewModel) {
|
||||
if (it is PublicChatChannel) {
|
||||
ChannelMetadataScreen(it, accountViewModel, nav)
|
||||
}
|
||||
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadLiveActivityChannel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dal.ChannelFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelFilterAssemblerSubscription
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.ChannelNewMessageViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.EditFieldRow
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun LiveActivityChannelView(
|
||||
channelId: Address?,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
if (channelId == null) return
|
||||
|
||||
LoadLiveActivityChannel(channelId, accountViewModel) {
|
||||
PrepareChannelViewModels(
|
||||
baseChannel = it,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PrepareChannelViewModels(
|
||||
baseChannel: LiveActivitiesChannel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val feedViewModel: ChannelFeedViewModel =
|
||||
viewModel(
|
||||
key = baseChannel.address.toValue() + "ChannelFeedViewModel",
|
||||
factory =
|
||||
ChannelFeedViewModel.Factory(
|
||||
baseChannel,
|
||||
accountViewModel.account,
|
||||
),
|
||||
)
|
||||
|
||||
val channelScreenModel: ChannelNewMessageViewModel = viewModel()
|
||||
channelScreenModel.init(accountViewModel)
|
||||
channelScreenModel.load(baseChannel)
|
||||
|
||||
LiveActivityChannelView(
|
||||
channel = baseChannel,
|
||||
feedViewModel = feedViewModel,
|
||||
newPostModel = channelScreenModel,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LiveActivityChannelView(
|
||||
channel: LiveActivitiesChannel,
|
||||
feedViewModel: ChannelFeedViewModel,
|
||||
newPostModel: ChannelNewMessageViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
WatchLifecycleAndUpdateModel(feedViewModel)
|
||||
ChannelFilterAssemblerSubscription(channel, accountViewModel.dataSources().channel, accountViewModel)
|
||||
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
Column(
|
||||
modifier =
|
||||
remember {
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(vertical = 0.dp)
|
||||
.weight(1f, true)
|
||||
},
|
||||
) {
|
||||
ShowVideoStreaming(channel, accountViewModel)
|
||||
RefreshingChatroomFeedView(
|
||||
viewModel = feedViewModel,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
routeForLastRead = "Channel/${channel.address.toValue()}",
|
||||
avoidDraft = newPostModel.draftTag,
|
||||
onWantsToReply = newPostModel::reply,
|
||||
onWantsToEditDraft = newPostModel::editFromDraft,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
// LAST ROW
|
||||
EditFieldRow(
|
||||
newPostModel,
|
||||
accountViewModel,
|
||||
onSendNewMessage = {
|
||||
scope.launch {
|
||||
feedViewModel.sendToTop()
|
||||
}
|
||||
},
|
||||
nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -64,9 +64,9 @@ fun LiveActivitiesChannelHeader(
|
||||
) {
|
||||
ShortLiveActivityChannelHeader(
|
||||
baseChannel = baseChannel,
|
||||
showFlag = showFlag,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
showFlag = showFlag,
|
||||
)
|
||||
|
||||
if (expanded.value) {
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadLiveActivityChannel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.header.LiveActivityTopBar
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||
|
||||
@Composable
|
||||
fun LiveActivityChannelScreen(
|
||||
channelId: Address?,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
if (channelId == null) return
|
||||
|
||||
DisappearingScaffold(
|
||||
isInvertedLayout = true,
|
||||
topBar = {
|
||||
LoadLiveActivityChannel(channelId, accountViewModel) {
|
||||
LiveActivityTopBar(it, accountViewModel, nav)
|
||||
}
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
) {
|
||||
Column(Modifier.padding(it)) {
|
||||
LiveActivityChannelView(channelId, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
-77
@@ -1,77 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.LikeReaction
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapReaction
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.StatusTag
|
||||
|
||||
@Composable
|
||||
fun LiveChannelActionOptions(
|
||||
channel: LiveActivitiesChannel,
|
||||
showFlag: Boolean = true,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val isLive by remember(channel) { derivedStateOf { channel.info?.status() == StatusTag.STATUS.LIVE.code } }
|
||||
|
||||
val note = remember(channel.idHex) { LocalCache.getNoteIfExists(channel.idHex) }
|
||||
|
||||
note?.let {
|
||||
if (showFlag && isLive) {
|
||||
LiveFlag()
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = RowColSpacing,
|
||||
) {
|
||||
LikeReaction(
|
||||
baseNote = it,
|
||||
grayTint = MaterialTheme.colorScheme.onSurface,
|
||||
accountViewModel = accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
ZapReaction(
|
||||
baseNote = it,
|
||||
grayTint = MaterialTheme.colorScheme.onSurface,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
+41
-37
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
@@ -43,11 +44,11 @@ import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel
|
||||
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
@@ -60,8 +61,8 @@ import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags
|
||||
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
@@ -79,44 +80,14 @@ fun LongLiveActivityChannelHeader(
|
||||
) {
|
||||
val channelState by observeChannel(baseChannel, accountViewModel)
|
||||
val channel = channelState?.channel as? LiveActivitiesChannel ?: return
|
||||
val activity = channel.info ?: return
|
||||
val callbackUri = remember(channel) { channel.toNostrUri() }
|
||||
|
||||
Row(
|
||||
lineModifier,
|
||||
) {
|
||||
val summary = remember(channelState) { channel.summary()?.ifBlank { null } }
|
||||
|
||||
Column(
|
||||
Modifier.weight(1f),
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
val defaultBackground = MaterialTheme.colorScheme.background
|
||||
val background = remember { mutableStateOf(defaultBackground) }
|
||||
|
||||
val tags = remember(channelState) { baseChannel.info?.tags?.toImmutableListOfLists() ?: EmptyTagList }
|
||||
|
||||
TranslatableRichTextViewer(
|
||||
content = summary ?: stringRes(id = R.string.groups_no_descriptor),
|
||||
canPreview = false,
|
||||
quotesLeft = 1,
|
||||
tags = tags,
|
||||
backgroundColor = background,
|
||||
id = baseChannel.idHex,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
Row(lineModifier) {
|
||||
RenderSummary(activity, callbackUri, accountViewModel, nav)
|
||||
}
|
||||
|
||||
if (summary != null) {
|
||||
baseChannel.info?.let {
|
||||
if (it.hasHashtags()) {
|
||||
DisplayUncitedHashtags(it, summary, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LoadNote(baseNoteHex = channel.idHex, accountViewModel) { loadingNote ->
|
||||
LoadAddressableNote(channel.address, accountViewModel) { loadingNote ->
|
||||
loadingNote?.let { note ->
|
||||
Row(
|
||||
lineModifier,
|
||||
@@ -197,3 +168,36 @@ fun LongLiveActivityChannelHeader(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.RenderSummary(
|
||||
activity: LiveActivitiesEvent,
|
||||
callbackUri: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val summary = activity.summary() ?: stringRes(id = R.string.groups_no_descriptor)
|
||||
|
||||
Column(Modifier.weight(1f)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
val defaultBackground = MaterialTheme.colorScheme.background
|
||||
val background = remember { mutableStateOf(defaultBackground) }
|
||||
|
||||
TranslatableRichTextViewer(
|
||||
content = summary,
|
||||
canPreview = false,
|
||||
quotesLeft = 1,
|
||||
tags = activity.tags.toImmutableListOfLists(),
|
||||
backgroundColor = background,
|
||||
id = activity.id,
|
||||
callbackUri = callbackUri,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
|
||||
if (activity.hasHashtags()) {
|
||||
DisplayUncitedHashtags(activity, summary, callbackUri, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+72
-4
@@ -23,10 +23,13 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -34,25 +37,51 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.LikeReaction
|
||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapReaction
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size34dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
|
||||
@Composable
|
||||
fun ShortLiveActivityChannelHeader(
|
||||
baseChannel: LiveActivitiesChannel,
|
||||
showFlag: Boolean,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
showFlag: Boolean,
|
||||
) {
|
||||
val channelState by observeChannel(baseChannel, accountViewModel)
|
||||
val channel = channelState?.channel as? LiveActivitiesChannel ?: return
|
||||
|
||||
ShortLiveActivityChannelHeader(
|
||||
name = channel.toBestDisplayName(),
|
||||
creator = channel.creator,
|
||||
liveActivitiesEvent = channel.info,
|
||||
showFlag = showFlag,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ShortLiveActivityChannelHeader(
|
||||
name: String,
|
||||
creator: User?,
|
||||
liveActivitiesEvent: LiveActivitiesEvent?,
|
||||
showFlag: Boolean,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
channel.creator?.let {
|
||||
creator?.let {
|
||||
UserPicture(
|
||||
user = it,
|
||||
size = Size34dp,
|
||||
@@ -71,13 +100,14 @@ fun ShortLiveActivityChannelHeader(
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = remember(channelState) { channel.toBestDisplayName() },
|
||||
text = name,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
liveActivitiesEvent?.let {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
@@ -85,7 +115,45 @@ fun ShortLiveActivityChannelHeader(
|
||||
.padding(start = 5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
LiveChannelActionOptions(channel, showFlag, accountViewModel, nav)
|
||||
LiveChannelActionOptions(it, showFlag, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LiveChannelActionOptions(
|
||||
activity: LiveActivitiesEvent,
|
||||
showFlag: Boolean = true,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val isLive by remember(activity) { derivedStateOf { activity.isLive() } }
|
||||
|
||||
if (showFlag && isLive) {
|
||||
LiveFlag()
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
}
|
||||
|
||||
val note = remember(activity) { LocalCache.getAddressableNoteIfExists(activity.address()) }
|
||||
note?.let {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = RowColSpacing,
|
||||
) {
|
||||
LikeReaction(
|
||||
baseNote = it,
|
||||
grayTint = MaterialTheme.colorScheme.onSurface,
|
||||
accountViewModel = accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
ZapReaction(
|
||||
baseNote = it,
|
||||
grayTint = MaterialTheme.colorScheme.onSurface,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -38,9 +38,9 @@ fun LiveActivityTopBar(
|
||||
title = {
|
||||
ShortLiveActivityChannelHeader(
|
||||
baseChannel = baseChannel,
|
||||
showFlag = true,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
showFlag = true,
|
||||
)
|
||||
},
|
||||
extendableRow = {
|
||||
|
||||
+1
-2
@@ -364,7 +364,6 @@ open class ChannelNewMessageViewModel :
|
||||
message = message.text,
|
||||
pTags = listOfNotNull(replyTo.value?.author),
|
||||
eTags = listOfNotNull(replyTo.value),
|
||||
channelHex = channel.idHex,
|
||||
dao = accountViewModel,
|
||||
)
|
||||
tagger.run()
|
||||
@@ -627,7 +626,7 @@ open class ChannelNewMessageViewModel :
|
||||
|
||||
fun updateZapFromText() {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), null, accountViewModel!!)
|
||||
val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel!!)
|
||||
tagger.run()
|
||||
tagger.pTags?.forEach { taggedUser ->
|
||||
if (!forwardZapTo.items.any { it.key == taggedUser }) {
|
||||
|
||||
+36
-105
@@ -32,7 +32,6 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -53,8 +52,6 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.logTime
|
||||
import com.vitorpamplona.amethyst.model.EphemeralChatChannel
|
||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||
@@ -62,31 +59,29 @@ import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteHasEvent
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.layouts.ChatHeaderLayout
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.BlankNote
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadChannel
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContentOrNull
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel
|
||||
import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures
|
||||
import com.vitorpamplona.amethyst.ui.note.ObserveDraftEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.externalLinkForNote
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgo
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.RoomNameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.loadRelayInfo
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.AccountPictureModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
|
||||
@Composable
|
||||
@@ -94,9 +89,6 @@ fun ChatroomHeaderCompose(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
logTime(
|
||||
debugMessage = { "ChatroomHeaderCompose " + externalLinkForNote(baseNote) },
|
||||
) {
|
||||
if (baseNote.event != null) {
|
||||
ChatroomComposeChannelOrUser(baseNote, accountViewModel, nav)
|
||||
@@ -109,7 +101,6 @@ fun ChatroomHeaderCompose(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatroomComposeChannelOrUser(
|
||||
@@ -117,80 +108,62 @@ fun ChatroomComposeChannelOrUser(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
if (baseNote.event is DraftEvent) {
|
||||
ObserveDraftEvent(baseNote, accountViewModel) {
|
||||
val channelHex by remember(it) { derivedStateOf { it.channelHex() } }
|
||||
|
||||
if (channelHex != null) {
|
||||
ChatroomChannel(channelHex!!, it, accountViewModel, nav)
|
||||
} else {
|
||||
ChatroomPrivateMessages(it, accountViewModel, nav)
|
||||
}
|
||||
val baseNoteEvent = baseNote.event
|
||||
if (baseNoteEvent is DraftEvent) {
|
||||
ObserveDraftEvent(baseNote, accountViewModel) { innerNote ->
|
||||
ChatroomEntry(innerNote, accountViewModel, nav)
|
||||
}
|
||||
} else {
|
||||
val channelHex by remember(baseNote) { derivedStateOf { baseNote.channelHex() } }
|
||||
|
||||
if (channelHex != null) {
|
||||
ChatroomChannel(channelHex!!, baseNote, accountViewModel, nav)
|
||||
} else {
|
||||
ChatroomPrivateMessages(baseNote, accountViewModel, nav)
|
||||
}
|
||||
ChatroomEntry(baseNote, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatroomPrivateMessages(
|
||||
baseNote: Note,
|
||||
private fun ChatroomEntry(
|
||||
lastMessage: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val userRoom by
|
||||
remember(baseNote) {
|
||||
derivedStateOf {
|
||||
(baseNote.event as? ChatroomKeyable)?.chatroomKey(accountViewModel.userProfile().pubkeyHex)
|
||||
val baseNoteEvent = lastMessage.event
|
||||
when (baseNoteEvent) {
|
||||
is ChannelMessageEvent ->
|
||||
baseNoteEvent.channelId()?.let {
|
||||
LoadPublicChatChannel(it, accountViewModel) { channel ->
|
||||
ChannelRoomCompose(lastMessage, channel, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
|
||||
CrossfadeIfEnabled(targetState = userRoom, label = "ChatroomPrivateMessages", accountViewModel = accountViewModel) { room ->
|
||||
if (room != null) {
|
||||
UserRoomCompose(baseNote, room, accountViewModel, nav)
|
||||
} else {
|
||||
BlankNote()
|
||||
is ChannelMetadataEvent ->
|
||||
baseNoteEvent.channelId()?.let {
|
||||
LoadPublicChatChannel(it, accountViewModel) { channel ->
|
||||
ChannelRoomCompose(lastMessage, channel, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
is ChannelCreateEvent ->
|
||||
LoadPublicChatChannel(baseNoteEvent.id, accountViewModel) { channel ->
|
||||
ChannelRoomCompose(lastMessage, channel, accountViewModel, nav)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatroomChannel(
|
||||
channelHex: HexKey,
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
LoadChannel(baseChannelHex = channelHex, accountViewModel) { channel ->
|
||||
when (channel) {
|
||||
is PublicChatChannel -> ChannelRoomCompose(baseNote, channel, accountViewModel, nav)
|
||||
is EphemeralChatChannel -> ChannelRoomCompose(baseNote, channel, accountViewModel, nav)
|
||||
is ChatroomKeyable -> {
|
||||
val room = baseNoteEvent.chatroomKey(accountViewModel.userProfile().pubkeyHex)
|
||||
UserRoomCompose(room, lastMessage, accountViewModel, nav)
|
||||
}
|
||||
else -> BlankNote()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChannelRoomCompose(
|
||||
note: Note,
|
||||
lastMessage: Note,
|
||||
channel: PublicChatChannel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val authorName by observeUserName(note.author!!, accountViewModel)
|
||||
val authorName by observeUserName(lastMessage.author!!, accountViewModel)
|
||||
val channelState by observeChannel(channel, accountViewModel)
|
||||
|
||||
val channelPicture = channelState?.channel?.profilePicture() ?: channel.profilePicture()
|
||||
val channelName = channelState?.channel?.toBestDisplayName() ?: channel.toBestDisplayName()
|
||||
|
||||
val noteEvent = note.event
|
||||
|
||||
val route = Route.Channel(channel.idHex)
|
||||
val noteEvent = lastMessage.event
|
||||
|
||||
val description =
|
||||
if (noteEvent is ChannelCreateEvent) {
|
||||
@@ -207,54 +180,12 @@ private fun ChannelRoomCompose(
|
||||
channelIdHex = channel.idHex,
|
||||
channelPicture = channelPicture,
|
||||
channelTitle = { modifier -> ChannelTitleWithLabelInfo(channelName, R.string.public_chat, modifier) },
|
||||
channelLastTime = note.createdAt(),
|
||||
channelLastTime = lastMessage.createdAt(),
|
||||
channelLastContent = "$authorName: $description",
|
||||
hasNewMessages = (noteEvent?.createdAt ?: Long.MIN_VALUE) > lastReadTime,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
||||
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
|
||||
onClick = { nav.nav(route) },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChannelRoomCompose(
|
||||
note: Note,
|
||||
channel: EphemeralChatChannel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val authorName by observeUserName(note.author!!, accountViewModel)
|
||||
val channelState by observeChannel(channel, accountViewModel)
|
||||
|
||||
val relayInfo by loadRelayInfo(channel.roomId.relayUrl, accountViewModel)
|
||||
|
||||
val channelName = channelState?.channel?.toBestDisplayName() ?: channel.toBestDisplayName()
|
||||
|
||||
val noteEvent = note.event
|
||||
|
||||
val route = Route.Channel(channel.idHex)
|
||||
|
||||
val description =
|
||||
if (noteEvent is ChannelCreateEvent) {
|
||||
stringRes(R.string.channel_created)
|
||||
} else if (noteEvent is ChannelMetadataEvent) {
|
||||
"${stringRes(R.string.channel_information_changed_to)} "
|
||||
} else {
|
||||
noteEvent?.content?.take(200)
|
||||
}
|
||||
|
||||
val lastReadTime by accountViewModel.account.loadLastReadFlow("Channel/${channel.idHex}").collectAsStateWithLifecycle()
|
||||
|
||||
ChannelName(
|
||||
channelIdHex = channel.idHex,
|
||||
channelPicture = relayInfo?.icon,
|
||||
channelTitle = { modifier -> ChannelTitleWithLabelInfo(channelName, R.string.ephemeral_relay_chat, modifier) },
|
||||
channelLastTime = note.createdAt(),
|
||||
channelLastContent = "$authorName: $description",
|
||||
hasNewMessages = (noteEvent?.createdAt ?: Long.MIN_VALUE) > lastReadTime,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
||||
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
|
||||
onClick = { nav.nav(route) },
|
||||
onClick = { nav.nav(routeFor(channel)) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -300,8 +231,8 @@ private fun ChannelTitleWithLabelInfo(
|
||||
|
||||
@Composable
|
||||
private fun UserRoomCompose(
|
||||
note: Note,
|
||||
room: ChatroomKey,
|
||||
lastMessage: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
@@ -315,10 +246,10 @@ private fun UserRoomCompose(
|
||||
},
|
||||
firstRow = {
|
||||
RoomNameDisplay(room, Modifier.weight(1f), accountViewModel)
|
||||
TimeAgo(note.createdAt())
|
||||
TimeAgo(lastMessage.createdAt())
|
||||
},
|
||||
secondRow = {
|
||||
LoadDecryptedContentOrNull(note, accountViewModel) { content ->
|
||||
LoadDecryptedContentOrNull(lastMessage, accountViewModel) { content ->
|
||||
if (content != null) {
|
||||
Text(
|
||||
content,
|
||||
@@ -340,7 +271,7 @@ private fun UserRoomCompose(
|
||||
}
|
||||
|
||||
val lastReadTime by accountViewModel.account.loadLastReadFlow("Room/${room.hashCode()}").collectAsStateWithLifecycle()
|
||||
if ((note.createdAt() ?: Long.MIN_VALUE) > lastReadTime) {
|
||||
if ((lastMessage.createdAt() ?: Long.MIN_VALUE) > lastReadTime) {
|
||||
NewItemsBubble()
|
||||
}
|
||||
},
|
||||
|
||||
+2
-2
@@ -58,7 +58,7 @@ class ChatroomListKnownFeedFilter(
|
||||
val publicChannels =
|
||||
account
|
||||
.publicChatList.flow.value
|
||||
.mapNotNull { LocalCache.getChannelIfExists(it.eventId) }
|
||||
.mapNotNull { LocalCache.getPublicChatChannelIfExists(it.eventId) }
|
||||
.mapNotNull { it ->
|
||||
it.notes
|
||||
.filter { key, it -> account.isAcceptable(it) && it.event != null }
|
||||
@@ -69,7 +69,7 @@ class ChatroomListKnownFeedFilter(
|
||||
val ephemeralChats =
|
||||
account
|
||||
.ephemeralChatList.liveEphemeralChatList.value
|
||||
.mapNotNull { LocalCache.getChannelIfExists(it) }
|
||||
.mapNotNull { LocalCache.getEphemeralChatChannelIfExists(it) }
|
||||
.mapNotNull { it ->
|
||||
it.notes
|
||||
.filter { key, it -> account.isAcceptable(it) && it.event != null }
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ fun filterFollowingPublicChats(
|
||||
mapOfSet {
|
||||
followingChannels.forEach { channelId ->
|
||||
val relays =
|
||||
LocalCache.getChannelIfExists(channelId)?.relays()
|
||||
LocalCache.getPublicChatChannelIfExists(channelId)?.relays()
|
||||
?: LocalCache.relayHints.hintsForEvent(channelId).ifEmpty { null }
|
||||
?: Constants.eventFinderRelays
|
||||
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ fun filterLastMessageFollowingPublicChats(
|
||||
mapOfSet {
|
||||
followingChannels.forEach { channelId ->
|
||||
val relays =
|
||||
LocalCache.getChannelIfExists(channelId)?.relays()
|
||||
LocalCache.getPublicChatChannelIfExists(channelId)?.relays()
|
||||
?: LocalCache.relayHints.hintsForEvent(channelId).ifEmpty { null }
|
||||
?: Constants.eventFinderRelays
|
||||
|
||||
|
||||
+5
-9
@@ -43,7 +43,7 @@ import com.vitorpamplona.amethyst.ui.navigation.MainTopBar
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.Chatroom
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ChannelView
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.PublicChatChannelView
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.ChannelFabColumn
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||
|
||||
@@ -62,13 +62,9 @@ fun MessagesTwoPane(
|
||||
val strategy =
|
||||
remember {
|
||||
if (widthSizeClass == WindowWidthSizeClass.Expanded) {
|
||||
HorizontalTwoPaneStrategy(
|
||||
splitFraction = 1f / 3f,
|
||||
)
|
||||
HorizontalTwoPaneStrategy(splitFraction = 1f / 3f)
|
||||
} else {
|
||||
HorizontalTwoPaneStrategy(
|
||||
splitFraction = 1f / 2.5f,
|
||||
)
|
||||
HorizontalTwoPaneStrategy(splitFraction = 1f / 2.5f)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,8 +113,8 @@ fun MessagesTwoPane(
|
||||
)
|
||||
}
|
||||
|
||||
if (it is Route.Channel) {
|
||||
ChannelView(
|
||||
if (it is Route.PublicChatChannel) {
|
||||
PublicChatChannelView(
|
||||
channelId = it.id,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
|
||||
+6
-6
@@ -38,18 +38,18 @@ class TwoPaneNav(
|
||||
val innerNav = mutableStateOf<Route?>(null)
|
||||
|
||||
override fun nav(route: Route) {
|
||||
if (route is Route.Room || route is Route.Channel) {
|
||||
if (route is Route.Room || route is Route.PublicChatChannel) {
|
||||
innerNav.value = route
|
||||
} else {
|
||||
nav.nav(route)
|
||||
}
|
||||
}
|
||||
|
||||
override fun nav(routeMaker: suspend () -> Route?) {
|
||||
override fun nav(computeRoute: suspend () -> Route?) {
|
||||
scope.launch(Dispatchers.Default) {
|
||||
val route = routeMaker()
|
||||
val route = computeRoute()
|
||||
if (route != null) {
|
||||
if (route is Route.Room || route is Route.Channel) {
|
||||
if (route is Route.Room || route is Route.PublicChatChannel) {
|
||||
innerNav.value = route
|
||||
} else {
|
||||
nav.nav(route)
|
||||
@@ -68,9 +68,9 @@ class TwoPaneNav(
|
||||
|
||||
override fun <T : Route> popUpTo(
|
||||
route: Route,
|
||||
upToClass: KClass<T>,
|
||||
klass: KClass<T>,
|
||||
) {
|
||||
nav.popUpTo<T>(route, upToClass)
|
||||
nav.popUpTo<T>(route, klass)
|
||||
}
|
||||
|
||||
override fun closeDrawer() {
|
||||
|
||||
+2
-2
@@ -35,7 +35,7 @@ import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.calculateBackgroundColor
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.RenderLongFormThumb
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.RenderChannelThumb
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.RenderPublicChatChannelThumb
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.RenderFollowSetThumb
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.RenderLiveActivityThumb
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.RenderCommunitiesThumb
|
||||
@@ -203,7 +203,7 @@ private fun RenderNoteRow(
|
||||
when (baseNote.event) {
|
||||
is LiveActivitiesEvent -> RenderLiveActivityThumb(baseNote, accountViewModel, nav)
|
||||
is CommunityDefinitionEvent -> RenderCommunitiesThumb(baseNote, accountViewModel, nav)
|
||||
is ChannelCreateEvent -> RenderChannelThumb(baseNote, accountViewModel, nav)
|
||||
is ChannelCreateEvent -> RenderPublicChatChannelThumb(baseNote, accountViewModel, nav)
|
||||
is AppDefinitionEvent -> RenderContentDVMThumb(baseNote, accountViewModel, nav)
|
||||
is FollowListEvent -> RenderFollowSetThumb(baseNote, accountViewModel, nav)
|
||||
is LongTextNoteEvent -> RenderLongFormThumb(baseNote, accountViewModel, nav)
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ import kotlinx.coroutines.launch
|
||||
class DiscoveryFollowsDiscoverySubAssembler1(
|
||||
client: NostrClient,
|
||||
allKeys: () -> Set<DiscoveryQueryState>,
|
||||
) : PerUserAndFollowListEoseManager<DiscoveryQueryState>(client, allKeys) {
|
||||
) : PerUserAndFollowListEoseManager<DiscoveryQueryState, String>(client, allKeys) {
|
||||
override fun updateFilter(
|
||||
key: DiscoveryQueryState,
|
||||
since: SincePerRelayMap?,
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ import kotlinx.coroutines.launch
|
||||
class DiscoveryFollowsDiscoverySubAssembler2(
|
||||
client: NostrClient,
|
||||
allKeys: () -> Set<DiscoveryQueryState>,
|
||||
) : PerUserAndFollowListEoseManager<DiscoveryQueryState>(client, allKeys) {
|
||||
) : PerUserAndFollowListEoseManager<DiscoveryQueryState, String>(client, allKeys) {
|
||||
override fun updateFilter(
|
||||
key: DiscoveryQueryState,
|
||||
since: SincePerRelayMap?,
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ import kotlinx.coroutines.launch
|
||||
class DiscoveryFollowsDiscoverySubAssembler3(
|
||||
client: NostrClient,
|
||||
allKeys: () -> Set<DiscoveryQueryState>,
|
||||
) : PerUserAndFollowListEoseManager<DiscoveryQueryState>(client, allKeys) {
|
||||
) : PerUserAndFollowListEoseManager<DiscoveryQueryState, String>(client, allKeys) {
|
||||
override fun updateFilter(
|
||||
key: DiscoveryQueryState,
|
||||
since: SincePerRelayMap?,
|
||||
|
||||
+3
-3
@@ -74,7 +74,7 @@ open class DiscoverChatFeedFilter(
|
||||
// note event here will never be null
|
||||
val noteEvent = note.event
|
||||
if (noteEvent is ChannelCreateEvent && params.match(noteEvent)) {
|
||||
if ((LocalCache.getChannelIfExists(noteEvent.id)?.notes?.size() ?: 0) > 0) {
|
||||
if ((LocalCache.getPublicChatChannelIfExists(noteEvent.id)?.notes?.size() ?: 0) > 0) {
|
||||
note
|
||||
} else {
|
||||
null
|
||||
@@ -86,7 +86,7 @@ open class DiscoverChatFeedFilter(
|
||||
if (channel != null &&
|
||||
(channelEvent == null || (channelEvent is ChannelCreateEvent && params.match(channelEvent)))
|
||||
) {
|
||||
if ((LocalCache.getChannelIfExists(channel.idHex)?.notes?.size() ?: 0) > 0) {
|
||||
if ((LocalCache.getPublicChatChannelIfExists(channel.idHex)?.notes?.size() ?: 0) > 0) {
|
||||
channel
|
||||
} else {
|
||||
null
|
||||
@@ -103,7 +103,7 @@ open class DiscoverChatFeedFilter(
|
||||
override fun sort(collection: Set<Note>): List<Note> {
|
||||
val lastNote =
|
||||
collection.associateWith { note ->
|
||||
LocalCache.getChannelIfExists(note.idHex)?.lastNoteCreatedAt ?: 0
|
||||
LocalCache.getPublicChatChannelIfExists(note.idHex)?.lastNoteCreatedAt ?: 0
|
||||
}
|
||||
|
||||
return collection
|
||||
|
||||
+7
-7
@@ -41,9 +41,9 @@ import androidx.compose.ui.unit.sp
|
||||
import coil3.compose.AsyncImage
|
||||
import coil3.compose.AsyncImagePainter
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.ParticipantListBuilder
|
||||
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavFilter
|
||||
@@ -55,7 +55,7 @@ import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.DisplayAuthorBanner
|
||||
import com.vitorpamplona.amethyst.ui.note.Gallery
|
||||
import com.vitorpamplona.amethyst.ui.note.LikeReaction
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadChannel
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapReaction
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
|
||||
@@ -73,22 +73,22 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun RenderChannelThumb(
|
||||
fun RenderPublicChatChannelThumb(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val noteEvent = baseNote.event as? ChannelCreateEvent ?: return
|
||||
|
||||
LoadChannel(baseChannelHex = baseNote.idHex, accountViewModel) {
|
||||
RenderChannelThumb(baseNote = baseNote, channel = it, accountViewModel, nav)
|
||||
LoadPublicChatChannel(baseNote.idHex, accountViewModel) {
|
||||
RenderPublicChatChannelThumb(baseNote = baseNote, channel = it, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderChannelThumb(
|
||||
fun RenderPublicChatChannelThumb(
|
||||
baseNote: Note,
|
||||
channel: Channel,
|
||||
channel: PublicChatChannel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
+6
-6
@@ -47,7 +47,7 @@ open class DiscoverLiveFeedFilter(
|
||||
followList() == MuteListEvent.Companion.blockListFor(account.userProfile().pubkeyHex)
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val allChannelNotes = LocalCache.liveChatChannels.mapNotNull { _, channel -> LocalCache.getNoteIfExists(channel.idHex) }
|
||||
val allChannelNotes = LocalCache.liveChatChannels.mapNotNull { _, channel -> LocalCache.getAddressableNoteIfExists(channel.address) }
|
||||
val allMessageNotes = LocalCache.liveChatChannels.map { _, channel -> channel.notes.filter { key, it -> it.event is LiveActivitiesEvent } }.flatten()
|
||||
|
||||
val notes = innerApplyFilter(allChannelNotes + allMessageNotes)
|
||||
@@ -94,7 +94,7 @@ open class DiscoverLiveFeedFilter(
|
||||
return collection
|
||||
.sortedWith(
|
||||
compareBy(
|
||||
{ convertStatusToOrder((it.event as? LiveActivitiesEvent)?.status()) },
|
||||
{ convertStatusToOrder((it.event as? LiveActivitiesEvent)?.statusEnum()) },
|
||||
{ participantCounts[it] },
|
||||
{ allParticipants[it] },
|
||||
{ (it.event as? LiveActivitiesEvent)?.starts() ?: it.createdAt() },
|
||||
@@ -103,11 +103,11 @@ open class DiscoverLiveFeedFilter(
|
||||
).reversed()
|
||||
}
|
||||
|
||||
fun convertStatusToOrder(status: String?): Int =
|
||||
fun convertStatusToOrder(status: StatusTag.STATUS?): Int =
|
||||
when (status) {
|
||||
StatusTag.STATUS.LIVE.code -> 2
|
||||
StatusTag.STATUS.PLANNED.code -> 1
|
||||
StatusTag.STATUS.ENDED.code -> 0
|
||||
StatusTag.STATUS.LIVE -> 2
|
||||
StatusTag.STATUS.PLANNED -> 1
|
||||
StatusTag.STATUS.ENDED -> 0
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
+16
-8
@@ -54,9 +54,10 @@ import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.DisplayAuthorBanner
|
||||
import com.vitorpamplona.amethyst.ui.note.Gallery
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadLiveActivityChannel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.EndedFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveActivitiesChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.OfflineFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.ScheduledFlag
|
||||
@@ -64,6 +65,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CheckIfVideoIsOnline
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.StatusTag
|
||||
@@ -75,13 +77,14 @@ import kotlinx.coroutines.launch
|
||||
|
||||
@Immutable
|
||||
data class LiveActivityCard(
|
||||
val id: Address?,
|
||||
val name: String,
|
||||
val cover: String?,
|
||||
val media: String?,
|
||||
val subject: String?,
|
||||
val content: String?,
|
||||
val participants: ImmutableList<ParticipantTag>,
|
||||
val status: String?,
|
||||
val status: StatusTag.STATUS?,
|
||||
val starts: Long?,
|
||||
)
|
||||
|
||||
@@ -95,13 +98,14 @@ fun RenderLiveActivityThumb(
|
||||
val noteEvent = it.event as? LiveActivitiesEvent
|
||||
|
||||
LiveActivityCard(
|
||||
id = noteEvent?.address(),
|
||||
name = noteEvent?.dTag() ?: "",
|
||||
cover = noteEvent?.image()?.ifBlank { null },
|
||||
media = noteEvent?.streaming(),
|
||||
subject = noteEvent?.title()?.ifBlank { null },
|
||||
content = noteEvent?.summary(),
|
||||
participants = noteEvent?.participants()?.toImmutableList() ?: persistentListOf(),
|
||||
status = noteEvent?.status(),
|
||||
status = noteEvent?.statusEnum(),
|
||||
starts = noteEvent?.starts(),
|
||||
)
|
||||
}
|
||||
@@ -146,7 +150,7 @@ fun RenderLiveActivityThumb(
|
||||
Box(Modifier.padding(10.dp)) {
|
||||
CrossfadeIfEnabled(targetState = card.status, label = "RenderLiveActivityThumb", accountViewModel = accountViewModel) {
|
||||
when (it) {
|
||||
StatusTag.STATUS.LIVE.code -> {
|
||||
StatusTag.STATUS.LIVE -> {
|
||||
val url = card.media
|
||||
if (url.isNullOrBlank()) {
|
||||
LiveFlag()
|
||||
@@ -160,10 +164,10 @@ fun RenderLiveActivityThumb(
|
||||
}
|
||||
}
|
||||
}
|
||||
StatusTag.STATUS.ENDED.code -> {
|
||||
StatusTag.STATUS.ENDED -> {
|
||||
EndedFlag()
|
||||
}
|
||||
StatusTag.STATUS.PLANNED.code -> {
|
||||
StatusTag.STATUS.PLANNED -> {
|
||||
ScheduledFlag(card.starts)
|
||||
}
|
||||
else -> {
|
||||
@@ -188,8 +192,10 @@ fun RenderLiveActivityThumb(
|
||||
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
|
||||
ChannelHeader(
|
||||
channelHex = baseNote.idHex,
|
||||
baseNote.address()?.let {
|
||||
LoadLiveActivityChannel(it, accountViewModel) {
|
||||
LiveActivitiesChannelHeader(
|
||||
baseChannel = it,
|
||||
showVideo = false,
|
||||
showFlag = false,
|
||||
sendToChannel = true,
|
||||
@@ -199,6 +205,8 @@ fun RenderLiveActivityThumb(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoadParticipants(
|
||||
|
||||
+1
-1
@@ -596,7 +596,7 @@ open class NewProductViewModel :
|
||||
|
||||
override fun updateZapFromText() {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), null, accountViewModel!!)
|
||||
val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel!!)
|
||||
tagger.run()
|
||||
tagger.pTags?.forEach { taggedUser ->
|
||||
if (!forwardZapTo.value.items.any { it.key == taggedUser }) {
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
class GeoHashFeedFilterSubAssembler(
|
||||
client: NostrClient,
|
||||
allKeys: () -> Set<GeohashQueryState>,
|
||||
) : PerUniqueIdEoseManager<GeohashQueryState>(client, allKeys) {
|
||||
) : PerUniqueIdEoseManager<GeohashQueryState, String>(client, allKeys) {
|
||||
override fun updateFilter(
|
||||
key: GeohashQueryState,
|
||||
since: SincePerRelayMap?,
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
class HashtagFeedFilterSubAssembler(
|
||||
client: NostrClient,
|
||||
allKeys: () -> Set<HashtagQueryState>,
|
||||
) : PerUniqueIdEoseManager<HashtagQueryState>(client, allKeys) {
|
||||
) : PerUniqueIdEoseManager<HashtagQueryState, String>(client, allKeys) {
|
||||
override fun updateFilter(
|
||||
key: HashtagQueryState,
|
||||
since: SincePerRelayMap?,
|
||||
|
||||
+2
-5
@@ -56,7 +56,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.AROUND_ME
|
||||
import com.vitorpamplona.amethyst.model.EphemeralChatChannel
|
||||
import com.vitorpamplona.amethyst.service.OnlineChecker
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
@@ -343,10 +342,8 @@ fun DisplayLiveBubbles(
|
||||
val feed by liveFeed.feed.collectAsStateWithLifecycle()
|
||||
|
||||
LazyRow(HorzPadding, horizontalArrangement = spacedBy(Size5dp)) {
|
||||
itemsIndexed(feed.list, key = { _, item -> item.idHex }) { _, item ->
|
||||
when (item) {
|
||||
is EphemeralChatChannel -> RenderEphemeralBubble(item, accountViewModel, nav)
|
||||
}
|
||||
itemsIndexed(feed.list, key = { _, item -> item.roomId.toKey() }) { _, item ->
|
||||
RenderEphemeralBubble(item, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -516,7 +516,6 @@ open class ShortNotePostViewModel :
|
||||
message.text,
|
||||
pTags,
|
||||
eTags,
|
||||
originalNote?.channelHex(),
|
||||
accountViewModel!!,
|
||||
)
|
||||
tagger.run()
|
||||
@@ -865,7 +864,7 @@ open class ShortNotePostViewModel :
|
||||
override fun updateZapFromText() {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
val tagger =
|
||||
NewMessageTagger(message.text, emptyList(), emptyList(), null, accountViewModel!!)
|
||||
NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel!!)
|
||||
tagger.run()
|
||||
tagger.pTags?.forEach { taggedUser ->
|
||||
if (!forwardZapTo.value.items.any { it.key == taggedUser }) {
|
||||
|
||||
+11
-13
@@ -21,7 +21,6 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.EphemeralChatChannel
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
@@ -38,7 +37,7 @@ import kotlin.collections.toSet
|
||||
|
||||
class HomeLiveFilter(
|
||||
val account: Account,
|
||||
) : AdditiveComplexFeedFilter<Channel, Note>() {
|
||||
) : AdditiveComplexFeedFilter<EphemeralChatChannel, Note>() {
|
||||
override fun feedKey(): String = account.userProfile().pubkeyHex
|
||||
|
||||
override fun showHiddenKey(): Boolean = false
|
||||
@@ -51,7 +50,7 @@ class HomeLiveFilter(
|
||||
|
||||
fun limitTime() = TimeUtils.fifteenMinutesAgo()
|
||||
|
||||
override fun feed(): List<Channel> {
|
||||
override fun feed(): List<EphemeralChatChannel> {
|
||||
val filterParams = buildFilterParams(account)
|
||||
val fiveMinsAgo = limitTime()
|
||||
|
||||
@@ -74,9 +73,9 @@ class HomeLiveFilter(
|
||||
}.isNotEmpty()
|
||||
|
||||
override fun updateListWith(
|
||||
oldList: List<Channel>,
|
||||
oldList: List<EphemeralChatChannel>,
|
||||
newItems: Set<Note>,
|
||||
): List<Channel> {
|
||||
): List<EphemeralChatChannel> {
|
||||
val fiveMinsAgo = limitTime()
|
||||
|
||||
val revisedOldList =
|
||||
@@ -91,7 +90,7 @@ class HomeLiveFilter(
|
||||
.mapNotNull {
|
||||
val room = (it.event as? EphemeralChatEvent)?.roomId()
|
||||
if (room != null) {
|
||||
LocalCache.getChannelIfExists(room)
|
||||
LocalCache.getEphemeralChatChannelIfExists(room)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -124,7 +123,7 @@ class HomeLiveFilter(
|
||||
filterParams.match(noteEvent, note.relays)
|
||||
}
|
||||
|
||||
fun sort(collection: Set<Channel>): List<Channel> {
|
||||
fun sort(collection: Set<EphemeralChatChannel>): List<EphemeralChatChannel> {
|
||||
val topFilter = account.liveDiscoveryFollowLists.value
|
||||
val topFilterAuthors =
|
||||
when (topFilter) {
|
||||
@@ -141,18 +140,17 @@ class HomeLiveFilter(
|
||||
collection.associate { it to followsThatParticipateOn(it, followingKeySet) }
|
||||
|
||||
return collection.sortedWith(
|
||||
compareByDescending<Channel> { followCounts[it] }
|
||||
.thenByDescending<Channel> { it.lastNoteCreatedAt }
|
||||
.thenBy { it.idHex },
|
||||
compareByDescending<EphemeralChatChannel> { followCounts[it] }
|
||||
.thenByDescending<EphemeralChatChannel> { it.lastNoteCreatedAt }
|
||||
.thenBy { it.roomId.id }
|
||||
.thenBy { it.roomId.relayUrl },
|
||||
)
|
||||
}
|
||||
|
||||
fun followsThatParticipateOn(
|
||||
channel: Channel,
|
||||
channel: EphemeralChatChannel,
|
||||
followingSet: Set<HexKey>?,
|
||||
): Int {
|
||||
if (channel == null) return 0
|
||||
|
||||
var count = 0
|
||||
|
||||
channel.notes.forEach { key, value ->
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ import kotlinx.coroutines.launch
|
||||
class HomeOutboxEventsEoseManager(
|
||||
client: NostrClient,
|
||||
allKeys: () -> Set<HomeQueryState>,
|
||||
) : PerUserAndFollowListEoseManager<HomeQueryState>(client, allKeys) {
|
||||
) : PerUserAndFollowListEoseManager<HomeQueryState, String>(client, allKeys) {
|
||||
override fun updateFilter(
|
||||
key: HomeQueryState,
|
||||
since: SincePerRelayMap?,
|
||||
|
||||
+1
-1
@@ -72,7 +72,7 @@ class CardFeedContentState(
|
||||
val scrollToTop = _scrollToTop.asStateFlow()
|
||||
var scrolltoTopPending = false
|
||||
|
||||
private var lastFeedKey: String? = null
|
||||
private var lastFeedKey: Any? = null
|
||||
|
||||
override val isRefreshing: MutableState<Boolean> = mutableStateOf(false)
|
||||
|
||||
|
||||
+12
-90
@@ -29,33 +29,21 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
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.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
|
||||
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Nav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Composable
|
||||
@@ -66,25 +54,16 @@ fun LoadRedirectScreen(
|
||||
) {
|
||||
if (eventId == null) return
|
||||
|
||||
var noteBase by remember { mutableStateOf<Note?>(null) }
|
||||
|
||||
LaunchedEffect(eventId) {
|
||||
launch(Dispatchers.IO) {
|
||||
val newNoteBase = LocalCache.checkGetOrCreateNote(eventId)
|
||||
if (newNoteBase != noteBase) {
|
||||
noteBase = newNoteBase
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
noteBase?.let {
|
||||
LoadNote(eventId, accountViewModel) { note ->
|
||||
note?.let {
|
||||
LoadRedirectScreen(
|
||||
baseNote = it,
|
||||
baseNote = note,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoadRedirectScreen(
|
||||
@@ -95,11 +74,13 @@ fun LoadRedirectScreen(
|
||||
val noteState by observeNote(baseNote, accountViewModel)
|
||||
|
||||
LaunchedEffect(key1 = noteState) {
|
||||
val note = noteState?.note ?: return@LaunchedEffect
|
||||
val event = note.event
|
||||
|
||||
val event = noteState.note.event
|
||||
if (event != null) {
|
||||
withContext(Dispatchers.IO) { redirect(event, accountViewModel, nav) }
|
||||
withContext(Dispatchers.IO) {
|
||||
routeFor(event, accountViewModel.userProfile())?.let { route ->
|
||||
nav.popUpTo(route, Route.EventRedirect::class)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,62 +92,3 @@ fun LoadRedirectScreen(
|
||||
Text(stringRes(R.string.looking_for_event, baseNote.idHex))
|
||||
}
|
||||
}
|
||||
|
||||
fun redirect(
|
||||
eventId: HexKey,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
LocalCache.getNoteIfExists(eventId)?.event?.let {
|
||||
redirect(it, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
|
||||
fun redirect(
|
||||
event: Event,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val channelHex =
|
||||
if (
|
||||
event is ChannelMessageEvent ||
|
||||
event is ChannelMetadataEvent ||
|
||||
event is ChannelCreateEvent ||
|
||||
event is LiveActivitiesChatMessageEvent ||
|
||||
event is LiveActivitiesEvent
|
||||
) {
|
||||
(event as? ChannelMessageEvent)?.channelId()
|
||||
?: (event as? ChannelMetadataEvent)?.channelId()
|
||||
?: (event as? ChannelCreateEvent)?.id
|
||||
?: (event as? LiveActivitiesChatMessageEvent)?.activity()?.toTag()
|
||||
?: (event as? LiveActivitiesEvent)?.aTag()?.toTag()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
if (event is GiftWrapEvent) {
|
||||
event.innerEventId?.let {
|
||||
redirect(it, accountViewModel, nav)
|
||||
} ?: run {
|
||||
accountViewModel.unwrap(event) { redirect(it, accountViewModel, nav) }
|
||||
}
|
||||
} else if (event is SealedRumorEvent) {
|
||||
event.innerEventId?.let {
|
||||
redirect(it, accountViewModel, nav)
|
||||
} ?: run {
|
||||
accountViewModel.unseal(event) { redirect(it, accountViewModel, nav) }
|
||||
}
|
||||
} else {
|
||||
if (event is ChannelCreateEvent) {
|
||||
nav.popUpTo(Route.Channel(event.id), Route.EventRedirect::class)
|
||||
} else if (event is ChatroomKeyable) {
|
||||
val withKey = event.chatroomKey(accountViewModel.userProfile().pubkeyHex)
|
||||
accountViewModel.userProfile().createChatroom(withKey)
|
||||
nav.popUpTo(Route.Room(withKey.hashCode()), Route.EventRedirect::class)
|
||||
} else if (channelHex != null) {
|
||||
nav.popUpTo(Route.Channel(channelHex), Route.EventRedirect::class)
|
||||
} else {
|
||||
nav.popUpTo(Route.Note(event.id), Route.EventRedirect::class)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+36
-15
@@ -29,7 +29,6 @@ import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.logTime
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.relayClient.searchCommand.SearchQueryState
|
||||
@@ -71,36 +70,58 @@ class SearchBarViewModel(
|
||||
val searchDataSourceState = SearchQueryState(MutableStateFlow(searchValue), account)
|
||||
|
||||
val searchResultsUsers =
|
||||
combine(searchValueFlow.debounce(100), invalidations.debounce(100)) { term, version ->
|
||||
logTime("SearchBarViewModel findUsersStartingWith") {
|
||||
combine(
|
||||
searchValueFlow.debounce(100),
|
||||
invalidations.debounce(100),
|
||||
) { term, version ->
|
||||
LocalCache.findUsersStartingWith(term, account)
|
||||
}
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(viewModelScope, WhileSubscribed(5000), emptyList())
|
||||
|
||||
val searchResultsNotes =
|
||||
combine(searchValueFlow.debounce(100), invalidations) { term, version ->
|
||||
logTime("SearchBarViewModel findNotesStartingWith") {
|
||||
combine(
|
||||
searchValueFlow.debounce(100),
|
||||
invalidations,
|
||||
) { term, version ->
|
||||
LocalCache
|
||||
.findNotesStartingWith(term, account.hiddenUsers)
|
||||
.sortedWith(DefaultFeedOrder)
|
||||
}
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(viewModelScope, WhileSubscribed(5000), emptyList())
|
||||
|
||||
val searchResultsChannels =
|
||||
combine(searchValueFlow.debounce(100), invalidations) { term, version ->
|
||||
logTime("SearchBarViewModel findChannelsStartingWith") {
|
||||
LocalCache.findChannelsStartingWith(term)
|
||||
}
|
||||
val searchResultsPublicChatChannels =
|
||||
combine(
|
||||
searchValueFlow.debounce(100),
|
||||
invalidations,
|
||||
) { term, version ->
|
||||
LocalCache.findPublicChatChannelsStartingWith(term)
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(viewModelScope, WhileSubscribed(5000), emptyList())
|
||||
|
||||
val searchResultsEphemeralChannels =
|
||||
combine(
|
||||
searchValueFlow.debounce(100),
|
||||
invalidations,
|
||||
) { term, version ->
|
||||
LocalCache.findEphemeralChatChannelsStartingWith(term)
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(viewModelScope, WhileSubscribed(5000), emptyList())
|
||||
|
||||
val searchResultsLiveActivityChannels =
|
||||
combine(
|
||||
searchValueFlow.debounce(100),
|
||||
invalidations,
|
||||
) { term, version ->
|
||||
LocalCache.findLiveActivityChannelsStartingWith(term)
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(viewModelScope, WhileSubscribed(5000), emptyList())
|
||||
|
||||
val hashtagResults =
|
||||
combine(searchValueFlow.debounce(100), invalidations) { term, version ->
|
||||
logTime("SearchBarViewModel findHashtags") {
|
||||
combine(
|
||||
searchValueFlow.debounce(100),
|
||||
invalidations,
|
||||
) { term, version ->
|
||||
findHashtags(term)
|
||||
}
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(viewModelScope, WhileSubscribed(5000), emptyList())
|
||||
|
||||
|
||||
+61
-4
@@ -63,6 +63,7 @@ import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
|
||||
import com.vitorpamplona.amethyst.ui.navigation.AppBottomBar
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.ClearTextIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.SearchIcon
|
||||
@@ -230,7 +231,9 @@ private fun DisplaySearchResults(
|
||||
|
||||
val hashTags by searchBarViewModel.hashtagResults.collectAsStateWithLifecycle()
|
||||
val users by searchBarViewModel.searchResultsUsers.collectAsStateWithLifecycle()
|
||||
val channels by searchBarViewModel.searchResultsChannels.collectAsStateWithLifecycle()
|
||||
val publicChatChannels by searchBarViewModel.searchResultsPublicChatChannels.collectAsStateWithLifecycle()
|
||||
val ephemeralChannels by searchBarViewModel.searchResultsEphemeralChannels.collectAsStateWithLifecycle()
|
||||
val liveActivityChannels by searchBarViewModel.searchResultsLiveActivityChannels.collectAsStateWithLifecycle()
|
||||
val notes by searchBarViewModel.searchResultsNotes.collectAsStateWithLifecycle()
|
||||
|
||||
LazyColumn(
|
||||
@@ -262,8 +265,8 @@ private fun DisplaySearchResults(
|
||||
}
|
||||
|
||||
itemsIndexed(
|
||||
channels,
|
||||
key = { _, item -> "c" + item.idHex },
|
||||
publicChatChannels,
|
||||
key = { _, item -> "public" + item.idHex },
|
||||
) { _, item ->
|
||||
ChannelName(
|
||||
channelIdHex = item.idHex,
|
||||
@@ -279,7 +282,61 @@ private fun DisplaySearchResults(
|
||||
hasNewMessages = false,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
||||
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
|
||||
onClick = { nav.nav(Route.Channel(item.idHex)) },
|
||||
onClick = { nav.nav(routeFor(item)) },
|
||||
)
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = StdTopPadding,
|
||||
thickness = DividerThickness,
|
||||
)
|
||||
}
|
||||
|
||||
itemsIndexed(
|
||||
ephemeralChannels,
|
||||
key = { _, item -> "ephem" + item.roomId.toKey() },
|
||||
) { _, item ->
|
||||
ChannelName(
|
||||
channelIdHex = item.roomId.toKey(),
|
||||
channelPicture = item.profilePicture(),
|
||||
channelTitle = {
|
||||
Text(
|
||||
item.toBestDisplayName(),
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
},
|
||||
channelLastTime = null,
|
||||
channelLastContent = item.summary(),
|
||||
hasNewMessages = false,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
||||
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
|
||||
onClick = { nav.nav(routeFor(item)) },
|
||||
)
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = StdTopPadding,
|
||||
thickness = DividerThickness,
|
||||
)
|
||||
}
|
||||
|
||||
itemsIndexed(
|
||||
liveActivityChannels,
|
||||
key = { _, item -> "live" + item.address.toValue() },
|
||||
) { _, item ->
|
||||
ChannelName(
|
||||
channelIdHex = item.address.toValue(),
|
||||
channelPicture = item.profilePicture(),
|
||||
channelTitle = {
|
||||
Text(
|
||||
item.toBestDisplayName(),
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
},
|
||||
channelLastTime = null,
|
||||
channelLastContent = item.summary(),
|
||||
hasNewMessages = false,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
||||
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
|
||||
onClick = { nav.nav(routeFor(item)) },
|
||||
)
|
||||
|
||||
HorizontalDivider(
|
||||
|
||||
+2
-3
@@ -153,7 +153,7 @@ import com.vitorpamplona.amethyst.ui.note.types.VideoDisplay
|
||||
import com.vitorpamplona.amethyst.ui.painterRes
|
||||
import com.vitorpamplona.amethyst.ui.screen.RenderFeedState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.PublicChatChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ThinSendButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.dal.LevelFeedViewModel
|
||||
@@ -525,9 +525,8 @@ private fun FullBleedNoteCompose(
|
||||
(noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) &&
|
||||
baseNote.channelHex() != null
|
||||
) {
|
||||
ChannelHeader(
|
||||
PublicChatChannelHeader(
|
||||
channelHex = baseNote.channelHex()!!,
|
||||
showVideo = true,
|
||||
sendToChannel = true,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
|
||||
+2
-1
@@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.model.ThreadAssembler
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.ThreadQueryState
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
|
||||
@@ -39,7 +40,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
class ThreadEventLoaderSubAssembler(
|
||||
client: NostrClient,
|
||||
allKeys: () -> Set<ThreadQueryState>,
|
||||
) : PerUniqueIdEoseManager<ThreadQueryState>(client, allKeys, invalidateAfterEose = true) {
|
||||
) : PerUniqueIdEoseManager<ThreadQueryState, HexKey>(client, allKeys, invalidateAfterEose = true) {
|
||||
override fun updateFilter(
|
||||
key: ThreadQueryState,
|
||||
since: SincePerRelayMap?,
|
||||
|
||||
+2
-1
@@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.model.ThreadAssembler
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.ThreadQueryState
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
|
||||
@@ -36,7 +37,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
class ThreadFilterSubAssembler(
|
||||
client: NostrClient,
|
||||
allKeys: () -> Set<ThreadQueryState>,
|
||||
) : PerUniqueIdEoseManager<ThreadQueryState>(client, allKeys) {
|
||||
) : PerUniqueIdEoseManager<ThreadQueryState, HexKey>(client, allKeys) {
|
||||
override fun updateFilter(
|
||||
key: ThreadQueryState,
|
||||
since: SincePerRelayMap?,
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ import kotlinx.coroutines.launch
|
||||
class VideoOutboxEventsFilterSubAssembler(
|
||||
client: NostrClient,
|
||||
allKeys: () -> Set<VideoQueryState>,
|
||||
) : PerUserAndFollowListEoseManager<VideoQueryState>(client, allKeys) {
|
||||
) : PerUserAndFollowListEoseManager<VideoQueryState, String>(client, allKeys) {
|
||||
override fun updateFilter(
|
||||
key: VideoQueryState,
|
||||
since: SincePerRelayMap?,
|
||||
|
||||
@@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
@@ -331,3 +332,13 @@ val LargeRelayIconModifier =
|
||||
Modifier
|
||||
.size(Size55dp)
|
||||
.clip(shape = CircleShape)
|
||||
|
||||
val FollowSetImageModifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(ratio = 21f / 9f)
|
||||
.clip(QuoteBorder)
|
||||
|
||||
val SimpleImageBorder = Modifier.fillMaxSize().clip(QuoteBorder)
|
||||
|
||||
val SimpleHeaderImage = Modifier.fillMaxWidth().heightIn(max = 200.dp)
|
||||
|
||||
+11
@@ -90,6 +90,10 @@ class LiveActivitiesEvent(
|
||||
|
||||
fun status() = checkStatus(tags.firstNotNullOfOrNull(StatusTag::parse))
|
||||
|
||||
fun statusEnum() = checkStatusEnum(tags.firstNotNullOfOrNull(StatusTag::parseEnum))
|
||||
|
||||
fun isLive() = statusEnum() == StatusTag.STATUS.LIVE
|
||||
|
||||
fun currentParticipants() = tags.firstNotNullOfOrNull(CurrentParticipantsTag::parse)
|
||||
|
||||
fun totalParticipants() = tags.firstNotNullOfOrNull(TotalParticipantsTag::parse)
|
||||
@@ -113,6 +117,13 @@ class LiveActivitiesEvent(
|
||||
eventStatus
|
||||
}
|
||||
|
||||
fun checkStatusEnum(eventStatus: StatusTag.STATUS?): StatusTag.STATUS? =
|
||||
if (eventStatus == StatusTag.STATUS.LIVE && createdAt < TimeUtils.eightHoursAgo()) {
|
||||
StatusTag.STATUS.ENDED
|
||||
} else {
|
||||
eventStatus
|
||||
}
|
||||
|
||||
fun participantsIntersect(keySet: Set<String>): Boolean = keySet.contains(pubKey) || tags.any(ParticipantTag::isIn, keySet)
|
||||
|
||||
companion object {
|
||||
|
||||
+18
@@ -33,6 +33,16 @@ class StatusTag {
|
||||
;
|
||||
|
||||
fun toTagArray() = assemble(this)
|
||||
|
||||
companion object {
|
||||
fun parse(code: String): STATUS? =
|
||||
when (code) {
|
||||
LIVE.code -> LIVE
|
||||
PLANNED.code -> PLANNED
|
||||
ENDED.code -> ENDED
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -46,6 +56,14 @@ class StatusTag {
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun parseEnum(tag: Array<String>): STATUS? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return STATUS.parse(tag[1])
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun assemble(status: STATUS) = arrayOf(TAG_NAME, status.code)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user