diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 1a0308cf3..5f8b95f1c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -179,9 +179,14 @@ object LocalCache { } fun consume(event: PeopleListEvent) { + val version = getOrCreateNote(event.id) val note = getOrCreateAddressableNote(event.address()) val author = getOrCreateUser(event.pubKey) + if (version.event == null) { + version.loadEvent(event, author, emptyList()) + } + // Already processed this event. if (note.event?.id() == event.id()) return @@ -234,9 +239,14 @@ object LocalCache { } fun consume(event: LongTextNoteEvent, relay: Relay?) { + val version = getOrCreateNote(event.id) val note = getOrCreateAddressableNote(event.address()) val author = getOrCreateUser(event.pubKey) + if (version.event == null) { + version.loadEvent(event, author, emptyList()) + } + if (relay != null) { author.addRelayBeingUsed(relay, event.createdAt) note.addRelay(relay) @@ -299,10 +309,33 @@ object LocalCache { refreshObservers(note) } - private fun consume(event: PinListEvent) { + private fun consume(event: LiveActivitiesEvent, relay: Relay?) { + val version = getOrCreateNote(event.id) val note = getOrCreateAddressableNote(event.address()) val author = getOrCreateUser(event.pubKey) + if (version.event == null) { + version.loadEvent(event, author, emptyList()) + } + + if (note.event?.id() == event.id()) return + + if (event.createdAt > (note.createdAt() ?: 0)) { + note.loadEvent(event, author, emptyList()) + + refreshObservers(note) + } + } + + private fun consume(event: PinListEvent) { + val version = getOrCreateNote(event.id) + val note = getOrCreateAddressableNote(event.address()) + val author = getOrCreateUser(event.pubKey) + + if (version.event == null) { + version.loadEvent(event, author, emptyList()) + } + if (note.event?.id() == event.id()) return if (event.createdAt > (note.createdAt() ?: 0)) { @@ -313,9 +346,14 @@ object LocalCache { } private fun consume(event: RelaySetEvent) { + val version = getOrCreateNote(event.id) val note = getOrCreateAddressableNote(event.address()) val author = getOrCreateUser(event.pubKey) + if (version.event == null) { + version.loadEvent(event, author, emptyList()) + } + if (note.event?.id() == event.id()) return if (event.createdAt > (note.createdAt() ?: 0)) { @@ -326,9 +364,14 @@ object LocalCache { } private fun consume(event: AudioTrackEvent) { + val version = getOrCreateNote(event.id) val note = getOrCreateAddressableNote(event.address()) val author = getOrCreateUser(event.pubKey) + if (version.event == null) { + version.loadEvent(event, author, emptyList()) + } + // Already processed this event. if (note.event?.id() == event.id()) return @@ -340,9 +383,14 @@ object LocalCache { } fun consume(event: BadgeDefinitionEvent) { + val version = getOrCreateNote(event.id) val note = getOrCreateAddressableNote(event.address()) val author = getOrCreateUser(event.pubKey) + if (version.event == null) { + version.loadEvent(event, author, emptyList()) + } + // Already processed this event. if (note.event?.id() == event.id()) return @@ -354,9 +402,14 @@ object LocalCache { } fun consume(event: BadgeProfilesEvent) { + val version = getOrCreateNote(event.id) val note = getOrCreateAddressableNote(event.address()) val author = getOrCreateUser(event.pubKey) + if (version.event == null) { + version.loadEvent(event, author, emptyList()) + } + // Already processed this event. if (note.event?.id() == event.id()) return @@ -392,21 +445,33 @@ object LocalCache { } fun consume(event: AppDefinitionEvent) { + val version = getOrCreateNote(event.id) val note = getOrCreateAddressableNote(event.address()) val author = getOrCreateUser(event.pubKey) + if (version.event == null) { + version.loadEvent(event, author, emptyList()) + } + // Already processed this event. if (note.event != null) return - note.loadEvent(event, author, emptyList()) + if (event.createdAt > (note.createdAt() ?: 0)) { + note.loadEvent(event, author, emptyList()) - refreshObservers(note) + refreshObservers(note) + } } fun consume(event: AppRecommendationEvent) { + val version = getOrCreateNote(event.id) val note = getOrCreateAddressableNote(event.address()) val author = getOrCreateUser(event.pubKey) + if (version.event == null) { + version.loadEvent(event, author, emptyList()) + } + // Already processed this event. if (note.event?.id() == event.id()) return @@ -1064,6 +1129,7 @@ object LocalCache { is FileStorageEvent -> consume(event, relay) is FileStorageHeaderEvent -> consume(event, relay) is HighlightEvent -> consume(event, relay) + is LiveActivitiesEvent -> consume(event, relay) is LnZapEvent -> { event.zapRequest?.let { verifyAndConsume(it, relay) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt index 3d891afed..96091d565 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt @@ -246,6 +246,7 @@ open class Event( FileStorageEvent.kind -> FileStorageEvent(id, pubKey, createdAt, tags, content, sig) FileStorageHeaderEvent.kind -> FileStorageHeaderEvent(id, pubKey, createdAt, tags, content, sig) HighlightEvent.kind -> HighlightEvent(id, pubKey, createdAt, tags, content, sig) + LiveActivitiesEvent.kind -> LiveActivitiesEvent(id, pubKey, createdAt, tags, content, sig) LnZapEvent.kind -> LnZapEvent(id, pubKey, createdAt, tags, content, sig) LnZapPaymentRequestEvent.kind -> LnZapPaymentRequestEvent(id, pubKey, createdAt, tags, content, sig) LnZapPaymentResponseEvent.kind -> LnZapPaymentResponseEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LiveActivitiesEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LiveActivitiesEvent.kt new file mode 100644 index 000000000..6b70eeaaa --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LiveActivitiesEvent.kt @@ -0,0 +1,48 @@ +package com.vitorpamplona.amethyst.service.model + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey +import nostr.postr.Utils +import java.util.Date + +@Immutable +class LiveActivitiesEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: List>, + content: String, + sig: HexKey +) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent { + + override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: "" + override fun address() = ATag(kind, pubKey, dTag(), null) + + fun title() = tags.firstOrNull { it.size > 1 && it[0] == "title" }?.get(1) + fun summary() = tags.firstOrNull { it.size > 1 && it[0] == "summary" }?.get(1) + fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1) + fun streaming() = tags.firstOrNull { it.size > 1 && it[0] == "streaming" }?.get(1) + fun starts() = tags.firstOrNull { it.size > 1 && it[0] == "starts" }?.get(1) + fun ends() = tags.firstOrNull { it.size > 1 && it[0] == "ends" }?.get(1) + fun status() = tags.firstOrNull { it.size > 1 && it[0] == "status" }?.get(1) + fun currentParticipants() = tags.firstOrNull { it.size > 1 && it[0] == "current_participants" }?.get(1) + fun totalParticipants() = tags.firstOrNull { it.size > 1 && it[0] == "total_participants" }?.get(1) + + fun participants() = tags.filter { it.size > 1 && it[0] == "p" }.map { Participant(it[1], it.getOrNull(2)) } + + companion object { + const val kind = 30311 + + fun create( + privateKey: ByteArray, + createdAt: Long = Date().time / 1000 + ): LiveActivitiesEvent { + val tags = mutableListOf>() + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() + val id = generateId(pubKey, createdAt, kind, tags, "") + val sig = Utils.sign(id, privateKey) + return LiveActivitiesEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey()) + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 7dcb0bc7f..0ae62b49d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -99,6 +99,7 @@ import com.vitorpamplona.amethyst.service.model.EventInterface import com.vitorpamplona.amethyst.service.model.FileHeaderEvent import com.vitorpamplona.amethyst.service.model.FileStorageHeaderEvent import com.vitorpamplona.amethyst.service.model.HighlightEvent +import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.model.Participant import com.vitorpamplona.amethyst.service.model.PeopleListEvent @@ -675,6 +676,10 @@ private fun RenderNoteRow( RenderPinListEvent(baseNote, backgroundColor, accountViewModel, nav) } + is LiveActivitiesEvent -> { + RenderLiveActivityEvent(baseNote, accountViewModel, nav) + } + is PrivateDmEvent -> { RenderPrivateMessage( baseNote, @@ -2462,6 +2467,80 @@ fun AudioTrackHeader(noteEvent: AudioTrackEvent, accountViewModel: AccountViewMo } } +@Composable +fun RenderLiveActivityEvent(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) { + val noteEvent = baseNote.event as? LiveActivitiesEvent ?: return + + val media = remember { noteEvent.streaming() } + val cover = remember { noteEvent.image() } + val subject = remember { noteEvent.title() } + val content = remember { noteEvent.summary() } + val participants = remember { noteEvent.participants() } + + var participantUsers by remember { mutableStateOf>>(emptyList()) } + + LaunchedEffect(key1 = participants) { + launch(Dispatchers.IO) { + participantUsers = participants.mapNotNull { part -> + LocalCache.checkGetOrCreateUser(part.key)?.let { Pair(part, it) } + } + } + } + + Row(modifier = Modifier.padding(top = 5.dp)) { + Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { + Row() { + subject?.let { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(top = 5.dp, bottom = 5.dp)) { + Text( + text = it, + fontWeight = FontWeight.Bold, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.fillMaxWidth() + ) + } + } + } + + participantUsers.forEach { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(top = 5.dp, start = 10.dp, end = 10.dp) + .clickable { + nav("User/${it.second.pubkeyHex}") + } + ) { + UserPicture(it.second, 25.dp, accountViewModel) + Spacer(Modifier.width(5.dp)) + UsernameDisplay(it.second, Modifier.weight(1f)) + Spacer(Modifier.width(5.dp)) + it.first.role?.let { + Text( + text = it.capitalize(Locale.ROOT), + color = MaterialTheme.colors.placeholderText, + maxLines = 1 + ) + } + } + } + + media?.let { media -> + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(10.dp) + ) { + VideoView( + videoUri = media, + description = subject + ) + } + } + } + } +} + @Composable private fun LongFormHeader(noteEvent: LongTextNoteEvent, note: Note, accountViewModel: AccountViewModel) { val image = remember(noteEvent) { noteEvent.image() }