- Support for Emoji Packs
- Support for Personal Emoji Lists - Support for Custom emoji Reactions
This commit is contained in:
@@ -176,6 +176,19 @@ class Account(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (reaction.startsWith(":")) {
|
||||||
|
val emojiUrl = EmojiUrl.decode(reaction)
|
||||||
|
if (emojiUrl != null) {
|
||||||
|
note.event?.let {
|
||||||
|
val event = ReactionEvent.create(emojiUrl, it, loggedIn.privKey!!)
|
||||||
|
Client.send(event)
|
||||||
|
LocalCache.consume(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
note.event?.let {
|
note.event?.let {
|
||||||
val event = ReactionEvent.create(reaction, it, loggedIn.privKey!!)
|
val event = ReactionEvent.create(reaction, it, loggedIn.privKey!!)
|
||||||
Client.send(event)
|
Client.send(event)
|
||||||
@@ -674,6 +687,51 @@ class Account(
|
|||||||
joinChannel(event.id)
|
joinChannel(event.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun removeEmojiPack(usersEmojiList: Note, emojiList: Note) {
|
||||||
|
if (!isWriteable()) return
|
||||||
|
|
||||||
|
val noteEvent = usersEmojiList.event
|
||||||
|
if (noteEvent !is EmojiPackSelectionEvent) return
|
||||||
|
val emojiListEvent = emojiList.event
|
||||||
|
if (emojiListEvent !is EmojiPackEvent) return
|
||||||
|
|
||||||
|
val event = EmojiPackSelectionEvent.create(
|
||||||
|
noteEvent.taggedAddresses().filter { it != emojiListEvent.address() },
|
||||||
|
loggedIn.privKey!!
|
||||||
|
)
|
||||||
|
|
||||||
|
Client.send(event)
|
||||||
|
LocalCache.consume(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addEmojiPack(usersEmojiList: Note, emojiList: Note) {
|
||||||
|
if (!isWriteable()) return
|
||||||
|
val emojiListEvent = emojiList.event
|
||||||
|
if (emojiListEvent !is EmojiPackEvent) return
|
||||||
|
|
||||||
|
val event = if (usersEmojiList.event == null) {
|
||||||
|
EmojiPackSelectionEvent.create(
|
||||||
|
listOf(emojiListEvent.address()),
|
||||||
|
loggedIn.privKey!!
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val noteEvent = usersEmojiList.event
|
||||||
|
if (noteEvent !is EmojiPackSelectionEvent) return
|
||||||
|
|
||||||
|
if (noteEvent.taggedAddresses().any { it == emojiListEvent.address() }) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
EmojiPackSelectionEvent.create(
|
||||||
|
noteEvent.taggedAddresses().plus(emojiListEvent.address()),
|
||||||
|
loggedIn.privKey!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Client.send(event)
|
||||||
|
LocalCache.consume(event)
|
||||||
|
}
|
||||||
|
|
||||||
fun addPrivateBookmark(note: Note) {
|
fun addPrivateBookmark(note: Note) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
|
|||||||
@@ -364,6 +364,44 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun consume(event: EmojiPackSelectionEvent) {
|
||||||
|
val version = getOrCreateNote(event.id)
|
||||||
|
val note = getOrCreateAddressableNote(event.address())
|
||||||
|
val author = getOrCreateUser(event.pubKey)
|
||||||
|
|
||||||
|
if (version.event == null) {
|
||||||
|
version.loadEvent(event, author, emptyList())
|
||||||
|
version.moveAllReferencesTo(note)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (note.event?.id() == event.id()) return
|
||||||
|
|
||||||
|
if (event.createdAt > (note.createdAt() ?: 0)) {
|
||||||
|
note.loadEvent(event, author, emptyList())
|
||||||
|
|
||||||
|
refreshObservers(note)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun consume(event: EmojiPackEvent) {
|
||||||
|
val version = getOrCreateNote(event.id)
|
||||||
|
val note = getOrCreateAddressableNote(event.address())
|
||||||
|
val author = getOrCreateUser(event.pubKey)
|
||||||
|
|
||||||
|
if (version.event == null) {
|
||||||
|
version.loadEvent(event, author, emptyList())
|
||||||
|
version.moveAllReferencesTo(note)
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
private fun consume(event: PinListEvent) {
|
||||||
val version = getOrCreateNote(event.id)
|
val version = getOrCreateNote(event.id)
|
||||||
val note = getOrCreateAddressableNote(event.address())
|
val note = getOrCreateAddressableNote(event.address())
|
||||||
@@ -1137,8 +1175,7 @@ object LocalCache {
|
|||||||
it.idNote().startsWith(text, true)
|
it.idNote().startsWith(text, true)
|
||||||
} + addressables.values.filter {
|
} + addressables.values.filter {
|
||||||
(it.event as? LongTextNoteEvent)?.content?.contains(text, true) ?: false ||
|
(it.event as? LongTextNoteEvent)?.content?.contains(text, true) ?: false ||
|
||||||
(it.event as? LongTextNoteEvent)?.title()?.contains(text, true) ?: false ||
|
it.event?.matchTag1With(text) ?: false ||
|
||||||
(it.event as? LongTextNoteEvent)?.summary()?.contains(text, true) ?: false ||
|
|
||||||
it.idHex.startsWith(text, true)
|
it.idHex.startsWith(text, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1278,6 +1315,8 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
is ContactListEvent -> consume(event)
|
is ContactListEvent -> consume(event)
|
||||||
is DeletionEvent -> consume(event)
|
is DeletionEvent -> consume(event)
|
||||||
|
is EmojiPackEvent -> consume(event)
|
||||||
|
is EmojiPackSelectionEvent -> consume(event)
|
||||||
|
|
||||||
is FileHeaderEvent -> consume(event, relay)
|
is FileHeaderEvent -> consume(event, relay)
|
||||||
is FileStorageEvent -> consume(event, relay)
|
is FileStorageEvent -> consume(event, relay)
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import androidx.compose.runtime.Stable
|
|||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
|
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
|
||||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||||
|
import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji
|
||||||
import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil
|
import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil
|
||||||
import com.vitorpamplona.amethyst.service.model.*
|
import com.vitorpamplona.amethyst.service.model.*
|
||||||
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
||||||
import com.vitorpamplona.amethyst.service.relays.EOSETime
|
import com.vitorpamplona.amethyst.service.relays.EOSETime
|
||||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
|
||||||
import com.vitorpamplona.amethyst.ui.actions.updated
|
import com.vitorpamplona.amethyst.ui.actions.updated
|
||||||
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
|
||||||
@@ -152,7 +154,8 @@ open class Note(val idHex: String) {
|
|||||||
liveSet?.boosts?.invalidateData()
|
liveSet?.boosts?.invalidateData()
|
||||||
}
|
}
|
||||||
fun removeReaction(note: Note) {
|
fun removeReaction(note: Note) {
|
||||||
val reaction = note.event?.content() ?: "+"
|
val tags = note.event?.tags() ?: emptyList()
|
||||||
|
val reaction = note.event?.content()?.firstFullCharOrEmoji(ImmutableListOfLists(tags)) ?: "+"
|
||||||
|
|
||||||
if (reaction in reactions.keys && reactions[reaction]?.contains(note) == true) {
|
if (reaction in reactions.keys && reactions[reaction]?.contains(note) == true) {
|
||||||
reactions[reaction]?.let {
|
reactions[reaction]?.let {
|
||||||
@@ -233,7 +236,8 @@ open class Note(val idHex: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun addReaction(note: Note) {
|
fun addReaction(note: Note) {
|
||||||
val reaction = note.event?.content() ?: "+"
|
val tags = note.event?.tags() ?: emptyList()
|
||||||
|
val reaction = note.event?.content()?.firstFullCharOrEmoji(ImmutableListOfLists(tags)) ?: "+"
|
||||||
|
|
||||||
if (reaction !in reactions.keys) {
|
if (reaction !in reactions.keys) {
|
||||||
reactions = reactions + Pair(reaction, setOf(note))
|
reactions = reactions + Pair(reaction, setOf(note))
|
||||||
@@ -462,7 +466,7 @@ open class Note(val idHex: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun reactedBy(loggedIn: User, content: String): List<Note> {
|
fun reactedBy(loggedIn: User, content: String): List<Note> {
|
||||||
return reactions[content]?.filter { it.author == loggedIn && it.event?.content() == content } ?: emptyList()
|
return reactions[content]?.filter { it.author == loggedIn } ?: emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reactedBy(loggedIn: User): List<String> {
|
fun reactedBy(loggedIn: User): List<String> {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class User(val pubkeyHex: String) {
|
|||||||
|
|
||||||
var latestContactList: ContactListEvent? = null
|
var latestContactList: ContactListEvent? = null
|
||||||
var latestBookmarkList: BookmarkListEvent? = null
|
var latestBookmarkList: BookmarkListEvent? = null
|
||||||
|
var latestAcceptedBadges: AddressableNote? = null
|
||||||
|
|
||||||
var notes = setOf<Note>()
|
var notes = setOf<Note>()
|
||||||
private set
|
private set
|
||||||
@@ -49,8 +50,6 @@ class User(val pubkeyHex: String) {
|
|||||||
var privateChatrooms = mapOf<User, Chatroom>()
|
var privateChatrooms = mapOf<User, Chatroom>()
|
||||||
private set
|
private set
|
||||||
|
|
||||||
var acceptedBadges: AddressableNote? = null
|
|
||||||
|
|
||||||
fun pubkey() = Hex.decode(pubkeyHex)
|
fun pubkey() = Hex.decode(pubkeyHex)
|
||||||
fun pubkeyNpub() = pubkey().toNpub()
|
fun pubkeyNpub() = pubkey().toNpub()
|
||||||
|
|
||||||
@@ -146,8 +145,8 @@ class User(val pubkeyHex: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun updateAcceptedBadges(note: AddressableNote) {
|
fun updateAcceptedBadges(note: AddressableNote) {
|
||||||
if (acceptedBadges?.idHex != note.idHex) {
|
if (latestAcceptedBadges?.idHex != note.idHex) {
|
||||||
acceptedBadges = note
|
latestAcceptedBadges = note
|
||||||
liveSet?.badges?.invalidateData()
|
liveSet?.badges?.invalidateData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,9 +51,9 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
|||||||
return TypedFilter(
|
return TypedFilter(
|
||||||
types = COMMON_FEED_TYPES,
|
types = COMMON_FEED_TYPES,
|
||||||
filter = JsonFilter(
|
filter = JsonFilter(
|
||||||
kinds = listOf(BadgeProfilesEvent.kind),
|
kinds = listOf(BadgeProfilesEvent.kind, EmojiPackSelectionEvent.kind),
|
||||||
authors = listOf(account.userProfile().pubkeyHex),
|
authors = listOf(account.userProfile().pubkeyHex),
|
||||||
limit = 1
|
limit = 10
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,14 +50,24 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
|
|||||||
|
|
||||||
return addressesToWatch.mapNotNull {
|
return addressesToWatch.mapNotNull {
|
||||||
it.address()?.let { aTag ->
|
it.address()?.let { aTag ->
|
||||||
TypedFilter(
|
if (aTag.kind < 25000 && aTag.dTag.isBlank()) {
|
||||||
types = COMMON_FEED_TYPES,
|
TypedFilter(
|
||||||
filter = JsonFilter(
|
types = COMMON_FEED_TYPES,
|
||||||
kinds = listOf(aTag.kind),
|
filter = JsonFilter(
|
||||||
tags = mapOf("d" to listOf(aTag.dTag)),
|
kinds = listOf(aTag.kind),
|
||||||
authors = listOf(aTag.pubKeyHex)
|
authors = listOf(aTag.pubKeyHex)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
} else {
|
||||||
|
TypedFilter(
|
||||||
|
types = COMMON_FEED_TYPES,
|
||||||
|
filter = JsonFilter(
|
||||||
|
kinds = listOf(aTag.kind),
|
||||||
|
tags = mapOf("d" to listOf(aTag.dTag)),
|
||||||
|
authors = listOf(aTag.pubKeyHex)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.vitorpamplona.amethyst.service
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
|
||||||
|
|
||||||
fun String.isUTF16Char(pos: Int): Boolean {
|
fun String.isUTF16Char(pos: Int): Boolean {
|
||||||
println("Test $pos ${Character.charCount(this.codePointAt(pos))}")
|
println("Test $pos ${Character.charCount(this.codePointAt(pos))}")
|
||||||
return Character.charCount(this.codePointAt(pos)) == 2
|
return Character.charCount(this.codePointAt(pos)) == 2
|
||||||
@@ -77,3 +79,23 @@ fun String.firstFullChar(): String {
|
|||||||
|
|
||||||
return substring(start, start + previousCharLength)
|
return substring(start, start + previousCharLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun String.firstFullCharOrEmoji(tags: ImmutableListOfLists<String>): String {
|
||||||
|
if (length <= 2) {
|
||||||
|
return firstFullChar()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this[0] == ':') {
|
||||||
|
// makes sure an emoji exists
|
||||||
|
val emojiParts = this.split(":", limit = 3)
|
||||||
|
if (emojiParts.size >= 2) {
|
||||||
|
val emojiName = emojiParts[1]
|
||||||
|
val emojiUrl = tags.lists.firstOrNull() { it.size > 1 && it[1] == emojiName }?.getOrNull(2)
|
||||||
|
if (emojiUrl != null) {
|
||||||
|
return ":$emojiName:$emojiUrl"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return firstFullChar()
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
|
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||||
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
|
import nostr.postr.Utils
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
class EmojiPackEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
tags: List<List<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey
|
||||||
|
) : GeneralListEvent(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val kind = 30030
|
||||||
|
|
||||||
|
fun create(
|
||||||
|
name: String = "",
|
||||||
|
privateKey: ByteArray,
|
||||||
|
createdAt: Long = TimeUtils.now()
|
||||||
|
): EmojiPackEvent {
|
||||||
|
val content = ""
|
||||||
|
val pubKey = Utils.pubkeyCreate(privateKey)
|
||||||
|
|
||||||
|
val tags = mutableListOf<List<String>>()
|
||||||
|
tags.add(listOf("d", name))
|
||||||
|
|
||||||
|
val id = generateId(pubKey.toHexKey(), createdAt, kind, tags, content)
|
||||||
|
val sig = Utils.sign(id, privateKey)
|
||||||
|
return EmojiPackEvent(id.toHexKey(), pubKey.toHexKey(), createdAt, tags, content, sig.toHexKey())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class EmojiUrl(val code: String, val url: String) {
|
||||||
|
fun encode(): String {
|
||||||
|
return ":$code:$url"
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun decode(encodedEmojiSetup: String): EmojiUrl? {
|
||||||
|
val emojiParts = encodedEmojiSetup.split(":", limit = 3)
|
||||||
|
return if (emojiParts.size > 2) {
|
||||||
|
EmojiUrl(emojiParts[1], emojiParts[2])
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
|
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||||
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
|
import nostr.postr.Utils
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
class EmojiPackSelectionEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
tags: List<List<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey
|
||||||
|
) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
|
||||||
|
|
||||||
|
override fun dTag() = ""
|
||||||
|
override fun address() = ATag(kind, pubKey, dTag(), null)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val kind = 10030
|
||||||
|
|
||||||
|
fun create(
|
||||||
|
listOfEmojiPacks: List<ATag>?,
|
||||||
|
privateKey: ByteArray,
|
||||||
|
createdAt: Long = TimeUtils.now()
|
||||||
|
): EmojiPackSelectionEvent {
|
||||||
|
val msg = ""
|
||||||
|
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||||
|
val tags = mutableListOf<List<String>>()
|
||||||
|
|
||||||
|
listOfEmojiPacks?.forEach {
|
||||||
|
tags.add(listOf("a", it.toTag()))
|
||||||
|
}
|
||||||
|
|
||||||
|
val id = generateId(pubKey, createdAt, kind, tags, msg)
|
||||||
|
val sig = Utils.sign(id, privateKey)
|
||||||
|
return EmojiPackSelectionEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,6 +49,8 @@ open class Event(
|
|||||||
|
|
||||||
fun taggedUrls() = tags.filter { it.size > 1 && it[0] == "r" }.map { it[1] }
|
fun taggedUrls() = tags.filter { it.size > 1 && it[0] == "r" }.map { it[1] }
|
||||||
|
|
||||||
|
override fun taggedEmojis() = tags.filter { it.size > 2 && it[0] == "emoji" }.map { EmojiUrl(it[1], it[2]) }
|
||||||
|
|
||||||
override fun isSensitive() = tags.any {
|
override fun isSensitive() = tags.any {
|
||||||
(it.size > 0 && it[0].equals("content-warning", true)) ||
|
(it.size > 0 && it[0].equals("content-warning", true)) ||
|
||||||
(it.size > 1 && it[0] == "t" && it[1].equals("nsfw", true)) ||
|
(it.size > 1 && it[0] == "t" && it[1].equals("nsfw", true)) ||
|
||||||
@@ -70,6 +72,8 @@ open class Event(
|
|||||||
|
|
||||||
override fun hashtags() = tags.filter { it.size > 1 && it[0] == "t" }.map { it[1] }
|
override fun hashtags() = tags.filter { it.size > 1 && it[0] == "t" }.map { it[1] }
|
||||||
|
|
||||||
|
override fun matchTag1With(text: String) = tags.any { it.size > 1 && it[1].contains(text, true) }
|
||||||
|
|
||||||
override fun isTaggedUser(idHex: String) = tags.any { it.size > 1 && it[0] == "p" && it[1] == idHex }
|
override fun isTaggedUser(idHex: String) = tags.any { it.size > 1 && it[0] == "p" && it[1] == idHex }
|
||||||
|
|
||||||
override fun isTaggedAddressableNote(idHex: String) = tags.any { it.size > 1 && it[0] == "a" && it[1] == idHex }
|
override fun isTaggedAddressableNote(idHex: String) = tags.any { it.size > 1 && it[0] == "a" && it[1] == idHex }
|
||||||
@@ -267,6 +271,8 @@ open class Event(
|
|||||||
CommunityPostApprovalEvent.kind -> CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig)
|
CommunityPostApprovalEvent.kind -> CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
ContactListEvent.kind -> ContactListEvent(id, pubKey, createdAt, tags, content, sig)
|
ContactListEvent.kind -> ContactListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
DeletionEvent.kind -> DeletionEvent(id, pubKey, createdAt, tags, content, sig)
|
DeletionEvent.kind -> DeletionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
EmojiPackEvent.kind -> EmojiPackEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
EmojiPackSelectionEvent.kind -> EmojiPackSelectionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
|
||||||
FileHeaderEvent.kind -> FileHeaderEvent(id, pubKey, createdAt, tags, content, sig)
|
FileHeaderEvent.kind -> FileHeaderEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
FileStorageEvent.kind -> FileStorageEvent(id, pubKey, createdAt, tags, content, sig)
|
FileStorageEvent.kind -> FileStorageEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
|||||||
@@ -45,4 +45,7 @@ interface EventInterface {
|
|||||||
fun zapAddress(): String?
|
fun zapAddress(): String?
|
||||||
fun isSensitive(): Boolean
|
fun isSensitive(): Boolean
|
||||||
fun zapraiserAmount(): Long?
|
fun zapraiserAmount(): Long?
|
||||||
|
|
||||||
|
fun taggedEmojis(): List<EmojiUrl>
|
||||||
|
fun matchTag1With(text: String): Boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,5 +42,24 @@ class ReactionEvent(
|
|||||||
val sig = Utils.sign(id, privateKey)
|
val sig = Utils.sign(id, privateKey)
|
||||||
return ReactionEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
|
return ReactionEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun create(emojiUrl: EmojiUrl, originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ReactionEvent {
|
||||||
|
val content = ":${emojiUrl.code}:"
|
||||||
|
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||||
|
|
||||||
|
var tags = listOf(
|
||||||
|
listOf("e", originalNote.id()),
|
||||||
|
listOf("p", originalNote.pubKey()),
|
||||||
|
listOf("emoji", emojiUrl.code, emojiUrl.url)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (originalNote is AddressableEvent) {
|
||||||
|
tags = tags + listOf(listOf("a", originalNote.address().toTag()))
|
||||||
|
}
|
||||||
|
|
||||||
|
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||||
|
val sig = Utils.sign(id, privateKey)
|
||||||
|
return ReactionEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -399,7 +399,7 @@ fun CreateTextWithEmoji(
|
|||||||
)
|
)
|
||||||
).toSpanStyle()
|
).toSpanStyle()
|
||||||
|
|
||||||
InLineIconRenderer(emojiList, style, maxLines, overflow, modifier)
|
InLineIconRenderer(emojiList, style, fontSize, maxLines, overflow, modifier)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -458,7 +458,7 @@ fun CreateTextWithEmoji(
|
|||||||
).toSpanStyle()
|
).toSpanStyle()
|
||||||
}
|
}
|
||||||
|
|
||||||
InLineIconRenderer(emojiList, style, maxLines, overflow, modifier)
|
InLineIconRenderer(emojiList, style, fontSize, maxLines, overflow, modifier)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -551,7 +551,7 @@ fun CreateClickableTextWithEmoji(
|
|||||||
InLineIconRenderer(
|
InLineIconRenderer(
|
||||||
emojiLists!!.part2,
|
emojiLists!!.part2,
|
||||||
LocalTextStyle.current.copy(color = overrideColor ?: MaterialTheme.colors.onBackground, fontWeight = fontWeight).toSpanStyle(),
|
LocalTextStyle.current.copy(color = overrideColor ?: MaterialTheme.colors.onBackground, fontWeight = fontWeight).toSpanStyle(),
|
||||||
maxLines
|
maxLines = maxLines
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -646,6 +646,7 @@ fun ClickableInLineIconRenderer(
|
|||||||
fun InLineIconRenderer(
|
fun InLineIconRenderer(
|
||||||
wordsInOrder: ImmutableList<Renderable>,
|
wordsInOrder: ImmutableList<Renderable>,
|
||||||
style: SpanStyle,
|
style: SpanStyle,
|
||||||
|
fontSize: TextUnit = TextUnit.Unspecified,
|
||||||
maxLines: Int = Int.MAX_VALUE,
|
maxLines: Int = Int.MAX_VALUE,
|
||||||
overflow: TextOverflow = TextOverflow.Clip,
|
overflow: TextOverflow = TextOverflow.Clip,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
@@ -692,6 +693,7 @@ fun InLineIconRenderer(
|
|||||||
Text(
|
Text(
|
||||||
text = annotatedText,
|
text = annotatedText,
|
||||||
inlineContent = inlineContent,
|
inlineContent = inlineContent,
|
||||||
|
fontSize = fontSize,
|
||||||
maxLines = maxLines,
|
maxLines = maxLines,
|
||||||
overflow = overflow,
|
overflow = overflow,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.vitorpamplona.amethyst.ui.note
|
|||||||
|
|
||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.Crossfade
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import androidx.compose.ui.graphics.compositeOver
|
|||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
@@ -50,11 +51,11 @@ import com.vitorpamplona.amethyst.R
|
|||||||
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.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.firstFullChar
|
|
||||||
import com.vitorpamplona.amethyst.service.model.LnZapEvent
|
import com.vitorpamplona.amethyst.service.model.LnZapEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent
|
import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent
|
||||||
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
|
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
|
||||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
import com.vitorpamplona.amethyst.ui.components.ImageUrlType
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
|
||||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
||||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||||
import com.vitorpamplona.amethyst.ui.screen.CombinedZap
|
import com.vitorpamplona.amethyst.ui.screen.CombinedZap
|
||||||
@@ -77,6 +78,7 @@ import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.overPictureBackground
|
import com.vitorpamplona.amethyst.ui.theme.overPictureBackground
|
||||||
import com.vitorpamplona.amethyst.ui.theme.profile35dpModifier
|
import com.vitorpamplona.amethyst.ui.theme.profile35dpModifier
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.time.ExperimentalTime
|
import kotlin.time.ExperimentalTime
|
||||||
@@ -207,18 +209,6 @@ fun RenderLikeGallery(
|
|||||||
nav: (String) -> Unit,
|
nav: (String) -> Unit,
|
||||||
accountViewModel: AccountViewModel
|
accountViewModel: AccountViewModel
|
||||||
) {
|
) {
|
||||||
val tags = remember(likeEvents) {
|
|
||||||
if (reactionType.startsWith(":")) {
|
|
||||||
ImmutableListOfLists<String>(
|
|
||||||
likeEvents.mapNotNull {
|
|
||||||
it.event?.tags()?.filter { it.size > 2 && it[0] == "emoji" }
|
|
||||||
}.flatten()
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
ImmutableListOfLists<String>()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (likeEvents.isNotEmpty()) {
|
if (likeEvents.isNotEmpty()) {
|
||||||
Row(Modifier.fillMaxWidth()) {
|
Row(Modifier.fillMaxWidth()) {
|
||||||
Box(
|
Box(
|
||||||
@@ -228,19 +218,32 @@ fun RenderLikeGallery(
|
|||||||
Modifier.align(Alignment.TopEnd)
|
Modifier.align(Alignment.TopEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
when (val shortReaction = reactionType.firstFullChar()) {
|
if (reactionType.startsWith(":")) {
|
||||||
"+" -> Icon(
|
val noStartColon = reactionType.removePrefix(":")
|
||||||
painter = painterResource(R.drawable.ic_liked),
|
val url = noStartColon.substringAfter(":")
|
||||||
null,
|
|
||||||
modifier = modifier.size(Size18dp),
|
val renderable = listOf(
|
||||||
tint = Color.Unspecified
|
ImageUrlType(url)
|
||||||
)
|
).toImmutableList()
|
||||||
"-" -> Text(text = "\uD83D\uDC4E", modifier = modifier)
|
|
||||||
else -> CreateTextWithEmoji(
|
InLineIconRenderer(
|
||||||
text = shortReaction,
|
renderable,
|
||||||
modifier = modifier,
|
style = SpanStyle(color = Color.White),
|
||||||
tags = tags
|
maxLines = 1,
|
||||||
|
modifier = modifier
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
when (val shortReaction = reactionType) {
|
||||||
|
"+" -> Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_liked),
|
||||||
|
null,
|
||||||
|
modifier = modifier.size(Size18dp),
|
||||||
|
tint = Color.Unspecified
|
||||||
|
)
|
||||||
|
|
||||||
|
"-" -> Text(text = "\uD83D\uDC4E", modifier = modifier)
|
||||||
|
else -> Text(text = shortReaction, modifier = modifier)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.Box
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@@ -25,6 +26,8 @@ import androidx.compose.foundation.layout.width
|
|||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.CutCornerShape
|
import androidx.compose.foundation.shape.CutCornerShape
|
||||||
import androidx.compose.foundation.text.ClickableText
|
import androidx.compose.foundation.text.ClickableText
|
||||||
|
import androidx.compose.material.Button
|
||||||
|
import androidx.compose.material.ButtonDefaults
|
||||||
import androidx.compose.material.Divider
|
import androidx.compose.material.Divider
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.IconButton
|
import androidx.compose.material.IconButton
|
||||||
@@ -96,6 +99,9 @@ import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
|||||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.CommunityDefinitionEvent
|
import com.vitorpamplona.amethyst.service.model.CommunityDefinitionEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.CommunityPostApprovalEvent
|
import com.vitorpamplona.amethyst.service.model.CommunityPostApprovalEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.EmojiPackEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.EmojiPackSelectionEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.EmojiUrl
|
||||||
import com.vitorpamplona.amethyst.service.model.FileHeaderEvent
|
import com.vitorpamplona.amethyst.service.model.FileHeaderEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.FileStorageHeaderEvent
|
import com.vitorpamplona.amethyst.service.model.FileStorageHeaderEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
|
import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
|
||||||
@@ -145,6 +151,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.LeaveCommunityButton
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LiveFlag
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LiveFlag
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag
|
||||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||||
@@ -158,6 +165,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size24Modifier
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size30Modifier
|
import com.vitorpamplona.amethyst.ui.theme.Size30Modifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size30dp
|
import com.vitorpamplona.amethyst.ui.theme.Size30dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size35Modifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||||
@@ -1035,6 +1043,10 @@ private fun RenderNoteRow(
|
|||||||
RenderPinListEvent(baseNote, backgroundColor, accountViewModel, nav)
|
RenderPinListEvent(baseNote, backgroundColor, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is EmojiPackEvent -> {
|
||||||
|
RenderEmojiPack(baseNote, true, backgroundColor, accountViewModel)
|
||||||
|
}
|
||||||
|
|
||||||
is LiveActivitiesEvent -> {
|
is LiveActivitiesEvent -> {
|
||||||
RenderLiveActivityEvent(baseNote, accountViewModel, nav)
|
RenderLiveActivityEvent(baseNote, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
@@ -1884,18 +1896,188 @@ fun LoadAddressableNote(aTag: ATag, content: @Composable (AddressableNote?) -> U
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RenderPinListEvent(
|
public fun RenderEmojiPack(
|
||||||
baseNote: Note,
|
baseNote: Note,
|
||||||
|
actionable: Boolean,
|
||||||
backgroundColor: MutableState<Color>,
|
backgroundColor: MutableState<Color>,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (String) -> Unit
|
onClick: ((EmojiUrl) -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
PinListHeader(baseNote, backgroundColor, accountViewModel, nav)
|
val noteEvent by baseNote.live().metadata.map {
|
||||||
|
it.note.event
|
||||||
|
}.distinctUntilChanged().observeAsState(baseNote.event)
|
||||||
|
|
||||||
|
if (noteEvent == null || noteEvent !is EmojiPackEvent) return
|
||||||
|
|
||||||
|
(noteEvent as? EmojiPackEvent)?.let {
|
||||||
|
RenderEmojiPack(
|
||||||
|
noteEvent = it,
|
||||||
|
baseNote = baseNote,
|
||||||
|
actionable = actionable,
|
||||||
|
backgroundColor = backgroundColor,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
onClick = onClick
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun PinListHeader(
|
public fun RenderEmojiPack(
|
||||||
|
noteEvent: EmojiPackEvent,
|
||||||
|
baseNote: Note,
|
||||||
|
actionable: Boolean,
|
||||||
|
backgroundColor: MutableState<Color>,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
onClick: ((EmojiUrl) -> Unit)? = null
|
||||||
|
) {
|
||||||
|
var expanded by remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
val allEmojis = remember(noteEvent) {
|
||||||
|
noteEvent.taggedEmojis()
|
||||||
|
}
|
||||||
|
|
||||||
|
val emojisToShow = if (expanded) {
|
||||||
|
allEmojis
|
||||||
|
} else {
|
||||||
|
allEmojis.take(60)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(verticalAlignment = CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = remember(noteEvent) { "#${noteEvent.dTag()}" },
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1F)
|
||||||
|
.padding(5.dp),
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
|
||||||
|
if (actionable) {
|
||||||
|
EmojiListOptions(accountViewModel, baseNote)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box {
|
||||||
|
FlowRow(modifier = Modifier.padding(top = 5.dp)) {
|
||||||
|
emojisToShow.forEach { emoji ->
|
||||||
|
if (onClick != null) {
|
||||||
|
IconButton(onClick = { onClick(emoji) }, modifier = Size35Modifier) {
|
||||||
|
AsyncImage(
|
||||||
|
model = emoji.url,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Size35Modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Size35Modifier,
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = emoji.url,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Size35Modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allEmojis.size > 60 && !expanded) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(getGradient(backgroundColor))
|
||||||
|
) {
|
||||||
|
ShowMoreButton {
|
||||||
|
expanded = !expanded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EmojiListOptions(
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
emojiPackNote: Note
|
||||||
|
) {
|
||||||
|
LoadAddressableNote(
|
||||||
|
aTag = ATag(
|
||||||
|
EmojiPackSelectionEvent.kind,
|
||||||
|
accountViewModel.userProfile().pubkeyHex,
|
||||||
|
"",
|
||||||
|
null
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
it?.let { usersEmojiList ->
|
||||||
|
val hasAddedThis by usersEmojiList.live().metadata.map {
|
||||||
|
usersEmojiList.event?.isTaggedAddressableNote(emojiPackNote.idHex)
|
||||||
|
}.distinctUntilChanged().observeAsState()
|
||||||
|
|
||||||
|
Crossfade(targetState = hasAddedThis) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
if (it != true) {
|
||||||
|
AddButton() {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
accountViewModel.addEmojiPack(usersEmojiList, emojiPackNote)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
RemoveButton {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
accountViewModel.removeEmojiPack(usersEmojiList, emojiPackNote)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RemoveButton(onClick: () -> Unit) {
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.padding(horizontal = 3.dp),
|
||||||
|
onClick = onClick,
|
||||||
|
shape = ButtonBorder,
|
||||||
|
colors = ButtonDefaults
|
||||||
|
.buttonColors(
|
||||||
|
backgroundColor = MaterialTheme.colors.primary
|
||||||
|
),
|
||||||
|
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.remove), color = Color.White)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AddButton(text: Int = R.string.add, onClick: () -> Unit) {
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.padding(start = 3.dp),
|
||||||
|
onClick = onClick,
|
||||||
|
shape = ButtonBorder,
|
||||||
|
colors = ButtonDefaults
|
||||||
|
.buttonColors(
|
||||||
|
backgroundColor = MaterialTheme.colors.primary
|
||||||
|
),
|
||||||
|
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(text), color = Color.White, textAlign = TextAlign.Center)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
fun RenderPinListEvent(
|
||||||
baseNote: Note,
|
baseNote: Note,
|
||||||
backgroundColor: MutableState<Color>,
|
backgroundColor: MutableState<Color>,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
@@ -81,8 +82,10 @@ import coil.request.CachePolicy
|
|||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
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.firstFullChar
|
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.ImageUrlType
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.TextType
|
||||||
import com.vitorpamplona.amethyst.ui.screen.CombinedZap
|
import com.vitorpamplona.amethyst.ui.screen.CombinedZap
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||||
@@ -808,7 +811,8 @@ private fun WatchReactionTypeForNote(baseNote: Note, accountViewModel: AccountVi
|
|||||||
|
|
||||||
LaunchedEffect(key1 = reactionsState) {
|
LaunchedEffect(key1 = reactionsState) {
|
||||||
launch(Dispatchers.Default) {
|
launch(Dispatchers.Default) {
|
||||||
onNewReactionType(reactionsState?.note?.getReactionBy(accountViewModel.userProfile())?.firstFullChar())
|
val reactionNote = reactionsState?.note?.getReactionBy(accountViewModel.userProfile())
|
||||||
|
onNewReactionType(reactionNote)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -840,18 +844,34 @@ private fun RenderReactionType(
|
|||||||
Modifier.size(iconSize)
|
Modifier.size(iconSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
when (reactionType) {
|
if (reactionType.startsWith(":")) {
|
||||||
"+" -> {
|
val noStartColon = reactionType.removePrefix(":")
|
||||||
Icon(
|
val url = noStartColon.substringAfter(":")
|
||||||
painter = painterResource(R.drawable.ic_liked),
|
|
||||||
null,
|
|
||||||
modifier = iconModifier,
|
|
||||||
tint = Color.Unspecified
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
"-" -> Text(text = "\uD83D\uDC4E", fontSize = iconFontSize)
|
val renderable = listOf(
|
||||||
else -> Text(text = reactionType, fontSize = iconFontSize)
|
ImageUrlType(url)
|
||||||
|
).toImmutableList()
|
||||||
|
|
||||||
|
InLineIconRenderer(
|
||||||
|
renderable,
|
||||||
|
style = SpanStyle(color = Color.White),
|
||||||
|
fontSize = iconFontSize,
|
||||||
|
maxLines = 1
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
when (reactionType) {
|
||||||
|
"+" -> {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_liked),
|
||||||
|
null,
|
||||||
|
modifier = iconModifier,
|
||||||
|
tint = Color.Unspecified
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
"-" -> Text(text = "\uD83D\uDC4E", fontSize = iconFontSize)
|
||||||
|
else -> Text(text = reactionType, fontSize = iconFontSize)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1269,7 +1289,7 @@ private fun BoostTypeChoicePopup(baseNote: Note, accountViewModel: AccountViewMo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ReactionChoicePopup(
|
fun ReactionChoicePopup(
|
||||||
baseNote: Note,
|
baseNote: Note,
|
||||||
@@ -1279,7 +1299,6 @@ fun ReactionChoicePopup(
|
|||||||
) {
|
) {
|
||||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||||
val account = accountState?.account ?: return
|
val account = accountState?.account ?: return
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
val toRemove = remember {
|
val toRemove = remember {
|
||||||
baseNote.reactedBy(account.userProfile()).toSet()
|
baseNote.reactedBy(account.userProfile()).toSet()
|
||||||
@@ -1292,67 +1311,117 @@ fun ReactionChoicePopup(
|
|||||||
) {
|
) {
|
||||||
FlowRow(horizontalArrangement = Arrangement.Center) {
|
FlowRow(horizontalArrangement = Arrangement.Center) {
|
||||||
account.reactionChoices.forEach { reactionType ->
|
account.reactionChoices.forEach { reactionType ->
|
||||||
Button(
|
ActionableReactionButton(
|
||||||
modifier = Modifier.padding(horizontal = 3.dp),
|
baseNote,
|
||||||
onClick = {
|
reactionType,
|
||||||
scope.launch(Dispatchers.IO) {
|
accountViewModel,
|
||||||
accountViewModel.reactToOrDelete(
|
onDismiss,
|
||||||
baseNote,
|
onChangeAmount,
|
||||||
reactionType
|
toRemove
|
||||||
)
|
)
|
||||||
onDismiss()
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
shape = ButtonBorder,
|
}
|
||||||
colors = ButtonDefaults
|
|
||||||
.buttonColors(
|
|
||||||
backgroundColor = MaterialTheme.colors.primary
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
val thisModifier = remember(reactionType) {
|
|
||||||
Modifier.combinedClickable(
|
|
||||||
onClick = {
|
|
||||||
scope.launch(Dispatchers.IO) {
|
|
||||||
accountViewModel.reactToOrDelete(
|
|
||||||
baseNote,
|
|
||||||
reactionType
|
|
||||||
)
|
|
||||||
onDismiss()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onLongClick = {
|
|
||||||
onChangeAmount()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val removeSymbol = remember(reactionType) {
|
@Composable
|
||||||
if (reactionType in toRemove) {
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
" ✖"
|
private fun ActionableReactionButton(
|
||||||
} else {
|
baseNote: Note,
|
||||||
""
|
reactionType: String,
|
||||||
}
|
accountViewModel: AccountViewModel,
|
||||||
}
|
onDismiss: () -> Unit,
|
||||||
|
onChangeAmount: () -> Unit,
|
||||||
|
toRemove: Set<String>
|
||||||
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
when (reactionType) {
|
Button(
|
||||||
"+" -> {
|
modifier = Modifier.padding(horizontal = 3.dp),
|
||||||
Icon(
|
onClick = {
|
||||||
painter = painterResource(R.drawable.ic_liked),
|
scope.launch(Dispatchers.IO) {
|
||||||
null,
|
accountViewModel.reactToOrDelete(
|
||||||
modifier = remember { thisModifier.size(16.dp) },
|
baseNote,
|
||||||
tint = Color.White
|
reactionType
|
||||||
)
|
)
|
||||||
Text(text = removeSymbol, color = Color.White, textAlign = TextAlign.Center, modifier = thisModifier)
|
onDismiss()
|
||||||
}
|
}
|
||||||
"-" -> Text(text = "\uD83D\uDC4E$removeSymbol", color = Color.White, textAlign = TextAlign.Center, modifier = thisModifier)
|
},
|
||||||
else -> Text(
|
shape = ButtonBorder,
|
||||||
"$reactionType$removeSymbol",
|
colors = ButtonDefaults
|
||||||
color = Color.White,
|
.buttonColors(
|
||||||
textAlign = TextAlign.Center,
|
backgroundColor = MaterialTheme.colors.primary
|
||||||
modifier = thisModifier
|
)
|
||||||
|
) {
|
||||||
|
val thisModifier = remember(reactionType) {
|
||||||
|
Modifier.combinedClickable(
|
||||||
|
onClick = {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
accountViewModel.reactToOrDelete(
|
||||||
|
baseNote,
|
||||||
|
reactionType
|
||||||
)
|
)
|
||||||
|
onDismiss()
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onLongClick = {
|
||||||
|
onChangeAmount()
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val removeSymbol = remember(reactionType) {
|
||||||
|
if (reactionType in toRemove) {
|
||||||
|
" ✖"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reactionType.startsWith(":")) {
|
||||||
|
val noStartColon = reactionType.removePrefix(":")
|
||||||
|
val url = noStartColon.substringAfter(":")
|
||||||
|
|
||||||
|
val renderable = listOf(
|
||||||
|
ImageUrlType(url),
|
||||||
|
TextType(removeSymbol)
|
||||||
|
).toImmutableList()
|
||||||
|
|
||||||
|
InLineIconRenderer(
|
||||||
|
renderable,
|
||||||
|
style = SpanStyle(color = Color.White),
|
||||||
|
maxLines = 1
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
when (reactionType) {
|
||||||
|
"+" -> {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_liked),
|
||||||
|
null,
|
||||||
|
modifier = remember { thisModifier.size(16.dp) },
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = removeSymbol,
|
||||||
|
color = Color.White,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = thisModifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
"-" -> Text(
|
||||||
|
text = "\uD83D\uDC4E$removeSymbol",
|
||||||
|
color = Color.White,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = thisModifier
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> Text(
|
||||||
|
"$reactionType$removeSymbol",
|
||||||
|
color = Color.White,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = thisModifier
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
@@ -24,7 +27,9 @@ import androidx.compose.material.Surface
|
|||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
@@ -33,6 +38,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
@@ -42,15 +48,25 @@ import androidx.compose.ui.window.Dialog
|
|||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.distinctUntilChanged
|
||||||
|
import androidx.lifecycle.map
|
||||||
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.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
import com.vitorpamplona.amethyst.service.firstFullChar
|
import com.vitorpamplona.amethyst.service.firstFullChar
|
||||||
|
import com.vitorpamplona.amethyst.service.model.ATag
|
||||||
|
import com.vitorpamplona.amethyst.service.model.EmojiPackSelectionEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.EmojiUrl
|
||||||
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||||
import com.vitorpamplona.amethyst.ui.actions.SaveButton
|
import com.vitorpamplona.amethyst.ui.actions.SaveButton
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.ImageUrlType
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.TextType
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
class UpdateReactionTypeViewModel(val account: Account) : ViewModel() {
|
class UpdateReactionTypeViewModel(val account: Account) : ViewModel() {
|
||||||
var nextChoice by mutableStateOf(TextFieldValue(""))
|
var nextChoice by mutableStateOf(TextFieldValue(""))
|
||||||
@@ -71,6 +87,10 @@ class UpdateReactionTypeViewModel(val account: Account) : ViewModel() {
|
|||||||
nextChoice = TextFieldValue("")
|
nextChoice = TextFieldValue("")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addChoice(customEmoji: EmojiUrl) {
|
||||||
|
reactionSet = reactionSet + (customEmoji.encode())
|
||||||
|
}
|
||||||
|
|
||||||
fun removeChoice(reaction: String) {
|
fun removeChoice(reaction: String) {
|
||||||
reactionSet = reactionSet - reaction
|
reactionSet = reactionSet - reaction
|
||||||
}
|
}
|
||||||
@@ -116,10 +136,13 @@ fun UpdateReactionTypeDialog(onClose: () -> Unit, nip47uri: String? = null, acco
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth()
|
||||||
.fillMaxWidth()
|
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(10.dp).imePadding()) {
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(10.dp)
|
||||||
|
.imePadding()
|
||||||
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
@@ -155,30 +178,7 @@ fun UpdateReactionTypeDialog(onClose: () -> Unit, nip47uri: String? = null, acco
|
|||||||
horizontalArrangement = Arrangement.Center
|
horizontalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
postViewModel.reactionSet.forEach { reactionType ->
|
postViewModel.reactionSet.forEach { reactionType ->
|
||||||
Button(
|
RenderReactionOption(reactionType, postViewModel)
|
||||||
modifier = Modifier.padding(horizontal = 3.dp),
|
|
||||||
shape = ButtonBorder,
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
backgroundColor = MaterialTheme.colors.primary
|
|
||||||
),
|
|
||||||
onClick = {
|
|
||||||
postViewModel.removeChoice(reactionType)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
when (reactionType) {
|
|
||||||
"+" -> {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(R.drawable.ic_liked),
|
|
||||||
null,
|
|
||||||
modifier = remember { Modifier.size(16.dp) },
|
|
||||||
tint = Color.White
|
|
||||||
)
|
|
||||||
Text(text = " ✖", color = Color.White, textAlign = TextAlign.Center)
|
|
||||||
}
|
|
||||||
"-" -> Text(text = "\uD83D\uDC4E ✖", color = Color.White, textAlign = TextAlign.Center)
|
|
||||||
else -> Text(text = "$reactionType ✖", color = Color.White, textAlign = TextAlign.Center)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,7 +226,130 @@ fun UpdateReactionTypeDialog(onClose: () -> Unit, nip47uri: String? = null, acco
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EmojiSelector(accountViewModel) {
|
||||||
|
postViewModel.addChoice(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RenderReactionOption(
|
||||||
|
reactionType: String,
|
||||||
|
postViewModel: UpdateReactionTypeViewModel
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.padding(horizontal = 3.dp),
|
||||||
|
shape = ButtonBorder,
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
backgroundColor = MaterialTheme.colors.primary
|
||||||
|
),
|
||||||
|
onClick = {
|
||||||
|
postViewModel.removeChoice(reactionType)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
if (reactionType.startsWith(":")) {
|
||||||
|
val noStartColon = reactionType.removePrefix(":")
|
||||||
|
val url = noStartColon.substringAfter(":")
|
||||||
|
|
||||||
|
val renderable = listOf(
|
||||||
|
ImageUrlType(url),
|
||||||
|
TextType(" ✖")
|
||||||
|
).toImmutableList()
|
||||||
|
|
||||||
|
InLineIconRenderer(
|
||||||
|
renderable,
|
||||||
|
style = SpanStyle(color = Color.White),
|
||||||
|
maxLines = 1
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
when (reactionType) {
|
||||||
|
"+" -> {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_liked),
|
||||||
|
null,
|
||||||
|
modifier = remember { Modifier.size(16.dp) },
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = " ✖",
|
||||||
|
color = Color.White,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
"-" -> Text(
|
||||||
|
text = "\uD83D\uDC4E ✖",
|
||||||
|
color = Color.White,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> Text(
|
||||||
|
text = "$reactionType ✖",
|
||||||
|
color = Color.White,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EmojiSelector(accountViewModel: AccountViewModel, onClick: ((EmojiUrl) -> Unit)? = null) {
|
||||||
|
LoadAddressableNote(
|
||||||
|
aTag = ATag(
|
||||||
|
EmojiPackSelectionEvent.kind,
|
||||||
|
accountViewModel.userProfile().pubkeyHex,
|
||||||
|
"",
|
||||||
|
null
|
||||||
|
)
|
||||||
|
) { emptyNote ->
|
||||||
|
emptyNote?.let { usersEmojiList ->
|
||||||
|
val collections by usersEmojiList.live().metadata.map {
|
||||||
|
(it.note.event as? EmojiPackSelectionEvent)?.taggedAddresses()
|
||||||
|
}.distinctUntilChanged().observeAsState((usersEmojiList.event as? EmojiPackSelectionEvent)?.taggedAddresses())
|
||||||
|
|
||||||
|
collections?.let {
|
||||||
|
EmojiCollectionGallery(it, accountViewModel, onClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EmojiCollectionGallery(emojiCollections: List<ATag>, accountViewModel: AccountViewModel, onClick: ((EmojiUrl) -> Unit)? = null) {
|
||||||
|
val color = MaterialTheme.colors.background
|
||||||
|
val bgColor = remember { mutableStateOf(color) }
|
||||||
|
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
state = listState
|
||||||
|
) {
|
||||||
|
itemsIndexed(emojiCollections, key = { _, item -> item.toTag() }) { _, item ->
|
||||||
|
LoadAddressableNote(aTag = item) {
|
||||||
|
it?.let {
|
||||||
|
WatchAndRenderNote(it, bgColor, accountViewModel, onClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun WatchAndRenderNote(
|
||||||
|
it: AddressableNote,
|
||||||
|
bgColor: MutableState<Color>,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
onClick: ((EmojiUrl) -> Unit)?
|
||||||
|
) {
|
||||||
|
RenderEmojiPack(
|
||||||
|
baseNote = it,
|
||||||
|
actionable = false,
|
||||||
|
backgroundColor = bgColor,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
onClick = onClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import androidx.compose.runtime.MutableState
|
|||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableMap
|
import kotlinx.collections.immutable.toImmutableMap
|
||||||
@@ -61,7 +63,9 @@ class MultiSetCard(
|
|||||||
boostEvents.minOfOrNull { it.createdAt() ?: Long.MAX_VALUE } ?: Long.MAX_VALUE
|
boostEvents.minOfOrNull { it.createdAt() ?: Long.MAX_VALUE } ?: Long.MAX_VALUE
|
||||||
)
|
)
|
||||||
|
|
||||||
val likeEventsByType = likeEvents.groupBy { it.event?.content() ?: "+" }.mapValues {
|
val likeEventsByType = likeEvents.groupBy {
|
||||||
|
it.event?.content()?.firstFullCharOrEmoji(ImmutableListOfLists(it.event?.tags() ?: emptyList())) ?: "+"
|
||||||
|
}.mapValues {
|
||||||
it.value.toImmutableList()
|
it.value.toImmutableList()
|
||||||
}.toImmutableMap()
|
}.toImmutableMap()
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import androidx.compose.material.Icon
|
|||||||
import androidx.compose.material.IconButton
|
import androidx.compose.material.IconButton
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.ProvideTextStyle
|
import androidx.compose.material.ProvideTextStyle
|
||||||
import androidx.compose.material.SnackbarDefaults.backgroundColor
|
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
@@ -62,6 +61,7 @@ import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
|||||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.CommunityDefinitionEvent
|
import com.vitorpamplona.amethyst.service.model.CommunityDefinitionEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.CommunityPostApprovalEvent
|
import com.vitorpamplona.amethyst.service.model.CommunityPostApprovalEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.EmojiPackEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.FileHeaderEvent
|
import com.vitorpamplona.amethyst.service.model.FileHeaderEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.FileStorageHeaderEvent
|
import com.vitorpamplona.amethyst.service.model.FileStorageHeaderEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
|
import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
|
||||||
@@ -417,12 +417,19 @@ fun NoteMaster(
|
|||||||
nav
|
nav
|
||||||
)
|
)
|
||||||
} else if (noteEvent is PinListEvent) {
|
} else if (noteEvent is PinListEvent) {
|
||||||
PinListHeader(
|
RenderPinListEvent(
|
||||||
baseNote,
|
baseNote,
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
accountViewModel,
|
accountViewModel,
|
||||||
nav
|
nav
|
||||||
)
|
)
|
||||||
|
} else if (noteEvent is EmojiPackEvent) {
|
||||||
|
RenderEmojiPack(
|
||||||
|
baseNote,
|
||||||
|
true,
|
||||||
|
backgroundColor,
|
||||||
|
accountViewModel
|
||||||
|
)
|
||||||
} else if (noteEvent is RelaySetEvent) {
|
} else if (noteEvent is RelaySetEvent) {
|
||||||
DisplayRelaySet(
|
DisplayRelaySet(
|
||||||
baseNote,
|
baseNote,
|
||||||
|
|||||||
@@ -165,6 +165,14 @@ class AccountViewModel(val account: Account) : ViewModel() {
|
|||||||
account.boost(note)
|
account.boost(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun removeEmojiPack(usersEmojiList: Note, emojiList: Note) {
|
||||||
|
account.removeEmojiPack(usersEmojiList, emojiList)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addEmojiPack(usersEmojiList: Note, emojiList: Note) {
|
||||||
|
account.addEmojiPack(usersEmojiList, emojiList)
|
||||||
|
}
|
||||||
|
|
||||||
fun addPrivateBookmark(note: Note) {
|
fun addPrivateBookmark(note: Note) {
|
||||||
account.addPrivateBookmark(note)
|
account.addPrivateBookmark(note)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1118,7 +1118,7 @@ private fun DisplayBadges(
|
|||||||
val userBadgeState by baseUser.live().badges.observeAsState()
|
val userBadgeState by baseUser.live().badges.observeAsState()
|
||||||
val badgeList by remember(userBadgeState) {
|
val badgeList by remember(userBadgeState) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
val list = (userBadgeState?.user?.acceptedBadges?.event as? BadgeProfilesEvent)?.badgeAwardEvents()
|
val list = (userBadgeState?.user?.latestAcceptedBadges?.event as? BadgeProfilesEvent)?.badgeAwardEvents()
|
||||||
if (list.isNullOrEmpty()) {
|
if (list.isNullOrEmpty()) {
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user