mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-04-09 20:39:24 +02:00
- Support for Emoji Packs
- Support for Personal Emoji Lists - Support for Custom emoji Reactions
This commit is contained in:
parent
3ee743ce1a
commit
f090bc82ae
@ -176,6 +176,19 @@ class Account(
|
||||
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 {
|
||||
val event = ReactionEvent.create(reaction, it, loggedIn.privKey!!)
|
||||
Client.send(event)
|
||||
@ -674,6 +687,51 @@ class Account(
|
||||
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) {
|
||||
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) {
|
||||
val version = getOrCreateNote(event.id)
|
||||
val note = getOrCreateAddressableNote(event.address())
|
||||
@ -1137,8 +1175,7 @@ object LocalCache {
|
||||
it.idNote().startsWith(text, true)
|
||||
} + addressables.values.filter {
|
||||
(it.event as? LongTextNoteEvent)?.content?.contains(text, true) ?: false ||
|
||||
(it.event as? LongTextNoteEvent)?.title()?.contains(text, true) ?: false ||
|
||||
(it.event as? LongTextNoteEvent)?.summary()?.contains(text, true) ?: false ||
|
||||
it.event?.matchTag1With(text) ?: false ||
|
||||
it.idHex.startsWith(text, true)
|
||||
}
|
||||
}
|
||||
@ -1278,6 +1315,8 @@ object LocalCache {
|
||||
}
|
||||
is ContactListEvent -> consume(event)
|
||||
is DeletionEvent -> consume(event)
|
||||
is EmojiPackEvent -> consume(event)
|
||||
is EmojiPackSelectionEvent -> consume(event)
|
||||
|
||||
is FileHeaderEvent -> consume(event, relay)
|
||||
is FileStorageEvent -> consume(event, relay)
|
||||
|
@ -5,11 +5,13 @@ import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji
|
||||
import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil
|
||||
import com.vitorpamplona.amethyst.service.model.*
|
||||
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSETime
|
||||
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.components.BundledUpdate
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
@ -152,7 +154,8 @@ open class Note(val idHex: String) {
|
||||
liveSet?.boosts?.invalidateData()
|
||||
}
|
||||
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) {
|
||||
reactions[reaction]?.let {
|
||||
@ -233,7 +236,8 @@ open class Note(val idHex: String) {
|
||||
}
|
||||
|
||||
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) {
|
||||
reactions = reactions + Pair(reaction, setOf(note))
|
||||
@ -462,7 +466,7 @@ open class Note(val idHex: String) {
|
||||
}
|
||||
|
||||
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> {
|
||||
|
@ -31,6 +31,7 @@ class User(val pubkeyHex: String) {
|
||||
|
||||
var latestContactList: ContactListEvent? = null
|
||||
var latestBookmarkList: BookmarkListEvent? = null
|
||||
var latestAcceptedBadges: AddressableNote? = null
|
||||
|
||||
var notes = setOf<Note>()
|
||||
private set
|
||||
@ -49,8 +50,6 @@ class User(val pubkeyHex: String) {
|
||||
var privateChatrooms = mapOf<User, Chatroom>()
|
||||
private set
|
||||
|
||||
var acceptedBadges: AddressableNote? = null
|
||||
|
||||
fun pubkey() = Hex.decode(pubkeyHex)
|
||||
fun pubkeyNpub() = pubkey().toNpub()
|
||||
|
||||
@ -146,8 +145,8 @@ class User(val pubkeyHex: String) {
|
||||
}
|
||||
|
||||
fun updateAcceptedBadges(note: AddressableNote) {
|
||||
if (acceptedBadges?.idHex != note.idHex) {
|
||||
acceptedBadges = note
|
||||
if (latestAcceptedBadges?.idHex != note.idHex) {
|
||||
latestAcceptedBadges = note
|
||||
liveSet?.badges?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
@ -51,9 +51,9 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
return TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter = JsonFilter(
|
||||
kinds = listOf(BadgeProfilesEvent.kind),
|
||||
kinds = listOf(BadgeProfilesEvent.kind, EmojiPackSelectionEvent.kind),
|
||||
authors = listOf(account.userProfile().pubkeyHex),
|
||||
limit = 1
|
||||
limit = 10
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -50,14 +50,24 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
|
||||
|
||||
return addressesToWatch.mapNotNull {
|
||||
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)
|
||||
if (aTag.kind < 25000 && aTag.dTag.isBlank()) {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter = JsonFilter(
|
||||
kinds = listOf(aTag.kind),
|
||||
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
|
||||
|
||||
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
|
||||
|
||||
fun String.isUTF16Char(pos: Int): Boolean {
|
||||
println("Test $pos ${Character.charCount(this.codePointAt(pos))}")
|
||||
return Character.charCount(this.codePointAt(pos)) == 2
|
||||
@ -77,3 +79,23 @@ fun String.firstFullChar(): String {
|
||||
|
||||
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] }
|
||||
|
||||
override fun taggedEmojis() = tags.filter { it.size > 2 && it[0] == "emoji" }.map { EmojiUrl(it[1], it[2]) }
|
||||
|
||||
override fun isSensitive() = tags.any {
|
||||
(it.size > 0 && it[0].equals("content-warning", 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 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 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)
|
||||
ContactListEvent.kind -> ContactListEvent(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)
|
||||
FileStorageEvent.kind -> FileStorageEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
|
@ -45,4 +45,7 @@ interface EventInterface {
|
||||
fun zapAddress(): String?
|
||||
fun isSensitive(): Boolean
|
||||
fun zapraiserAmount(): Long?
|
||||
|
||||
fun taggedEmojis(): List<EmojiUrl>
|
||||
fun matchTag1With(text: String): Boolean
|
||||
}
|
||||
|
@ -42,5 +42,24 @@ class ReactionEvent(
|
||||
val sig = Utils.sign(id, privateKey)
|
||||
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()
|
||||
|
||||
InLineIconRenderer(emojiList, style, maxLines, overflow, modifier)
|
||||
InLineIconRenderer(emojiList, style, fontSize, maxLines, overflow, modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@ -458,7 +458,7 @@ fun CreateTextWithEmoji(
|
||||
).toSpanStyle()
|
||||
}
|
||||
|
||||
InLineIconRenderer(emojiList, style, maxLines, overflow, modifier)
|
||||
InLineIconRenderer(emojiList, style, fontSize, maxLines, overflow, modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@ -551,7 +551,7 @@ fun CreateClickableTextWithEmoji(
|
||||
InLineIconRenderer(
|
||||
emojiLists!!.part2,
|
||||
LocalTextStyle.current.copy(color = overrideColor ?: MaterialTheme.colors.onBackground, fontWeight = fontWeight).toSpanStyle(),
|
||||
maxLines
|
||||
maxLines = maxLines
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -646,6 +646,7 @@ fun ClickableInLineIconRenderer(
|
||||
fun InLineIconRenderer(
|
||||
wordsInOrder: ImmutableList<Renderable>,
|
||||
style: SpanStyle,
|
||||
fontSize: TextUnit = TextUnit.Unspecified,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
overflow: TextOverflow = TextOverflow.Clip,
|
||||
modifier: Modifier = Modifier
|
||||
@ -692,6 +693,7 @@ fun InLineIconRenderer(
|
||||
Text(
|
||||
text = annotatedText,
|
||||
inlineContent = inlineContent,
|
||||
fontSize = fontSize,
|
||||
maxLines = maxLines,
|
||||
overflow = overflow,
|
||||
modifier = modifier
|
||||
|
@ -2,7 +2,6 @@ package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
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.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.Note
|
||||
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.LnZapRequestEvent
|
||||
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.TranslatableRichTextViewer
|
||||
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.profile35dpModifier
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.ExperimentalTime
|
||||
@ -207,18 +209,6 @@ fun RenderLikeGallery(
|
||||
nav: (String) -> Unit,
|
||||
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()) {
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Box(
|
||||
@ -228,19 +218,32 @@ fun RenderLikeGallery(
|
||||
Modifier.align(Alignment.TopEnd)
|
||||
}
|
||||
|
||||
when (val shortReaction = reactionType.firstFullChar()) {
|
||||
"+" -> Icon(
|
||||
painter = painterResource(R.drawable.ic_liked),
|
||||
null,
|
||||
modifier = modifier.size(Size18dp),
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
"-" -> Text(text = "\uD83D\uDC4E", modifier = modifier)
|
||||
else -> CreateTextWithEmoji(
|
||||
text = shortReaction,
|
||||
modifier = modifier,
|
||||
tags = tags
|
||||
if (reactionType.startsWith(":")) {
|
||||
val noStartColon = reactionType.removePrefix(":")
|
||||
val url = noStartColon.substringAfter(":")
|
||||
|
||||
val renderable = listOf(
|
||||
ImageUrlType(url)
|
||||
).toImmutableList()
|
||||
|
||||
InLineIconRenderer(
|
||||
renderable,
|
||||
style = SpanStyle(color = Color.White),
|
||||
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.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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.CutCornerShape
|
||||
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.Icon
|
||||
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.CommunityDefinitionEvent
|
||||
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.FileStorageHeaderEvent
|
||||
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.ScheduledFlag
|
||||
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.DoubleHorzSpacer
|
||||
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.Size30Modifier
|
||||
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.Size55Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||
@ -1035,6 +1043,10 @@ private fun RenderNoteRow(
|
||||
RenderPinListEvent(baseNote, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
|
||||
is EmojiPackEvent -> {
|
||||
RenderEmojiPack(baseNote, true, backgroundColor, accountViewModel)
|
||||
}
|
||||
|
||||
is LiveActivitiesEvent -> {
|
||||
RenderLiveActivityEvent(baseNote, accountViewModel, nav)
|
||||
}
|
||||
@ -1884,18 +1896,188 @@ fun LoadAddressableNote(aTag: ATag, content: @Composable (AddressableNote?) -> U
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderPinListEvent(
|
||||
public fun RenderEmojiPack(
|
||||
baseNote: Note,
|
||||
actionable: Boolean,
|
||||
backgroundColor: MutableState<Color>,
|
||||
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)
|
||||
@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,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
|
@ -66,6 +66,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
@ -81,8 +82,10 @@ import coil.request.CachePolicy
|
||||
import coil.request.ImageRequest
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.firstFullChar
|
||||
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.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
@ -808,7 +811,8 @@ private fun WatchReactionTypeForNote(baseNote: Note, accountViewModel: AccountVi
|
||||
|
||||
LaunchedEffect(key1 = reactionsState) {
|
||||
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)
|
||||
}
|
||||
|
||||
when (reactionType) {
|
||||
"+" -> {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_liked),
|
||||
null,
|
||||
modifier = iconModifier,
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
}
|
||||
if (reactionType.startsWith(":")) {
|
||||
val noStartColon = reactionType.removePrefix(":")
|
||||
val url = noStartColon.substringAfter(":")
|
||||
|
||||
"-" -> Text(text = "\uD83D\uDC4E", fontSize = iconFontSize)
|
||||
else -> Text(text = reactionType, fontSize = iconFontSize)
|
||||
val renderable = listOf(
|
||||
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
|
||||
fun ReactionChoicePopup(
|
||||
baseNote: Note,
|
||||
@ -1279,7 +1299,6 @@ fun ReactionChoicePopup(
|
||||
) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = accountState?.account ?: return
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val toRemove = remember {
|
||||
baseNote.reactedBy(account.userProfile()).toSet()
|
||||
@ -1292,67 +1311,117 @@ fun ReactionChoicePopup(
|
||||
) {
|
||||
FlowRow(horizontalArrangement = Arrangement.Center) {
|
||||
account.reactionChoices.forEach { reactionType ->
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.reactToOrDelete(
|
||||
baseNote,
|
||||
reactionType
|
||||
)
|
||||
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()
|
||||
}
|
||||
)
|
||||
}
|
||||
ActionableReactionButton(
|
||||
baseNote,
|
||||
reactionType,
|
||||
accountViewModel,
|
||||
onDismiss,
|
||||
onChangeAmount,
|
||||
toRemove
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val removeSymbol = remember(reactionType) {
|
||||
if (reactionType in toRemove) {
|
||||
" ✖"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
@Composable
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
private fun ActionableReactionButton(
|
||||
baseNote: Note,
|
||||
reactionType: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
onDismiss: () -> Unit,
|
||||
onChangeAmount: () -> Unit,
|
||||
toRemove: Set<String>
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
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
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.reactToOrDelete(
|
||||
baseNote,
|
||||
reactionType
|
||||
)
|
||||
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) {
|
||||
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.padding
|
||||
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.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
@ -24,7 +27,9 @@ import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
@ -33,6 +38,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
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.KeyboardType
|
||||
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.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
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.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.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
class UpdateReactionTypeViewModel(val account: Account) : ViewModel() {
|
||||
var nextChoice by mutableStateOf(TextFieldValue(""))
|
||||
@ -71,6 +87,10 @@ class UpdateReactionTypeViewModel(val account: Account) : ViewModel() {
|
||||
nextChoice = TextFieldValue("")
|
||||
}
|
||||
|
||||
fun addChoice(customEmoji: EmojiUrl) {
|
||||
reactionSet = reactionSet + (customEmoji.encode())
|
||||
}
|
||||
|
||||
fun removeChoice(reaction: String) {
|
||||
reactionSet = reactionSet - reaction
|
||||
}
|
||||
@ -116,10 +136,13 @@ fun UpdateReactionTypeDialog(onClose: () -> Unit, nip47uri: String? = null, acco
|
||||
)
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(modifier = Modifier.padding(10.dp).imePadding()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(10.dp)
|
||||
.imePadding()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
@ -155,30 +178,7 @@ fun UpdateReactionTypeDialog(onClose: () -> Unit, nip47uri: String? = null, acco
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
postViewModel.reactionSet.forEach { reactionType ->
|
||||
Button(
|
||||
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)
|
||||
}
|
||||
}
|
||||
RenderReactionOption(reactionType, postViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 com.vitorpamplona.amethyst.model.Note
|
||||
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.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
@ -61,7 +63,9 @@ class MultiSetCard(
|
||||
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()
|
||||
}.toImmutableMap()
|
||||
|
||||
|
@ -23,7 +23,6 @@ import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.ProvideTextStyle
|
||||
import androidx.compose.material.SnackbarDefaults.backgroundColor
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
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.CommunityDefinitionEvent
|
||||
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.FileStorageHeaderEvent
|
||||
import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
|
||||
@ -417,12 +417,19 @@ fun NoteMaster(
|
||||
nav
|
||||
)
|
||||
} else if (noteEvent is PinListEvent) {
|
||||
PinListHeader(
|
||||
RenderPinListEvent(
|
||||
baseNote,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
} else if (noteEvent is EmojiPackEvent) {
|
||||
RenderEmojiPack(
|
||||
baseNote,
|
||||
true,
|
||||
backgroundColor,
|
||||
accountViewModel
|
||||
)
|
||||
} else if (noteEvent is RelaySetEvent) {
|
||||
DisplayRelaySet(
|
||||
baseNote,
|
||||
|
@ -165,6 +165,14 @@ class AccountViewModel(val account: Account) : ViewModel() {
|
||||
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) {
|
||||
account.addPrivateBookmark(note)
|
||||
}
|
||||
|
@ -1118,7 +1118,7 @@ private fun DisplayBadges(
|
||||
val userBadgeState by baseUser.live().badges.observeAsState()
|
||||
val badgeList by remember(userBadgeState) {
|
||||
derivedStateOf {
|
||||
val list = (userBadgeState?.user?.acceptedBadges?.event as? BadgeProfilesEvent)?.badgeAwardEvents()
|
||||
val list = (userBadgeState?.user?.latestAcceptedBadges?.event as? BadgeProfilesEvent)?.badgeAwardEvents()
|
||||
if (list.isNullOrEmpty()) {
|
||||
null
|
||||
} else {
|
||||
|
Loading…
x
Reference in New Issue
Block a user