diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index b1a5c9a7f..70d71fbd1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index a69c79e44..fbfd81030 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -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) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 8cc2910f7..786e68353 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -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 { - 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 { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt index 8609e6e55..dbffa6439 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -31,6 +31,7 @@ class User(val pubkeyHex: String) { var latestContactList: ContactListEvent? = null var latestBookmarkList: BookmarkListEvent? = null + var latestAcceptedBadges: AddressableNote? = null var notes = setOf() private set @@ -49,8 +50,6 @@ class User(val pubkeyHex: String) { var privateChatrooms = mapOf() 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() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt index 3e11bfc1d..1dd93b5fe 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt @@ -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 ) ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt index 481d020b0..c09d38206 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt @@ -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) + ) + ) + } } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/StringUtils.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/StringUtils.kt index fa0d48dc2..fc3ff514d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/StringUtils.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/StringUtils.kt @@ -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 { + 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() +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/EmojiPackEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/EmojiPackEvent.kt new file mode 100644 index 000000000..4ca9dcafb --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/EmojiPackEvent.kt @@ -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>, + 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>() + 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 + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/EmojiPackSelectionEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/EmojiPackSelectionEvent.kt new file mode 100644 index 000000000..1b013e832 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/EmojiPackSelectionEvent.kt @@ -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>, + 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?, + privateKey: ByteArray, + createdAt: Long = TimeUtils.now() + ): EmojiPackSelectionEvent { + val msg = "" + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() + val tags = mutableListOf>() + + 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()) + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt index ddc21ee35..5d8f25d99 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt @@ -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) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventInterface.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventInterface.kt index a874324b7..8ca18b031 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventInterface.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventInterface.kt @@ -45,4 +45,7 @@ interface EventInterface { fun zapAddress(): String? fun isSensitive(): Boolean fun zapraiserAmount(): Long? + + fun taggedEmojis(): List + fun matchTag1With(text: String): Boolean } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt index bfde96a4c..96a6cab14 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt @@ -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()) + } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt index 3299ce87a..3fce5790e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt @@ -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, 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index e829302d6..8f52784b6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt index 27234153b..59751e65f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt @@ -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( - likeEvents.mapNotNull { - it.event?.tags()?.filter { it.size > 2 && it[0] == "emoji" } - }.flatten() - ) - } else { - ImmutableListOfLists() - } - } - 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) + } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 5d1f51c25..01b059978 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -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, 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, + 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, accountViewModel: AccountViewModel, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 0aa5ec64c..05cea01f4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -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 +) { + 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 + ) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt index 143090947..df1b7a05d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt @@ -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, 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, + accountViewModel: AccountViewModel, + onClick: ((EmojiUrl) -> Unit)? +) { + RenderEmojiPack( + baseNote = it, + actionable = false, + backgroundColor = bgColor, + accountViewModel = accountViewModel, + onClick = onClick + ) +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedState.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedState.kt index 587daab5f..4a2899aa9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedState.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedState.kt @@ -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() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index b2c9c26fc..8b9070568 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -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, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 6456340a9..877b4edc3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -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) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index 2c0451cb1..5218ceb52 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -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 {