Support for live chats in streaming events.
This commit is contained in:
@@ -103,7 +103,7 @@ class Account(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun followingChannels(): List<Channel> {
|
fun followingChannels(): List<Channel> {
|
||||||
return followingChannels.map { LocalCache.getOrCreateChannel(it) }
|
return followingChannels.mapNotNull { LocalCache.checkGetOrCreateChannel(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isWriteable(): Boolean {
|
fun isWriteable(): Boolean {
|
||||||
@@ -597,6 +597,27 @@ class Account(
|
|||||||
LocalCache.consume(signedEvent, null)
|
LocalCache.consume(signedEvent, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun sendLiveMessage(message: String, toChannel: ATag, replyTo: List<Note>?, mentions: List<User>?, zapReceiver: String? = null, wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null) {
|
||||||
|
if (!isWriteable()) return
|
||||||
|
|
||||||
|
// val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null }
|
||||||
|
val repliesToHex = replyTo?.map { it.idHex }
|
||||||
|
val mentionsHex = mentions?.map { it.pubkeyHex }
|
||||||
|
|
||||||
|
val signedEvent = LiveActivitiesChatMessageEvent.create(
|
||||||
|
message = message,
|
||||||
|
activity = toChannel,
|
||||||
|
replyTos = repliesToHex,
|
||||||
|
mentions = mentionsHex,
|
||||||
|
zapReceiver = zapReceiver,
|
||||||
|
markAsSensitive = wantsToMarkAsSensitive,
|
||||||
|
zapRaiserAmount = zapRaiserAmount,
|
||||||
|
privateKey = loggedIn.privKey!!
|
||||||
|
)
|
||||||
|
Client.send(signedEvent)
|
||||||
|
LocalCache.consume(signedEvent, null)
|
||||||
|
}
|
||||||
|
|
||||||
fun sendPrivateMessage(message: String, toUser: User, replyingTo: Note? = null, mentions: List<User>?, zapReceiver: String? = null, wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null) {
|
fun sendPrivateMessage(message: String, toUser: User, replyingTo: Note? = null, mentions: List<User>?, zapReceiver: String? = null, wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package com.vitorpamplona.amethyst.model
|
|||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import com.vitorpamplona.amethyst.service.NostrSingleChannelDataSource
|
import com.vitorpamplona.amethyst.service.NostrSingleChannelDataSource
|
||||||
|
import com.vitorpamplona.amethyst.service.model.ATag
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent
|
||||||
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
||||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||||
import fr.acinq.secp256k1.Hex
|
import fr.acinq.secp256k1.Hex
|
||||||
@@ -11,20 +13,96 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class Channel(val idHex: String) {
|
class PublicChatChannel(idHex: String) : Channel(idHex) {
|
||||||
var creator: User? = null
|
|
||||||
var info = ChannelCreateEvent.ChannelData(null, null, null)
|
var info = ChannelCreateEvent.ChannelData(null, null, null)
|
||||||
|
|
||||||
|
fun updateChannelInfo(creator: User, channelInfo: ChannelCreateEvent.ChannelData, updatedAt: Long) {
|
||||||
|
this.creator = creator
|
||||||
|
this.info = channelInfo
|
||||||
|
this.updatedMetadataAt = updatedAt
|
||||||
|
|
||||||
|
live.invalidateData()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toBestDisplayName(): String {
|
||||||
|
return info.name ?: super.toBestDisplayName()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun summary(): String? {
|
||||||
|
return info.about
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun profilePicture(): String? {
|
||||||
|
if (info.picture.isNullOrBlank()) info.picture = null
|
||||||
|
return info.picture ?: super.profilePicture()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun anyNameStartsWith(prefix: String): Boolean {
|
||||||
|
return listOfNotNull(info.name, info.about)
|
||||||
|
.filter { it.contains(prefix, true) }.isNotEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
class LiveActivitiesChannel(val address: ATag) : Channel(address.toTag()) {
|
||||||
|
var info: LiveActivitiesEvent? = null
|
||||||
|
|
||||||
|
override fun idNote() = address.toNAddr()
|
||||||
|
override fun idDisplayNote() = idNote().toShortenHex()
|
||||||
|
fun address() = address
|
||||||
|
|
||||||
|
fun updateChannelInfo(creator: User, channelInfo: LiveActivitiesEvent, updatedAt: Long) {
|
||||||
|
this.info = channelInfo
|
||||||
|
super.updateChannelInfo(creator, updatedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toBestDisplayName(): String {
|
||||||
|
return info?.title() ?: super.toBestDisplayName()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun summary(): String? {
|
||||||
|
return info?.summary()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun profilePicture(): String? {
|
||||||
|
return info?.image()?.ifBlank { null } ?: super.profilePicture()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun anyNameStartsWith(prefix: String): Boolean {
|
||||||
|
return listOfNotNull(info?.title(), info?.summary())
|
||||||
|
.filter { it.contains(prefix, true) }.isNotEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
abstract class Channel(val idHex: String) {
|
||||||
|
var creator: User? = null
|
||||||
|
|
||||||
var updatedMetadataAt: Long = 0
|
var updatedMetadataAt: Long = 0
|
||||||
|
|
||||||
val notes = ConcurrentHashMap<HexKey, Note>()
|
val notes = ConcurrentHashMap<HexKey, Note>()
|
||||||
|
|
||||||
fun id() = Hex.decode(idHex)
|
open fun id() = Hex.decode(idHex)
|
||||||
fun idNote() = id().toNote()
|
open fun idNote() = id().toNote()
|
||||||
fun idDisplayNote() = idNote().toShortenHex()
|
open fun idDisplayNote() = idNote().toShortenHex()
|
||||||
|
|
||||||
fun toBestDisplayName(): String {
|
open fun toBestDisplayName(): String {
|
||||||
return info.name ?: idDisplayNote()
|
return idDisplayNote()
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun summary(): String? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun profilePicture(): String? {
|
||||||
|
return creator?.profilePicture()
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun updateChannelInfo(creator: User, updatedAt: Long) {
|
||||||
|
this.creator = creator
|
||||||
|
this.updatedMetadataAt = updatedAt
|
||||||
|
|
||||||
|
live.invalidateData()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addNote(note: Note) {
|
fun addNote(note: Note) {
|
||||||
@@ -39,23 +117,7 @@ class Channel(val idHex: String) {
|
|||||||
notes.remove(noteHex)
|
notes.remove(noteHex)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateChannelInfo(creator: User, channelInfo: ChannelCreateEvent.ChannelData, updatedAt: Long) {
|
abstract fun anyNameStartsWith(prefix: String): Boolean
|
||||||
this.creator = creator
|
|
||||||
this.info = channelInfo
|
|
||||||
this.updatedMetadataAt = updatedAt
|
|
||||||
|
|
||||||
live.invalidateData()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun profilePicture(): String? {
|
|
||||||
if (info.picture.isNullOrBlank()) info.picture = null
|
|
||||||
return info.picture
|
|
||||||
}
|
|
||||||
|
|
||||||
fun anyNameStartsWith(prefix: String): Boolean {
|
|
||||||
return listOfNotNull(info.name, info.about)
|
|
||||||
.filter { it.startsWith(prefix, true) }.isNotEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Observers line up here.
|
// Observers line up here.
|
||||||
val live: ChannelLiveData = ChannelLiveData(this)
|
val live: ChannelLiveData = ChannelLiveData(this)
|
||||||
@@ -92,12 +154,20 @@ class ChannelLiveData(val channel: Channel) : LiveData<ChannelState>(ChannelStat
|
|||||||
|
|
||||||
override fun onActive() {
|
override fun onActive() {
|
||||||
super.onActive()
|
super.onActive()
|
||||||
|
if (channel is PublicChatChannel) {
|
||||||
NostrSingleChannelDataSource.add(channel.idHex)
|
NostrSingleChannelDataSource.add(channel.idHex)
|
||||||
|
} else {
|
||||||
|
NostrSingleChannelDataSource.add(channel.idHex)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onInactive() {
|
override fun onInactive() {
|
||||||
super.onInactive()
|
super.onInactive()
|
||||||
|
if (channel is PublicChatChannel) {
|
||||||
NostrSingleChannelDataSource.remove(channel.idHex)
|
NostrSingleChannelDataSource.remove(channel.idHex)
|
||||||
|
} else {
|
||||||
|
NostrSingleChannelDataSource.remove(channel.idHex)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.util.Log
|
|||||||
import com.vitorpamplona.amethyst.Amethyst
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||||
import com.vitorpamplona.amethyst.service.model.*
|
import com.vitorpamplona.amethyst.service.model.*
|
||||||
|
import com.vitorpamplona.amethyst.service.model.ATag.Companion.isATag
|
||||||
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
||||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||||
import com.vitorpamplona.amethyst.ui.components.BundledInsert
|
import com.vitorpamplona.amethyst.ui.components.BundledInsert
|
||||||
@@ -97,7 +98,15 @@ object LocalCache {
|
|||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
|
|
||||||
if (isValidHexNpub(key)) {
|
if (isValidHexNpub(key)) {
|
||||||
return getOrCreateChannel(key)
|
return getOrCreateChannel(key) {
|
||||||
|
PublicChatChannel(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val aTag = ATag.parse(key, null)
|
||||||
|
if (aTag != null) {
|
||||||
|
return getOrCreateChannel(aTag.toTag()) {
|
||||||
|
LiveActivitiesChannel(aTag)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -113,11 +122,11 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun getOrCreateChannel(key: String): Channel {
|
fun getOrCreateChannel(key: String, channelFactory: (String) -> Channel): Channel {
|
||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
|
|
||||||
return channels[key] ?: run {
|
return channels[key] ?: run {
|
||||||
val answer = Channel(key)
|
val answer = channelFactory(key)
|
||||||
channels.put(key, answer)
|
channels.put(key, answer)
|
||||||
answer
|
answer
|
||||||
}
|
}
|
||||||
@@ -333,6 +342,11 @@ object LocalCache {
|
|||||||
if (event.createdAt > (note.createdAt() ?: 0)) {
|
if (event.createdAt > (note.createdAt() ?: 0)) {
|
||||||
note.loadEvent(event, author, emptyList())
|
note.loadEvent(event, author, emptyList())
|
||||||
|
|
||||||
|
val channel = getOrCreateChannel(note.idHex) {
|
||||||
|
LiveActivitiesChannel(note.address)
|
||||||
|
} as? LiveActivitiesChannel
|
||||||
|
channel?.updateChannelInfo(author, event, event.createdAt)
|
||||||
|
|
||||||
refreshObservers(note)
|
refreshObservers(note)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -572,8 +586,11 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deleteNote.channelHex()?.let {
|
deleteNote.channelHex()?.let {
|
||||||
val channel = checkGetOrCreateChannel(it)
|
channels[it]?.removeNote(deleteNote)
|
||||||
channel?.removeNote(deleteNote)
|
}
|
||||||
|
|
||||||
|
(deleteNote.event as? LiveActivitiesChatMessageEvent)?.activity()?.let {
|
||||||
|
channels[it.toTag()]?.removeNote(deleteNote)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deleteNote.event is PrivateDmEvent) {
|
if (deleteNote.event is PrivateDmEvent) {
|
||||||
@@ -708,7 +725,9 @@ object LocalCache {
|
|||||||
|
|
||||||
fun consume(event: ChannelCreateEvent) {
|
fun consume(event: ChannelCreateEvent) {
|
||||||
// Log.d("MT", "New Event ${event.content} ${event.id.toHex()}")
|
// Log.d("MT", "New Event ${event.content} ${event.id.toHex()}")
|
||||||
val oldChannel = getOrCreateChannel(event.id)
|
val oldChannel = getOrCreateChannel(event.id) {
|
||||||
|
PublicChatChannel(it)
|
||||||
|
}
|
||||||
val author = getOrCreateUser(event.pubKey)
|
val author = getOrCreateUser(event.pubKey)
|
||||||
|
|
||||||
val note = getOrCreateNote(event.id)
|
val note = getOrCreateNote(event.id)
|
||||||
@@ -723,9 +742,11 @@ object LocalCache {
|
|||||||
return // older data, does nothing
|
return // older data, does nothing
|
||||||
}
|
}
|
||||||
if (oldChannel.creator == null || oldChannel.creator == author) {
|
if (oldChannel.creator == null || oldChannel.creator == author) {
|
||||||
|
if (oldChannel is PublicChatChannel) {
|
||||||
oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt)
|
oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun consume(event: ChannelMetadataEvent) {
|
fun consume(event: ChannelMetadataEvent) {
|
||||||
val channelId = event.channel()
|
val channelId = event.channel()
|
||||||
@@ -734,11 +755,14 @@ object LocalCache {
|
|||||||
|
|
||||||
// new event
|
// new event
|
||||||
val oldChannel = checkGetOrCreateChannel(channelId) ?: return
|
val oldChannel = checkGetOrCreateChannel(channelId) ?: return
|
||||||
|
|
||||||
val author = getOrCreateUser(event.pubKey)
|
val author = getOrCreateUser(event.pubKey)
|
||||||
if (event.createdAt > oldChannel.updatedMetadataAt) {
|
if (event.createdAt > oldChannel.updatedMetadataAt) {
|
||||||
if (oldChannel.creator == null || oldChannel.creator == author) {
|
if (oldChannel.creator == null || oldChannel.creator == author) {
|
||||||
|
if (oldChannel is PublicChatChannel) {
|
||||||
oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt)
|
oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()} ${formattedDateTime(event.createdAt)} > ${formattedDateTime(oldUser.updatedAt)}")
|
// Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()} ${formattedDateTime(event.createdAt)} > ${formattedDateTime(oldUser.updatedAt)}")
|
||||||
}
|
}
|
||||||
@@ -795,6 +819,49 @@ object LocalCache {
|
|||||||
refreshObservers(note)
|
refreshObservers(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun consume(event: LiveActivitiesChatMessageEvent, relay: Relay?) {
|
||||||
|
val activityId = event.activity() ?: return
|
||||||
|
|
||||||
|
val channel = getOrCreateChannel(activityId.toTag()) {
|
||||||
|
LiveActivitiesChannel(activityId)
|
||||||
|
}
|
||||||
|
|
||||||
|
val note = getOrCreateNote(event.id)
|
||||||
|
channel.addNote(note)
|
||||||
|
|
||||||
|
val author = getOrCreateUser(event.pubKey)
|
||||||
|
|
||||||
|
if (relay != null) {
|
||||||
|
author.addRelayBeingUsed(relay, event.createdAt)
|
||||||
|
note.addRelay(relay)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already processed this event.
|
||||||
|
if (note.event != null) return
|
||||||
|
|
||||||
|
if (antiSpam.isSpam(event, relay)) {
|
||||||
|
relay?.let {
|
||||||
|
it.spamCounter++
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val replyTo = event.tagsWithoutCitations()
|
||||||
|
.filter { it != event.activity()?.toTag() }
|
||||||
|
.mapNotNull { checkGetOrCreateNote(it) }
|
||||||
|
|
||||||
|
note.loadEvent(event, author, replyTo)
|
||||||
|
|
||||||
|
Log.d("CM", "AAA REPLY TO ${event.content} ${event.taggedEvents()}")
|
||||||
|
|
||||||
|
// Counts the replies
|
||||||
|
replyTo.forEach {
|
||||||
|
it.addReply(note)
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshObservers(note)
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("UNUSED_PARAMETER")
|
@Suppress("UNUSED_PARAMETER")
|
||||||
fun consume(event: ChannelHideMessageEvent) {
|
fun consume(event: ChannelHideMessageEvent) {
|
||||||
}
|
}
|
||||||
@@ -1084,7 +1151,7 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
println("PRUNE: ${toBeRemoved.size} messages removed from ${it.value.info.name}")
|
println("PRUNE: ${toBeRemoved.size} messages removed from ${it.value.toBestDisplayName()}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1172,6 +1239,7 @@ object LocalCache {
|
|||||||
is FileStorageHeaderEvent -> consume(event, relay)
|
is FileStorageHeaderEvent -> consume(event, relay)
|
||||||
is HighlightEvent -> consume(event, relay)
|
is HighlightEvent -> consume(event, relay)
|
||||||
is LiveActivitiesEvent -> consume(event, relay)
|
is LiveActivitiesEvent -> consume(event, relay)
|
||||||
|
is LiveActivitiesChatMessageEvent -> consume(event, relay)
|
||||||
is LnZapEvent -> {
|
is LnZapEvent -> {
|
||||||
event.zapRequest?.let {
|
event.zapRequest?.let {
|
||||||
verifyAndConsume(it, relay)
|
verifyAndConsume(it, relay)
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ open class Note(val idHex: String) {
|
|||||||
return (event as? ChannelMessageEvent)?.channel()
|
return (event as? ChannelMessageEvent)?.channel()
|
||||||
?: (event as? ChannelMetadataEvent)?.channel()
|
?: (event as? ChannelMetadataEvent)?.channel()
|
||||||
?: (event as? ChannelCreateEvent)?.id
|
?: (event as? ChannelCreateEvent)?.id
|
||||||
|
?: (event as? LiveActivitiesChatMessageEvent)?.activity()?.toTag()
|
||||||
|
?: (event as? LiveActivitiesEvent)?.address()?.toTag()
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun address(): ATag? = null
|
open fun address(): ATag? = null
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ package com.vitorpamplona.amethyst.service
|
|||||||
|
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.Channel
|
import com.vitorpamplona.amethyst.model.Channel
|
||||||
|
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||||
|
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.LiveActivitiesChatMessageEvent
|
||||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||||
@@ -25,7 +28,7 @@ object NostrChannelDataSource : NostrDataSource("ChatroomFeed") {
|
|||||||
fun createMessagesByMeToChannelFilter(): TypedFilter? {
|
fun createMessagesByMeToChannelFilter(): TypedFilter? {
|
||||||
val myAccount = account ?: return null
|
val myAccount = account ?: return null
|
||||||
|
|
||||||
if (channel != null) {
|
if (channel is PublicChatChannel) {
|
||||||
// Brings on messages by the user from all other relays.
|
// Brings on messages by the user from all other relays.
|
||||||
// Since we ship with write to public, read from private only
|
// Since we ship with write to public, read from private only
|
||||||
// this guarantees that messages from the author do not disappear.
|
// this guarantees that messages from the author do not disappear.
|
||||||
@@ -37,12 +40,24 @@ object NostrChannelDataSource : NostrDataSource("ChatroomFeed") {
|
|||||||
limit = 50
|
limit = 50
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
} else if (channel is LiveActivitiesChannel) {
|
||||||
|
// Brings on messages by the user from all other relays.
|
||||||
|
// Since we ship with write to public, read from private only
|
||||||
|
// this guarantees that messages from the author do not disappear.
|
||||||
|
return TypedFilter(
|
||||||
|
types = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS, FeedType.GLOBAL, FeedType.SEARCH),
|
||||||
|
filter = JsonFilter(
|
||||||
|
kinds = listOf(LiveActivitiesChatMessageEvent.kind),
|
||||||
|
authors = listOf(myAccount.userProfile().pubkeyHex),
|
||||||
|
limit = 50
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createMessagesToChannelFilter(): TypedFilter? {
|
fun createMessagesToChannelFilter(): TypedFilter? {
|
||||||
if (channel != null) {
|
if (channel is PublicChatChannel) {
|
||||||
return TypedFilter(
|
return TypedFilter(
|
||||||
types = setOf(FeedType.PUBLIC_CHATS),
|
types = setOf(FeedType.PUBLIC_CHATS),
|
||||||
filter = JsonFilter(
|
filter = JsonFilter(
|
||||||
@@ -51,6 +66,15 @@ object NostrChannelDataSource : NostrDataSource("ChatroomFeed") {
|
|||||||
limit = 200
|
limit = 200
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
} else if (channel is LiveActivitiesChannel) {
|
||||||
|
return TypedFilter(
|
||||||
|
types = setOf(FeedType.PUBLIC_CHATS),
|
||||||
|
filter = JsonFilter(
|
||||||
|
kinds = listOf(LiveActivitiesChatMessageEvent.kind),
|
||||||
|
tags = mapOf("a" to listOfNotNull(channel?.idHex)),
|
||||||
|
limit = 200
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-3
@@ -1,6 +1,8 @@
|
|||||||
package com.vitorpamplona.amethyst.service
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||||
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
|
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
|
||||||
@@ -30,8 +32,8 @@ object NostrSingleChannelDataSource : NostrDataSource("SingleChannelFeed") {
|
|||||||
|
|
||||||
fun createLoadEventsIfNotLoadedFilter(): TypedFilter? {
|
fun createLoadEventsIfNotLoadedFilter(): TypedFilter? {
|
||||||
val directEventsToLoad = channelsToWatch
|
val directEventsToLoad = channelsToWatch
|
||||||
.map { LocalCache.getOrCreateChannel(it) }
|
.mapNotNull { LocalCache.checkGetOrCreateChannel(it) }
|
||||||
.filter { it.notes.isEmpty() }
|
.filter { it.notes.isEmpty() && it is PublicChatChannel }
|
||||||
|
|
||||||
val interestedEvents = (directEventsToLoad).map { it.idHex }.toSet()
|
val interestedEvents = (directEventsToLoad).map { it.idHex }.toSet()
|
||||||
|
|
||||||
@@ -49,13 +51,43 @@ object NostrSingleChannelDataSource : NostrDataSource("SingleChannelFeed") {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createLoadStreamingIfNotLoadedFilter(): List<TypedFilter>? {
|
||||||
|
val directEventsToLoad = channelsToWatch
|
||||||
|
.mapNotNull { LocalCache.checkGetOrCreateChannel(it) }
|
||||||
|
.filterIsInstance<LiveActivitiesChannel>()
|
||||||
|
.filter { it.info == null }
|
||||||
|
|
||||||
|
val interestedEvents = (directEventsToLoad).map { it.idHex }.toSet()
|
||||||
|
|
||||||
|
if (interestedEvents.isEmpty()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// downloads linked events to this event.
|
||||||
|
return directEventsToLoad.map {
|
||||||
|
it.address().let { aTag ->
|
||||||
|
TypedFilter(
|
||||||
|
types = COMMON_FEED_TYPES,
|
||||||
|
filter = JsonFilter(
|
||||||
|
kinds = listOf(aTag.kind),
|
||||||
|
tags = mapOf("d" to listOf(aTag.dTag)),
|
||||||
|
authors = listOf(aTag.pubKeyHex.substring(0, 8))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val singleChannelChannel = requestNewChannel()
|
val singleChannelChannel = requestNewChannel()
|
||||||
|
|
||||||
override fun updateChannelFilters() {
|
override fun updateChannelFilters() {
|
||||||
val reactions = createRepliesAndReactionsFilter()
|
val reactions = createRepliesAndReactionsFilter()
|
||||||
val missing = createLoadEventsIfNotLoadedFilter()
|
val missing = createLoadEventsIfNotLoadedFilter()
|
||||||
|
val missingStreaming = createLoadStreamingIfNotLoadedFilter()
|
||||||
|
|
||||||
singleChannelChannel.typedFilters = listOfNotNull(reactions, missing).ifEmpty { null }
|
singleChannelChannel.typedFilters = (
|
||||||
|
(listOfNotNull(reactions, missing)) + (missingStreaming ?: emptyList())
|
||||||
|
).ifEmpty { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun add(eventId: String) {
|
fun add(eventId: String) {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ open class BaseTextNoteEvent(
|
|||||||
sig: HexKey
|
sig: HexKey
|
||||||
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
fun mentions() = taggedUsers()
|
fun mentions() = taggedUsers()
|
||||||
open fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
open fun replyTos() = taggedEvents()
|
||||||
|
|
||||||
private var citedUsersCache: Set<HexKey>? = null
|
private var citedUsersCache: Set<HexKey>? = null
|
||||||
private var citedNotesCache: Set<HexKey>? = null
|
private var citedNotesCache: Set<HexKey>? = null
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ open class Event(
|
|||||||
GenericRepostEvent.kind -> GenericRepostEvent(id, pubKey, createdAt, tags, content, sig)
|
GenericRepostEvent.kind -> GenericRepostEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
HighlightEvent.kind -> HighlightEvent(id, pubKey, createdAt, tags, content, sig)
|
HighlightEvent.kind -> HighlightEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
LiveActivitiesEvent.kind -> LiveActivitiesEvent(id, pubKey, createdAt, tags, content, sig)
|
LiveActivitiesEvent.kind -> LiveActivitiesEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
LiveActivitiesChatMessageEvent.kind -> LiveActivitiesChatMessageEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
LnZapEvent.kind -> LnZapEvent(id, pubKey, createdAt, tags, content, sig)
|
LnZapEvent.kind -> LnZapEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
LnZapPaymentRequestEvent.kind -> LnZapPaymentRequestEvent(id, pubKey, createdAt, tags, content, sig)
|
LnZapPaymentRequestEvent.kind -> LnZapPaymentRequestEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
LnZapPaymentResponseEvent.kind -> LnZapPaymentResponseEvent(id, pubKey, createdAt, tags, content, sig)
|
LnZapPaymentResponseEvent.kind -> LnZapPaymentResponseEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
|||||||
+82
@@ -0,0 +1,82 @@
|
|||||||
|
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 LiveActivitiesChatMessageEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
tags: List<List<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey
|
||||||
|
) : BaseTextNoteEvent(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
|
|
||||||
|
private fun innerActivity() = tags.firstOrNull {
|
||||||
|
it.size > 3 && it[0] == "a" && it[3] == "root"
|
||||||
|
} ?: tags.firstOrNull {
|
||||||
|
it.size > 1 && it[0] == "a"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun activityHex() = innerActivity()?.let {
|
||||||
|
it.getOrNull(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun activity() = innerActivity()?.let {
|
||||||
|
if (it.size > 1) {
|
||||||
|
val aTagValue = it[1]
|
||||||
|
val relay = it.getOrNull(2)
|
||||||
|
|
||||||
|
ATag.parse(aTagValue, relay)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun replyTos() = taggedEvents().minus(activityHex() ?: "")
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val kind = 1311
|
||||||
|
|
||||||
|
fun create(
|
||||||
|
message: String,
|
||||||
|
activity: ATag,
|
||||||
|
replyTos: List<String>? = null,
|
||||||
|
mentions: List<String>? = null,
|
||||||
|
zapReceiver: String?,
|
||||||
|
privateKey: ByteArray,
|
||||||
|
createdAt: Long = Date().time / 1000,
|
||||||
|
markAsSensitive: Boolean,
|
||||||
|
zapRaiserAmount: Long?
|
||||||
|
): LiveActivitiesChatMessageEvent {
|
||||||
|
val content = message
|
||||||
|
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||||
|
val tags = mutableListOf(
|
||||||
|
listOf("a", activity.toTag(), "", "root")
|
||||||
|
)
|
||||||
|
replyTos?.forEach {
|
||||||
|
tags.add(listOf("e", it))
|
||||||
|
}
|
||||||
|
mentions?.forEach {
|
||||||
|
tags.add(listOf("p", it))
|
||||||
|
}
|
||||||
|
zapReceiver?.let {
|
||||||
|
tags.add(listOf("zap", it))
|
||||||
|
}
|
||||||
|
if (markAsSensitive) {
|
||||||
|
tags.add(listOf("content-warning", ""))
|
||||||
|
}
|
||||||
|
zapRaiserAmount?.let {
|
||||||
|
tags.add(listOf("zapraiser", "$it"))
|
||||||
|
}
|
||||||
|
|
||||||
|
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||||
|
val sig = Utils.sign(id, privateKey)
|
||||||
|
return LiveActivitiesChatMessageEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -352,12 +352,12 @@ private fun RenderChannel(
|
|||||||
channelPicture = item.profilePicture(),
|
channelPicture = item.profilePicture(),
|
||||||
channelTitle = {
|
channelTitle = {
|
||||||
Text(
|
Text(
|
||||||
"${item.info.name}",
|
item.toBestDisplayName(),
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
channelLastTime = null,
|
channelLastTime = null,
|
||||||
channelLastContent = item.info.about,
|
channelLastContent = item.summary(),
|
||||||
false,
|
false,
|
||||||
onClick = onClick
|
onClick = onClick
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -25,14 +25,14 @@ import androidx.compose.ui.window.Dialog
|
|||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Channel
|
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NewChannelView(onClose: () -> Unit, accountViewModel: AccountViewModel, channel: Channel? = null) {
|
fun NewChannelView(onClose: () -> Unit, accountViewModel: AccountViewModel, channel: PublicChatChannel? = null) {
|
||||||
val postViewModel: NewChannelViewModel = viewModel()
|
val postViewModel: NewChannelViewModel = viewModel()
|
||||||
postViewModel.load(accountViewModel.account, channel)
|
postViewModel.load(accountViewModel.account, channel)
|
||||||
|
|
||||||
|
|||||||
@@ -4,17 +4,17 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.Channel
|
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||||
|
|
||||||
class NewChannelViewModel : ViewModel() {
|
class NewChannelViewModel : ViewModel() {
|
||||||
private var account: Account? = null
|
private var account: Account? = null
|
||||||
private var originalChannel: Channel? = null
|
private var originalChannel: PublicChatChannel? = null
|
||||||
|
|
||||||
val channelName = mutableStateOf(TextFieldValue())
|
val channelName = mutableStateOf(TextFieldValue())
|
||||||
val channelPicture = mutableStateOf(TextFieldValue())
|
val channelPicture = mutableStateOf(TextFieldValue())
|
||||||
val channelDescription = mutableStateOf(TextFieldValue())
|
val channelDescription = mutableStateOf(TextFieldValue())
|
||||||
|
|
||||||
fun load(account: Account, channel: Channel?) {
|
fun load(account: Account, channel: PublicChatChannel?) {
|
||||||
this.account = account
|
this.account = account
|
||||||
if (channel != null) {
|
if (channel != null) {
|
||||||
originalChannel = channel
|
originalChannel = channel
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import com.vitorpamplona.amethyst.model.*
|
import com.vitorpamplona.amethyst.model.*
|
||||||
import com.vitorpamplona.amethyst.service.FileHeader
|
import com.vitorpamplona.amethyst.service.FileHeader
|
||||||
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
||||||
|
import com.vitorpamplona.amethyst.service.model.AddressableEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.BaseTextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.BaseTextNoteEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||||
@@ -143,7 +144,11 @@ open class NewPostViewModel() : ViewModel() {
|
|||||||
if (wantsPoll) {
|
if (wantsPoll) {
|
||||||
account?.sendPoll(tagger.message, tagger.replyTos, tagger.mentions, pollOptions, valueMaximum, valueMinimum, consensusThreshold, closedAt, zapReceiver, wantsToMarkAsSensitive, localZapRaiserAmount)
|
account?.sendPoll(tagger.message, tagger.replyTos, tagger.mentions, pollOptions, valueMaximum, valueMinimum, consensusThreshold, closedAt, zapReceiver, wantsToMarkAsSensitive, localZapRaiserAmount)
|
||||||
} else if (originalNote?.channelHex() != null) {
|
} else if (originalNote?.channelHex() != null) {
|
||||||
|
if (originalNote is AddressableEvent && originalNote?.address() != null) {
|
||||||
|
account?.sendLiveMessage(tagger.message, originalNote?.address()!!, tagger.replyTos, tagger.mentions, zapReceiver, wantsToMarkAsSensitive, localZapRaiserAmount)
|
||||||
|
} else {
|
||||||
account?.sendChannelMessage(tagger.message, tagger.channelHex!!, tagger.replyTos, tagger.mentions, zapReceiver, wantsToMarkAsSensitive, localZapRaiserAmount)
|
account?.sendChannelMessage(tagger.message, tagger.channelHex!!, tagger.replyTos, tagger.mentions, zapReceiver, wantsToMarkAsSensitive, localZapRaiserAmount)
|
||||||
|
}
|
||||||
} else if (originalNote?.event is PrivateDmEvent) {
|
} else if (originalNote?.event is PrivateDmEvent) {
|
||||||
account?.sendPrivateMessage(tagger.message, originalNote!!.author!!, originalNote!!, tagger.mentions, zapReceiver, wantsToMarkAsSensitive, localZapRaiserAmount)
|
account?.sendPrivateMessage(tagger.message, originalNote!!.author!!, originalNote!!, tagger.mentions, zapReceiver, wantsToMarkAsSensitive, localZapRaiserAmount)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ private fun ChannelRoomCompose(
|
|||||||
}
|
}
|
||||||
val channelName by remember(channelState) {
|
val channelName by remember(channelState) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
channel.info.name
|
channel.toBestDisplayName()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ import androidx.compose.ui.unit.sp
|
|||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
||||||
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
|
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
|
||||||
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
|
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
|
||||||
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
|
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
|
||||||
@@ -244,7 +244,7 @@ fun ChatroomMessageCompose(
|
|||||||
bubbleSize = it
|
bubbleSize = it
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
if ((innerQuote || note.author != loggedIn) && noteEvent is ChannelMessageEvent) {
|
if ((innerQuote || note.author != loggedIn) && noteEvent !is PrivateDmEvent) {
|
||||||
DrawAuthorInfo(
|
DrawAuthorInfo(
|
||||||
baseNote,
|
baseNote,
|
||||||
alignment,
|
alignment,
|
||||||
@@ -255,6 +255,7 @@ fun ChatroomMessageCompose(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val replyTo = note.replyTo
|
val replyTo = note.replyTo
|
||||||
|
println("AAA replyTo ${replyTo?.lastOrNull()}")
|
||||||
if (!innerQuote && !replyTo.isNullOrEmpty()) {
|
if (!innerQuote && !replyTo.isNullOrEmpty()) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
replyTo.lastOrNull()?.let { note ->
|
replyTo.lastOrNull()?.let { note ->
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ import com.vitorpamplona.amethyst.service.model.FileHeaderEvent
|
|||||||
import com.vitorpamplona.amethyst.service.model.FileStorageHeaderEvent
|
import com.vitorpamplona.amethyst.service.model.FileStorageHeaderEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
|
import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.HighlightEvent
|
import com.vitorpamplona.amethyst.service.model.HighlightEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.LiveActivitiesChatMessageEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent
|
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.Participant
|
import com.vitorpamplona.amethyst.service.model.Participant
|
||||||
@@ -738,6 +739,10 @@ fun routeFor(note: Note, loggedIn: User): String? {
|
|||||||
note.channelHex()?.let {
|
note.channelHex()?.let {
|
||||||
return "Channel/$it"
|
return "Channel/$it"
|
||||||
}
|
}
|
||||||
|
} else if (noteEvent is LiveActivitiesEvent || noteEvent is LiveActivitiesChatMessageEvent) {
|
||||||
|
note.channelHex()?.let {
|
||||||
|
return "Channel/$it"
|
||||||
|
}
|
||||||
} else if (noteEvent is PrivateDmEvent) {
|
} else if (noteEvent is PrivateDmEvent) {
|
||||||
return "Room/${noteEvent.talkingWith(loggedIn.pubkeyHex)}"
|
return "Room/${noteEvent.talkingWith(loggedIn.pubkeyHex)}"
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -332,23 +332,30 @@ fun NoteQuickActionItem(icon: ImageVector, label: String, onClick: () -> Unit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DeleteAlertDialog(note: Note, accountViewModel: AccountViewModel, onDismiss: () -> Unit) =
|
fun DeleteAlertDialog(note: Note, accountViewModel: AccountViewModel, onDismiss: () -> Unit) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
QuickActionAlertDialog(
|
QuickActionAlertDialog(
|
||||||
title = stringResource(R.string.quick_action_request_deletion_alert_title),
|
title = stringResource(R.string.quick_action_request_deletion_alert_title),
|
||||||
textContent = stringResource(R.string.quick_action_request_deletion_alert_body),
|
textContent = stringResource(R.string.quick_action_request_deletion_alert_body),
|
||||||
buttonIcon = Icons.Default.Delete,
|
buttonIcon = Icons.Default.Delete,
|
||||||
buttonText = stringResource(R.string.quick_action_delete_dialog_btn),
|
buttonText = stringResource(R.string.quick_action_delete_dialog_btn),
|
||||||
onClickDoOnce = {
|
onClickDoOnce = {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
accountViewModel.delete(note)
|
accountViewModel.delete(note)
|
||||||
|
}
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
},
|
||||||
onClickDontShowAgain = {
|
onClickDontShowAgain = {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
accountViewModel.delete(note)
|
accountViewModel.delete(note)
|
||||||
accountViewModel.dontShowDeleteRequestDialog()
|
accountViewModel.dontShowDeleteRequestDialog()
|
||||||
|
}
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
},
|
||||||
onDismiss = onDismiss
|
onDismiss = onDismiss
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun BlockAlertDialog(note: Note, accountViewModel: AccountViewModel, onDismiss: () -> Unit) =
|
private fun BlockAlertDialog(note: Note, accountViewModel: AccountViewModel, onDismiss: () -> Unit) =
|
||||||
@@ -394,7 +401,9 @@ private fun QuickActionAlertDialog(
|
|||||||
},
|
},
|
||||||
buttons = {
|
buttons = {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.padding(all = 8.dp).fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.padding(all = 8.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
TextButton(onClick = onClickDontShowAgain) {
|
TextButton(onClick = onClickDontShowAgain) {
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ fun ReplyInformationChannel(
|
|||||||
val channel = channelState?.channel ?: return
|
val channel = channelState?.channel ?: return
|
||||||
|
|
||||||
val channelName = remember(channelState) {
|
val channelName = remember(channelState) {
|
||||||
AnnotatedString("${channel.info.name} ")
|
AnnotatedString("${channel.toBestDisplayName()} ")
|
||||||
}
|
}
|
||||||
|
|
||||||
FlowRow() {
|
FlowRow() {
|
||||||
|
|||||||
@@ -64,8 +64,10 @@ import androidx.lifecycle.LifecycleEventObserver
|
|||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Channel
|
import com.vitorpamplona.amethyst.model.Channel
|
||||||
|
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||||
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
|
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NewChannelView
|
import com.vitorpamplona.amethyst.ui.actions.NewChannelView
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger
|
import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger
|
||||||
@@ -75,6 +77,7 @@ import com.vitorpamplona.amethyst.ui.actions.ServersAvailable
|
|||||||
import com.vitorpamplona.amethyst.ui.actions.UploadFromGallery
|
import com.vitorpamplona.amethyst.ui.actions.UploadFromGallery
|
||||||
import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
||||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.VideoView
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||||
import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose
|
import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrChannelFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrChannelFeedViewModel
|
||||||
@@ -232,6 +235,7 @@ fun ChannelScreen(
|
|||||||
message = newPostModel.message.text
|
message = newPostModel.message.text
|
||||||
)
|
)
|
||||||
tagger.run()
|
tagger.run()
|
||||||
|
if (channel is PublicChatChannel) {
|
||||||
accountViewModel.account.sendChannelMessage(
|
accountViewModel.account.sendChannelMessage(
|
||||||
message = tagger.message,
|
message = tagger.message,
|
||||||
toChannel = channel.idHex,
|
toChannel = channel.idHex,
|
||||||
@@ -239,6 +243,15 @@ fun ChannelScreen(
|
|||||||
mentions = tagger.mentions,
|
mentions = tagger.mentions,
|
||||||
wantsToMarkAsSensitive = false
|
wantsToMarkAsSensitive = false
|
||||||
)
|
)
|
||||||
|
} else if (channel is LiveActivitiesChannel) {
|
||||||
|
accountViewModel.account.sendLiveMessage(
|
||||||
|
message = tagger.message,
|
||||||
|
toChannel = channel.address,
|
||||||
|
replyTo = tagger.replyTos,
|
||||||
|
mentions = tagger.mentions,
|
||||||
|
wantsToMarkAsSensitive = false
|
||||||
|
)
|
||||||
|
}
|
||||||
newPostModel.message = TextFieldValue("")
|
newPostModel.message = TextFieldValue("")
|
||||||
replyTo.value = null
|
replyTo.value = null
|
||||||
feedViewModel.sendToTop()
|
feedViewModel.sendToTop()
|
||||||
@@ -412,14 +425,14 @@ fun ChannelHeader(baseChannel: Channel, accountViewModel: AccountViewModel, nav:
|
|||||||
) {
|
) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text(
|
Text(
|
||||||
"${channel.info.name}",
|
channel.toBestDisplayName(),
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text(
|
Text(
|
||||||
"${channel.info.about}",
|
"${channel.summary()}",
|
||||||
color = MaterialTheme.colors.placeholderText,
|
color = MaterialTheme.colors.placeholderText,
|
||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
@@ -433,10 +446,29 @@ fun ChannelHeader(baseChannel: Channel, accountViewModel: AccountViewModel, nav:
|
|||||||
.height(Size35dp)
|
.height(Size35dp)
|
||||||
.padding(bottom = 3.dp)
|
.padding(bottom = 3.dp)
|
||||||
) {
|
) {
|
||||||
|
if (channel is PublicChatChannel) {
|
||||||
ChannelActionOptions(channel, accountViewModel, nav)
|
ChannelActionOptions(channel, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel is LiveActivitiesChannel) {
|
||||||
|
val streamingUrl by remember(channelState) {
|
||||||
|
derivedStateOf {
|
||||||
|
channel.info?.streaming()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (streamingUrl != null) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(bottom = 5.dp)) {
|
||||||
|
VideoView(
|
||||||
|
videoUri = streamingUrl!!,
|
||||||
|
description = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Divider(
|
Divider(
|
||||||
modifier = Modifier.padding(start = 12.dp, end = 12.dp),
|
modifier = Modifier.padding(start = 12.dp, end = 12.dp),
|
||||||
@@ -447,7 +479,7 @@ fun ChannelHeader(baseChannel: Channel, accountViewModel: AccountViewModel, nav:
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ChannelActionOptions(
|
private fun ChannelActionOptions(
|
||||||
channel: Channel,
|
channel: PublicChatChannel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (String) -> Unit
|
nav: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
@@ -514,7 +546,7 @@ private fun NoteCopyButton(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun EditButton(accountViewModel: AccountViewModel, channel: Channel) {
|
private fun EditButton(accountViewModel: AccountViewModel, channel: PublicChatChannel) {
|
||||||
var wantsToPost by remember {
|
var wantsToPost by remember {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -399,12 +399,12 @@ private fun DisplaySearchResults(
|
|||||||
channelPicture = item.profilePicture(),
|
channelPicture = item.profilePicture(),
|
||||||
channelTitle = {
|
channelTitle = {
|
||||||
Text(
|
Text(
|
||||||
"${item.info.name}",
|
"${item.toBestDisplayName()}",
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
channelLastTime = null,
|
channelLastTime = null,
|
||||||
channelLastContent = item.info.about,
|
channelLastContent = item.summary(),
|
||||||
false,
|
false,
|
||||||
onClick = { nav("Channel/${item.idHex}") }
|
onClick = { nav("Channel/${item.idHex}") }
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user