Fixing Ephemeral Chat interface

This commit is contained in:
Vitor Pamplona
2025-07-10 19:42:18 -04:00
parent e3acf4c250
commit a94c8673d5
6 changed files with 46 additions and 161 deletions

View File

@@ -70,7 +70,6 @@ import com.vitorpamplona.amethyst.service.uploads.FileHeader
import com.vitorpamplona.amethyst.ui.tor.TorType import com.vitorpamplona.amethyst.ui.tor.TorType
import com.vitorpamplona.quartz.experimental.bounties.BountyAddValueEvent import com.vitorpamplona.quartz.experimental.bounties.BountyAddValueEvent
import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent
import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryReadingStateEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryReadingStateEvent
@@ -145,9 +144,6 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay
import com.vitorpamplona.quartz.nip19Bech32.entities.NSec import com.vitorpamplona.quartz.nip19Bech32.entities.NSec
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.nip30CustomEmoji.EmojiUrlTag import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
import com.vitorpamplona.quartz.nip30CustomEmoji.emojis import com.vitorpamplona.quartz.nip30CustomEmoji.emojis
import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent
@@ -163,8 +159,6 @@ import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent
import com.vitorpamplona.quartz.nip51Lists.FollowListEvent import com.vitorpamplona.quartz.nip51Lists.FollowListEvent
import com.vitorpamplona.quartz.nip51Lists.GeneralListEvent import com.vitorpamplona.quartz.nip51Lists.GeneralListEvent
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip56Reports.ReportType import com.vitorpamplona.quartz.nip56Reports.ReportType
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent
@@ -815,33 +809,7 @@ class Account(
?: cache.relayHints.hintsForKey(pubkey).toSet() ?: cache.relayHints.hintsForKey(pubkey).toSet()
} }
private fun computeRelaysForChannels(event: Event): Set<NormalizedRelayUrl> { private fun computeRelaysForChannels(event: Event): Set<NormalizedRelayUrl> = LocalCache.getAnyChannel(event)?.relays() ?: emptySet()
val isInChannel =
if (
event is ChannelMessageEvent ||
event is ChannelMetadataEvent ||
event is ChannelCreateEvent ||
event is LiveActivitiesChatMessageEvent ||
event is LiveActivitiesEvent ||
event is EphemeralChatEvent
) {
(event as? ChannelMessageEvent)?.channelId()
?: (event as? ChannelMetadataEvent)?.channelId()
?: (event as? ChannelCreateEvent)?.id
?: (event as? LiveActivitiesChatMessageEvent)?.activity()?.toTag()
?: (event as? LiveActivitiesEvent)?.aTag()?.toTag()
?: (event as? EphemeralChatEvent)?.roomId()?.toKey()
} else {
null
}
return if (isInChannel != null) {
val channel = LocalCache.checkGetOrCreateChannel(isInChannel)
channel?.relays() ?: emptySet()
} else {
emptySet()
}
}
fun computeRelayListToBroadcast(event: Event): Set<NormalizedRelayUrl> { fun computeRelayListToBroadcast(event: Event): Set<NormalizedRelayUrl> {
if (event is MetadataEvent || event is AdvertisedRelayListEvent) { if (event is MetadataEvent || event is AdvertisedRelayListEvent) {

View File

@@ -61,7 +61,6 @@ import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.isLocalHost import com.vitorpamplona.quartz.nip01Core.relay.normalizer.isLocalHost
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
@@ -416,33 +415,16 @@ object LocalCache : ILocalCache {
EphemeralChatChannel(key) EphemeralChatChannel(key)
} }
fun checkGetOrCreateChannel(key: String): Channel? { fun checkGetOrCreatePublicChatChannel(key: String): PublicChatChannel? {
checkNotInMainThread()
if (key.contains("@")) {
val idParts = key.split("@")
val relay = RelayUrlNormalizer.normalizeOrNull(idParts[1])
if (relay == null) {
return null
} else {
getOrCreateEphemeralChannel(RoomId(idParts[0], relay))
}
}
if (isValidHex(key)) { if (isValidHex(key)) {
return getOrCreatePublicChatChannel(key) return getOrCreatePublicChatChannel(key)
} }
val address = Address.parse(key)
if (address != null) {
return getOrCreateLiveChannel(address)
}
return null return null
} }
private fun isValidHex(key: String): Boolean { private fun isValidHex(key: String): Boolean {
if (key.isBlank()) return false if (key.isBlank()) return false
if (key.length != 64) return false
if (key.contains(":")) return false if (key.contains(":")) return false
return Hex.isHex(key) return Hex.isHex(key)
@@ -1622,12 +1604,12 @@ object LocalCache : ILocalCache {
if (channelId.isNullOrBlank()) return false if (channelId.isNullOrBlank()) return false
// new event // new event
val oldChannel = checkGetOrCreateChannel(channelId) ?: return false val oldChannel = checkGetOrCreatePublicChatChannel(channelId) ?: return false
val author = getOrCreateUser(event.pubKey) val author = getOrCreateUser(event.pubKey)
val isVerified = val isVerified =
if (event.createdAt > oldChannel.updatedMetadataAt) { if (event.createdAt > oldChannel.updatedMetadataAt) {
if (oldChannel is PublicChatChannel && (wasVerified || justVerify(event))) { if (wasVerified || justVerify(event)) {
oldChannel.updateChannelInfo(author, event) oldChannel.updateChannelInfo(author, event)
true true
} else { } else {
@@ -1653,44 +1635,18 @@ object LocalCache : ILocalCache {
relay: NormalizedRelayUrl?, relay: NormalizedRelayUrl?,
wasVerified: Boolean, wasVerified: Boolean,
): Boolean { ): Boolean {
val channelId = event.channelId() val channelId = event.channelId() ?: return false
if (channelId.isNullOrBlank()) return false val channel = checkGetOrCreatePublicChatChannel(channelId)
if (channel == null) {
val channel = checkGetOrCreateChannel(channelId) ?: return false Log.w("LocalCache", "Unable to create public chat channel for event ${event.toJson()}")
return false
}
val note = getOrCreateNote(event.id) val note = getOrCreateNote(event.id)
channel.addNote(note, relay) channel.addNote(note, relay)
val author = getOrCreateUser(event.pubKey) return consumeRegularEvent(event, relay, wasVerified)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return false
if (antiSpam.isSpam(event, relay)) {
return false
}
if (wasVerified || justVerify(event)) {
val replyTo = computeReplyTo(event)
note.loadEvent(event, author, replyTo)
// Log.d("CM", "New Chat Note (${note.author?.toBestDisplayName()} ${note.event?.content}
// ${formattedDateTime(event.createdAt)}")
// Counts the replies
replyTo.forEach { it.addReply(note) }
refreshObservers(note)
}
return true
} }
fun consume( fun consume(
@@ -1698,46 +1654,15 @@ object LocalCache : ILocalCache {
relay: NormalizedRelayUrl?, relay: NormalizedRelayUrl?,
wasVerified: Boolean, wasVerified: Boolean,
): Boolean { ): Boolean {
val roomId = event.roomId() val roomId = event.roomId() ?: return false
if (roomId == null) return false
val channelId = roomId
val channel = getOrCreateEphemeralChannel(channelId) ?: return false
val note = getOrCreateNote(event.id) val note = getOrCreateNote(event.id)
val channel = getOrCreateEphemeralChannel(roomId)
channel.addNote(note, relay) channel.addNote(note, relay)
val author = getOrCreateUser(event.pubKey) return consumeRegularEvent(event, relay, wasVerified)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return false
if (antiSpam.isSpam(event, relay)) {
return false
}
if (wasVerified || justVerify(event)) {
note.loadEvent(event, author, emptyList())
refreshObservers(note)
return true
}
return false
} }
fun consume(
event: CommentEvent,
relay: NormalizedRelayUrl?,
wasVerified: Boolean,
) = consumeRegularEvent(event, relay, wasVerified)
fun consume( fun consume(
event: LiveActivitiesChatMessageEvent, event: LiveActivitiesChatMessageEvent,
relay: NormalizedRelayUrl?, relay: NormalizedRelayUrl?,
@@ -1750,36 +1675,15 @@ object LocalCache : ILocalCache {
val note = getOrCreateNote(event.id) val note = getOrCreateNote(event.id)
channel.addNote(note, relay) channel.addNote(note, relay)
val author = getOrCreateUser(event.pubKey) return consumeRegularEvent(event, relay, wasVerified)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return false
if (antiSpam.isSpam(event, relay)) {
return false
}
if (wasVerified || justVerify(event)) {
val replyTo = computeReplyTo(event)
note.loadEvent(event, author, replyTo)
// Counts the replies
replyTo.forEach { it.addReply(note) }
refreshObservers(note)
return true
}
return false
} }
fun consume(
event: CommentEvent,
relay: NormalizedRelayUrl?,
wasVerified: Boolean,
) = consumeRegularEvent(event, relay, wasVerified)
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
fun consume( fun consume(
event: ChannelHideMessageEvent, event: ChannelHideMessageEvent,
@@ -2859,18 +2763,18 @@ object LocalCache : ILocalCache {
} }
} }
is EphemeralChatEvent -> { is EphemeralChatEvent -> {
draft.roomId()?.toKey()?.let { draft.roomId()?.let {
checkGetOrCreateChannel(it)?.addNote(note, null) getOrCreateEphemeralChannel(it).addNote(note, null)
} }
} }
is ChannelMessageEvent -> { is ChannelMessageEvent -> {
draft.channelId()?.let { channelId -> draft.channelId()?.let { channelId ->
checkGetOrCreateChannel(channelId)?.addNote(note, null) checkGetOrCreatePublicChatChannel(channelId)?.addNote(note, null)
} }
} }
is LiveActivitiesChatMessageEvent -> { is LiveActivitiesChatMessageEvent -> {
draft.activityAddress()?.let { channelId -> draft.activityAddress()?.let { channelId ->
checkGetOrCreateChannel(channelId.toValue())?.addNote(note, null) getOrCreateLiveChannel(channelId).addNote(note, null)
} }
} }
is TextNoteEvent -> { is TextNoteEvent -> {
@@ -2937,12 +2841,17 @@ object LocalCache : ILocalCache {
} }
is ChannelMessageEvent -> { is ChannelMessageEvent -> {
draft.channelId()?.let { channelId -> draft.channelId()?.let { channelId ->
checkGetOrCreateChannel(channelId)?.removeNote(draftWrap) getPublicChatChannelIfExists(channelId)?.removeNote(draftWrap)
} }
} }
is EphemeralChatEvent -> { is EphemeralChatEvent -> {
draft.roomId()?.let { draft.roomId()?.let {
getOrCreateEphemeralChannel(it).removeNote(draftWrap) getEphemeralChatChannelIfExists(it)?.removeNote(draftWrap)
}
}
is LiveActivitiesChatMessageEvent -> {
draft.activityAddress()?.let { channelId ->
getLiveActivityChannelIfExists(channelId)?.removeNote(draftWrap)
} }
} }
is TextNoteEvent -> { is TextNoteEvent -> {

View File

@@ -124,7 +124,7 @@ class HomeLiveFilter(
} }
fun sort(collection: Set<EphemeralChatChannel>): List<EphemeralChatChannel> { fun sort(collection: Set<EphemeralChatChannel>): List<EphemeralChatChannel> {
val topFilter = account.liveDiscoveryFollowLists.value val topFilter = account.liveHomeFollowLists.value
val topFilterAuthors = val topFilterAuthors =
when (topFilter) { when (topFilter) {
is AuthorsByOutboxTopNavFilter -> topFilter.authors is AuthorsByOutboxTopNavFilter -> topFilter.authors
@@ -137,7 +137,7 @@ class HomeLiveFilter(
val followingKeySet = topFilterAuthors ?: account.kind3FollowList.flow.value.authors val followingKeySet = topFilterAuthors ?: account.kind3FollowList.flow.value.authors
val followCounts = val followCounts =
collection.associate { it to followsThatParticipateOn(it, followingKeySet) } collection.associateWith { followsThatParticipateOn(it, followingKeySet) }
return collection.sortedWith( return collection.sortedWith(
compareByDescending<EphemeralChatChannel> { followCounts[it] } compareByDescending<EphemeralChatChannel> { followCounts[it] }

View File

@@ -26,8 +26,17 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
data class RoomId( data class RoomId(
val id: String, val id: String,
val relayUrl: NormalizedRelayUrl, val relayUrl: NormalizedRelayUrl,
) { ) : Comparable<RoomId> {
fun toKey() = "$id@$relayUrl" fun toKey() = "$id@$relayUrl"
fun toDisplayKey() = id + "@" + relayUrl.displayUrl() fun toDisplayKey() = id + "@" + relayUrl.displayUrl()
override fun compareTo(other: RoomId): Int {
val result = id.compareTo(other.id)
return if (result == 0) {
relayUrl.url.compareTo(other.relayUrl.url)
} else {
result
}
}
} }

View File

@@ -39,7 +39,7 @@ class RelayTag {
ensure(tag[0] == TAG_NAME) { return null } ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null } ensure(tag[1].isNotEmpty()) { return null }
return RelayUrlNormalizer.normalizeOrNull(tag[1]) ?: return null return RelayUrlNormalizer.normalizeOrNull(tag[1])
} }
@JvmStatic @JvmStatic

View File

@@ -149,7 +149,6 @@ open class BasicRelayClient(
override fun onMessage(text: String) { override fun onMessage(text: String) {
// Log.d(logTag, "Receiving: $text") // Log.d(logTag, "Receiving: $text")
stats.addBytesReceived(text.bytesUsedInMemory()) stats.addBytesReceived(text.bytesUsedInMemory())
try { try {