From accad0c77accc5783568b8a2437a0872755389ec Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 21 Jun 2024 11:59:58 -0400 Subject: [PATCH 01/12] Adds a Share option with NIP-19 nprofile icon on the User Profile screen Moves to NProfile instead of NPub to cite users. Adds Npub or NIP-05 to the QR Screen --- .../amethyst/model/LocalCache.kt | 95 ++++++------ .../com/vitorpamplona/amethyst/model/User.kt | 145 +++++++++--------- .../amethyst/ui/actions/NewMessageTagger.kt | 20 +-- .../amethyst/ui/navigation/DrawerContent.kt | 9 +- .../ui/note/NIP05VerificationDisplay.kt | 2 +- .../amethyst/ui/note/NoteQuickActionMenu.kt | 20 ++- .../amethyst/ui/note/elements/DropDownMenu.kt | 10 +- .../amethyst/ui/qrcode/ShowQRDialog.kt | 47 ++++-- .../ui/screen/loggedIn/ProfileScreen.kt | 34 +++- .../quartz/encoders/Nip19Bech32.kt | 100 ++++++++---- 10 files changed, 281 insertions(+), 201 deletions(-) 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 a88e97566..48c5f88d1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -241,33 +241,25 @@ object LocalCache { return users.get(key) } - fun getAddressableNoteIfExists(key: String): AddressableNote? { - return addressables.get(key) - } + fun getAddressableNoteIfExists(key: String): AddressableNote? = addressables.get(key) - fun getNoteIfExists(key: String): Note? { - return addressables.get(key) ?: notes.get(key) - } + fun getNoteIfExists(key: String): Note? = addressables.get(key) ?: notes.get(key) - fun getChannelIfExists(key: String): Channel? { - return channels.get(key) - } + fun getChannelIfExists(key: String): Channel? = channels.get(key) - fun getNoteIfExists(event: Event): Note? { - return if (event is AddressableEvent) { + fun getNoteIfExists(event: Event): Note? = + if (event is AddressableEvent) { getAddressableNoteIfExists(event.addressTag()) } else { getNoteIfExists(event.id) } - } - fun getOrCreateNote(event: Event): Note { - return if (event is AddressableEvent) { + fun getOrCreateNote(event: Event): Note = + if (event is AddressableEvent) { getOrCreateAddressableNote(event.address()) } else { getOrCreateNote(event.id) } - } fun checkGetOrCreateNote(key: String): Note? { checkNotInMainThread() @@ -348,8 +340,8 @@ object LocalCache { return HexValidator.isHex(key) } - fun checkGetOrCreateAddressableNote(key: String): AddressableNote? { - return try { + fun checkGetOrCreateAddressableNote(key: String): AddressableNote? = + try { val addr = ATag.parse(key, null) // relay doesn't matter for the index. if (addr != null) { getOrCreateAddressableNote(addr) @@ -360,7 +352,6 @@ object LocalCache { Log.e("LocalCache", "Invalid Key to create channel: $key", e) null } - } fun getOrCreateAddressableNoteInternal(key: ATag): AddressableNote { // checkNotInMainThread() @@ -395,6 +386,10 @@ object LocalCache { val newUserMetadata = event.contactMetaData() if (newUserMetadata != null) { oldUser.updateUserInfo(newUserMetadata, event) + if (relay != null) { + oldUser.addRelayBeingUsed(relay, event.createdAt) + oldUser.latestMetadataRelay = relay.url + } } // Log.d("MT", "New User Metadata ${oldUser.pubkeyDisplayHex()} ${oldUser.toBestDisplayName()} from ${relay?.url}") } else { @@ -428,11 +423,11 @@ object LocalCache { } } - fun formattedDateTime(timestamp: Long): String { - return Instant.ofEpochSecond(timestamp) + fun formattedDateTime(timestamp: Long): String = + Instant + .ofEpochSecond(timestamp) .atZone(ZoneId.systemDefault()) .format(DateTimeFormatter.ofPattern("uuuu MMM d hh:mm a")) - } fun consume( event: TextNoteEvent, @@ -755,8 +750,8 @@ object LocalCache { } } - fun computeReplyTo(event: Event): List { - return when (event) { + fun computeReplyTo(event: Event): List = + when (event) { is PollNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } is WikiNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } is LongTextNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } @@ -805,7 +800,6 @@ object LocalCache { else -> emptyList() } - } fun consume( event: PollNoteEvent, @@ -1218,7 +1212,8 @@ object LocalCache { if (deletionIndex.add(event)) { var deletedAtLeastOne = false - event.deleteEvents() + event + .deleteEvents() .mapNotNull { getNoteIfExists(it) } .forEach { deleteNote -> // must be the same author @@ -2030,16 +2025,16 @@ object LocalCache { suspend fun findStatusesForUser(user: User): ImmutableList { checkNotInMainThread() - return addressables.filter { _, it -> - val noteEvent = it.event - ( - noteEvent is StatusEvent && - noteEvent.pubKey == user.pubkeyHex && - !noteEvent.isExpired() && - noteEvent.content.isNotBlank() - ) - } - .sortedWith(compareBy({ it.event?.expiration() ?: it.event?.createdAt() }, { it.idHex })) + return addressables + .filter { _, it -> + val noteEvent = it.event + ( + noteEvent is StatusEvent && + noteEvent.pubKey == user.pubkeyHex && + !noteEvent.isExpired() && + noteEvent.content.isNotBlank() + ) + }.sortedWith(compareBy({ it.event?.expiration() ?: it.event?.createdAt() }, { it.idHex })) .reversed() .toImmutableList() } @@ -2066,9 +2061,7 @@ object LocalCache { val modificationCache = LruCache>(20) - fun cachedModificationEventsForNote(note: Note): List? { - return modificationCache[note.idHex] - } + fun cachedModificationEventsForNote(note: Note): List? = modificationCache[note.idHex] suspend fun findLatestModificationForNote(note: Note): List { checkNotInMainThread() @@ -2082,11 +2075,12 @@ object LocalCache { val time = TimeUtils.now() val newNotes = - notes.filter { _, item -> - val noteEvent = item.event + notes + .filter { _, item -> + val noteEvent = item.event - noteEvent is TextNoteModificationEvent && noteEvent.pubKey == originalAuthor && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time) - }.sortedWith(compareBy({ it.createdAt() }, { it.idHex })) + noteEvent is TextNoteModificationEvent && noteEvent.pubKey == originalAuthor && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time) + }.sortedWith(compareBy({ it.createdAt() }, { it.idHex })) modificationCache.put(note.idHex, newNotes) @@ -2210,9 +2204,11 @@ object LocalCache { note.event is GenericRepostEvent ) && note.replyTo?.any { it.liveSet?.isInUse() == true } != true && - note.liveSet?.isInUse() != true && // don't delete if observing. + note.liveSet?.isInUse() != true && + // don't delete if observing. note.author?.pubkeyHex !in - accounts && // don't delete if it is the logged in account + accounts && + // don't delete if it is the logged in account note.event?.isTaggedUsers(accounts) != true // don't delete if it's a notification to the logged in user } @@ -2308,8 +2304,7 @@ object LocalCache { ?.hiddenUsers ?.map { userHex -> (notes.filter { _, it -> it.event?.pubKey() == userHex } + addressables.filter { _, it -> it.event?.pubKey() == userHex }).toSet() - } - ?.flatten() + }?.flatten() ?: emptyList() toBeRemoved.forEach { @@ -2596,8 +2591,8 @@ object LocalCache { } } - fun hasConsumed(notificationEvent: Event): Boolean { - return if (notificationEvent is AddressableEvent) { + fun hasConsumed(notificationEvent: Event): Boolean = + if (notificationEvent is AddressableEvent) { val note = addressables.get(notificationEvent.addressTag()) val noteEvent = note?.event noteEvent != null && notificationEvent.createdAt <= noteEvent.createdAt() @@ -2605,7 +2600,6 @@ object LocalCache { val note = notes.get(notificationEvent.id) note?.event != null } - } } @Stable @@ -2617,8 +2611,7 @@ class LocalCacheLiveData { private val bundler = BundledInsert(1000, Dispatchers.IO) fun invalidateData(newNote: Note) { - bundler.invalidateList(newNote) { - bundledNewNotes -> + bundler.invalidateList(newNote) { bundledNewNotes -> _newEventBundles.emit(bundledNewNotes) } } 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 9844ef622..0d78e8c3e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -34,7 +34,9 @@ import com.vitorpamplona.amethyst.ui.note.toShortenHex import com.vitorpamplona.quartz.encoders.Hex import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.Lud06 +import com.vitorpamplona.quartz.encoders.Nip19Bech32 import com.vitorpamplona.quartz.encoders.toNpub +import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent import com.vitorpamplona.quartz.events.BookmarkListEvent import com.vitorpamplona.quartz.events.ChatroomKey import com.vitorpamplona.quartz.events.ContactListEvent @@ -49,10 +51,13 @@ import kotlinx.coroutines.flow.MutableStateFlow import java.math.BigDecimal @Stable -class User(val pubkeyHex: String) { +class User( + val pubkeyHex: String, +) { var info: UserMetadata? = null var latestMetadata: MetadataEvent? = null + var latestMetadataRelay: String? = null var latestContactList: ContactListEvent? = null var latestBookmarkList: BookmarkListEvent? = null @@ -76,7 +81,16 @@ class User(val pubkeyHex: String) { fun pubkeyDisplayHex() = pubkeyNpub().toShortenHex() - fun toNostrUri() = "nostr:${pubkeyNpub()}" + fun toNProfile(): String { + val relayList = (LocalCache.getAddressableNoteIfExists(AdvertisedRelayListEvent.createAddressTag(pubkeyHex))?.event as? AdvertisedRelayListEvent)?.writeRelays() + + return Nip19Bech32.createNProfile( + pubkeyHex, + relayList?.take(3) ?: listOfNotNull(latestMetadataRelay), + ) + } + + fun toNostrUri() = "nostr:${toNProfile()}" override fun toString(): String = pubkeyHex @@ -96,17 +110,11 @@ class User(val pubkeyHex: String) { return firstName } - fun toBestDisplayName(): String { - return info?.bestName() ?: pubkeyDisplayHex() - } + fun toBestDisplayName(): String = info?.bestName() ?: pubkeyDisplayHex() - fun nip05(): String? { - return info?.nip05 - } + fun nip05(): String? = info?.nip05 - fun profilePicture(): String? { - return info?.picture - } + fun profilePicture(): String? = info?.picture fun updateBookmark(event: BookmarkListEvent) { if (event.id == latestBookmarkList?.id) return @@ -132,10 +140,18 @@ class User(val pubkeyHex: String) { // Update Followers of the past user list // Update Followers of the new contact list (oldContactListEvent)?.unverifiedFollowKeySet()?.forEach { - LocalCache.getUserIfExists(it)?.liveSet?.innerFollowers?.invalidateData() + LocalCache + .getUserIfExists(it) + ?.liveSet + ?.innerFollowers + ?.invalidateData() } (latestContactList)?.unverifiedFollowKeySet()?.forEach { - LocalCache.getUserIfExists(it)?.liveSet?.innerFollowers?.invalidateData() + LocalCache + .getUserIfExists(it) + ?.liveSet + ?.innerFollowers + ?.invalidateData() } liveSet?.innerRelays?.invalidateData() @@ -198,25 +214,19 @@ class User(val pubkeyHex: String) { return amount } - fun reportsBy(user: User): Set { - return reports[user] ?: emptySet() - } + fun reportsBy(user: User): Set = reports[user] ?: emptySet() - fun countReportAuthorsBy(users: Set): Int { - return reports.count { it.key.pubkeyHex in users } - } + fun countReportAuthorsBy(users: Set): Int = reports.count { it.key.pubkeyHex in users } - fun reportsBy(users: Set): List { - return reports + fun reportsBy(users: Set): List = + reports .mapNotNull { if (it.key.pubkeyHex in users) { it.value } else { null } - } - .flatten() - } + }.flatten() @Synchronized private fun getOrCreatePrivateChatroomSync(key: ChatroomKey): Chatroom { @@ -235,9 +245,7 @@ class User(val pubkeyHex: String) { return getOrCreatePrivateChatroom(key) } - private fun getOrCreatePrivateChatroom(key: ChatroomKey): Chatroom { - return privateChatrooms[key] ?: getOrCreatePrivateChatroomSync(key) - } + private fun getOrCreatePrivateChatroom(key: ChatroomKey): Chatroom = privateChatrooms[key] ?: getOrCreatePrivateChatroomSync(key) fun addMessage( room: ChatroomKey, @@ -326,13 +334,9 @@ class User(val pubkeyHex: String) { liveSet?.innerMetadata?.invalidateData() } - fun isFollowing(user: User): Boolean { - return latestContactList?.isTaggedUser(user.pubkeyHex) ?: false - } + fun isFollowing(user: User): Boolean = latestContactList?.isTaggedUser(user.pubkeyHex) ?: false - fun isFollowingHashtag(tag: String): Boolean { - return latestContactList?.isTaggedHash(tag) ?: false - } + fun isFollowingHashtag(tag: String): Boolean = latestContactList?.isTaggedHash(tag) ?: false fun isFollowingHashtagCached(tag: String): Boolean { return latestContactList?.verifiedFollowTagSet?.let { @@ -362,37 +366,21 @@ class User(val pubkeyHex: String) { ?: false } - fun transientFollowCount(): Int? { - return latestContactList?.unverifiedFollowKeySet()?.size - } + fun transientFollowCount(): Int? = latestContactList?.unverifiedFollowKeySet()?.size - suspend fun transientFollowerCount(): Int { - return LocalCache.users.count { _, it -> it.latestContactList?.isTaggedUser(pubkeyHex) ?: false } - } + suspend fun transientFollowerCount(): Int = LocalCache.users.count { _, it -> it.latestContactList?.isTaggedUser(pubkeyHex) ?: false } - fun cachedFollowingKeySet(): Set { - return latestContactList?.verifiedFollowKeySet ?: emptySet() - } + fun cachedFollowingKeySet(): Set = latestContactList?.verifiedFollowKeySet ?: emptySet() - fun cachedFollowingTagSet(): Set { - return latestContactList?.verifiedFollowTagSet ?: emptySet() - } + fun cachedFollowingTagSet(): Set = latestContactList?.verifiedFollowTagSet ?: emptySet() - fun cachedFollowingGeohashSet(): Set { - return latestContactList?.verifiedFollowGeohashSet ?: emptySet() - } + fun cachedFollowingGeohashSet(): Set = latestContactList?.verifiedFollowGeohashSet ?: emptySet() - fun cachedFollowingCommunitiesSet(): Set { - return latestContactList?.verifiedFollowCommunitySet ?: emptySet() - } + fun cachedFollowingCommunitiesSet(): Set = latestContactList?.verifiedFollowCommunitySet ?: emptySet() - fun cachedFollowCount(): Int? { - return latestContactList?.verifiedFollowKeySet?.size - } + fun cachedFollowCount(): Int? = latestContactList?.verifiedFollowKeySet?.size - suspend fun cachedFollowerCount(): Int { - return LocalCache.users.count { _, it -> it.latestContactList?.isTaggedUser(pubkeyHex) ?: false } - } + suspend fun cachedFollowerCount(): Int = LocalCache.users.count { _, it -> it.latestContactList?.isTaggedUser(pubkeyHex) ?: false } fun hasSentMessagesTo(key: ChatroomKey?): Boolean { val messagesToUser = privateChatrooms[key] ?: return false @@ -403,16 +391,13 @@ class User(val pubkeyHex: String) { fun hasReport( loggedIn: User, type: ReportEvent.ReportType, - ): Boolean { - return reports[loggedIn]?.firstOrNull { + ): Boolean = + reports[loggedIn]?.firstOrNull { it.event is ReportEvent && (it.event as ReportEvent).reportedAuthor().any { it.reportType == type } } != null - } - fun anyNameStartsWith(username: String): Boolean { - return info?.anyNameStartsWith(username) ?: false - } + fun anyNameStartsWith(username: String): Boolean = info?.anyNameStartsWith(username) ?: false var liveSet: UserLiveSet? = null var flowSet: UserFlowSet? = null @@ -473,14 +458,14 @@ class User(val pubkeyHex: String) { } @Stable -class UserFlowSet(u: User) { +class UserFlowSet( + u: User, +) { // Observers line up here. val follows = UserBundledRefresherFlow(u) val relays = UserBundledRefresherFlow(u) - fun isInUse(): Boolean { - return relays.stateFlow.subscriptionCount.value > 0 || follows.stateFlow.subscriptionCount.value > 0 - } + fun isInUse(): Boolean = relays.stateFlow.subscriptionCount.value > 0 || follows.stateFlow.subscriptionCount.value > 0 fun destroy() { relays.destroy() @@ -489,7 +474,9 @@ class UserFlowSet(u: User) { } @Stable -class UserLiveSet(u: User) { +class UserLiveSet( + u: User, +) { val innerMetadata = UserBundledRefresherLiveData(u) // UI Observers line up here. @@ -521,8 +508,8 @@ class UserLiveSet(u: User) { val userMetadataInfo = innerMetadata.map { it.user.info }.distinctUntilChanged() - fun isInUse(): Boolean { - return metadata.hasObservers() || + fun isInUse(): Boolean = + metadata.hasObservers() || follows.hasObservers() || followers.hasObservers() || reports.hasObservers() || @@ -535,7 +522,6 @@ class UserLiveSet(u: User) { profilePictureChanges.hasObservers() || nip05Changes.hasObservers() || userMetadataInfo.hasObservers() - } fun destroy() { innerMetadata.destroy() @@ -558,7 +544,9 @@ data class RelayInfo( var counter: Long, ) -class UserBundledRefresherLiveData(val user: User) : LiveData(UserState(user)) { +class UserBundledRefresherLiveData( + val user: User, +) : LiveData(UserState(user)) { // Refreshes observers in batches. private val bundler = BundledUpdate(500, Dispatchers.IO) @@ -585,7 +573,9 @@ class UserBundledRefresherLiveData(val user: User) : LiveData(UserSta } @Stable -class UserBundledRefresherFlow(val user: User) { +class UserBundledRefresherFlow( + val user: User, +) { // Refreshes observers in batches. private val bundler = BundledUpdate(500, Dispatchers.IO) val stateFlow = MutableStateFlow(UserState(user)) @@ -605,7 +595,10 @@ class UserBundledRefresherFlow(val user: User) { } } -class UserLoadingLiveData(val user: User, initialValue: Y?) : MediatorLiveData(initialValue) { +class UserLoadingLiveData( + val user: User, + initialValue: Y?, +) : MediatorLiveData(initialValue) { override fun onActive() { super.onActive() NostrSingleUserDataSource.add(user) @@ -617,4 +610,6 @@ class UserLoadingLiveData(val user: User, initialValue: Y?) : MediatorLiveDat } } -@Immutable class UserState(val user: User) +@Immutable class UserState( + val user: User, +) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt index 8cbeb910a..710563b55 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt @@ -100,10 +100,10 @@ class NewMessageTagger( val results = parseDirtyWordForKey(word) when (val entity = results?.key?.entity) { is Nip19Bech32.NPub -> { - getNostrAddress(dao.getOrCreateUser(entity.hex).pubkeyNpub(), results.restOfWord) + getNostrAddress(dao.getOrCreateUser(entity.hex).toNProfile(), results.restOfWord) } is Nip19Bech32.NProfile -> { - getNostrAddress(dao.getOrCreateUser(entity.hex).pubkeyNpub(), results.restOfWord) + getNostrAddress(dao.getOrCreateUser(entity.hex).toNProfile(), results.restOfWord) } is Nip19Bech32.Note -> { @@ -138,17 +138,15 @@ class NewMessageTagger( word } } - } - .joinToString(" ") - } - .joinToString("\n") + }.joinToString(" ") + }.joinToString("\n") } fun getNostrAddress( bechAddress: String, restOfTheWord: String?, - ): String { - return if (restOfTheWord.isNullOrEmpty()) { + ): String = + if (restOfTheWord.isNullOrEmpty()) { "nostr:$bechAddress" } else { if (Bech32.ALPHABET.contains(restOfTheWord.get(0), true)) { @@ -157,9 +155,11 @@ class NewMessageTagger( "nostr:${bechAddress}$restOfTheWord" } } - } - @Immutable data class DirtyKeyInfo(val key: Nip19Bech32.ParseReturn, val restOfWord: String?) + @Immutable data class DirtyKeyInfo( + val key: Nip19Bech32.ParseReturn, + val restOfWord: String?, + ) fun parseDirtyWordForKey(mightBeAKey: String): DirtyKeyInfo? { var key = mightBeAKey diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index 4202294fc..edcf50c7f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -168,8 +168,7 @@ fun DrawerContent( BottomContent( accountViewModel.account.userProfile(), drawerState, - loadProfilePicture = accountViewModel.settings.showProfilePictures.value, - loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, + accountViewModel, nav, ) } @@ -741,8 +740,7 @@ fun IconRowRelays( fun BottomContent( user: User, drawerState: DrawerState, - loadProfilePicture: Boolean, - loadRobohash: Boolean, + accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { val coroutineScope = rememberCoroutineScope() @@ -800,8 +798,7 @@ fun BottomContent( if (dialogOpen) { ShowQRDialog( user, - loadProfilePicture = loadProfilePicture, - loadRobohash = loadRobohash, + accountViewModel, onScan = { dialogOpen = false coroutineScope.launch { drawerState.close() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt index 5435d2f7d..3e6e49e0e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt @@ -310,7 +310,7 @@ fun DisplayStatus( } @Composable -private fun DisplayNIP05( +fun DisplayNIP05( nip05: String, nip05Verified: MutableState, accountViewModel: AccountViewModel, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt index 159961c92..2cc7a5c62 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt @@ -84,6 +84,7 @@ import androidx.core.graphics.ColorUtils import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.actions.NewPostView import com.vitorpamplona.amethyst.ui.components.SelectTextDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -95,7 +96,6 @@ import com.vitorpamplona.amethyst.ui.theme.secondaryButtonBackground import com.vitorpamplona.quartz.events.AudioTrackEvent import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.PeopleListEvent -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch private fun lightenColor( @@ -110,6 +110,10 @@ private fun lightenColor( return Color(argb) } +val externalLinkForUser = { user: User -> + "https://njump.me/${user.toNProfile()}" +} + val externalLinkForNote = { note: Note -> if (note is AddressableNote) { if (note.event?.getReward() != null) { @@ -298,10 +302,12 @@ private fun RenderMainPopup( Icons.Default.AlternateEmail, stringRes(R.string.quick_action_copy_user_id), ) { - scope.launch(Dispatchers.IO) { - clipboardManager.setText(AnnotatedString("nostr:${note.author?.pubkeyNpub()}")) - showToast(R.string.copied_user_id_to_clipboard) - onDismiss() + note.author?.let { + scope.launch { + clipboardManager.setText(AnnotatedString(it.toNostrUri())) + showToast(R.string.copied_user_id_to_clipboard) + onDismiss() + } } } VerticalDivider(color = primaryLight) @@ -309,8 +315,8 @@ private fun RenderMainPopup( Icons.Default.FormatQuote, stringRes(R.string.quick_action_copy_note_id), ) { - scope.launch(Dispatchers.IO) { - clipboardManager.setText(AnnotatedString("nostr:${note.toNEvent()}")) + scope.launch { + clipboardManager.setText(AnnotatedString(note.toNostrUri())) showToast(R.string.copied_note_id_to_clipboard) onDismiss() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt index 1c251469d..ffa6e8a74 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt @@ -197,9 +197,11 @@ fun NoteDropDownMenu( DropdownMenuItem( text = { Text(stringRes(R.string.copy_user_pubkey)) }, onClick = { - scope.launch(Dispatchers.IO) { - clipboardManager.setText(AnnotatedString("nostr:${note.author?.pubkeyNpub()}")) - onDismiss() + note.author?.let { + scope.launch(Dispatchers.IO) { + clipboardManager.setText(AnnotatedString("nostr:${it.pubkeyNpub()}")) + onDismiss() + } } }, ) @@ -207,7 +209,7 @@ fun NoteDropDownMenu( text = { Text(stringRes(R.string.copy_note_id)) }, onClick = { scope.launch(Dispatchers.IO) { - clipboardManager.setText(AnnotatedString("nostr:" + note.toNEvent())) + clipboardManager.setText(AnnotatedString(note.toNostrUri())) onDismiss() } }, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/ShowQRDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/ShowQRDialog.kt index 981efabe2..6509f4253 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/ShowQRDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/ShowQRDialog.kt @@ -45,16 +45,22 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.actions.CloseButton import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji +import com.vitorpamplona.amethyst.ui.components.DisplayNIP05 import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage +import com.vitorpamplona.amethyst.ui.components.mockAccountViewModel +import com.vitorpamplona.amethyst.ui.components.nip05VerificationAsAState +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size35dp import com.vitorpamplona.quartz.events.UserMetadata @@ -62,21 +68,20 @@ import com.vitorpamplona.quartz.events.UserMetadata @Preview @Composable fun ShowQRDialogPreview() { - val user = User("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") - - user.info = + val accountViewModel = mockAccountViewModel() + accountViewModel.userProfile().info = UserMetadata().apply { name = "My Name" picture = "Picture" + nip05 = null banner = "http://banner.com/test" website = "http://mywebsite.com/test" about = "This is the about me" } ShowQRDialog( - user = user, - loadProfilePicture = false, - loadRobohash = false, + user = accountViewModel.userProfile(), + accountViewModel = accountViewModel, onScan = {}, onClose = {}, ) @@ -85,8 +90,7 @@ fun ShowQRDialogPreview() { @Composable fun ShowQRDialog( user: User, - loadProfilePicture: Boolean, - loadRobohash: Boolean, + accountViewModel: AccountViewModel, onScan: (String) -> Unit, onClose: () -> Unit, ) { @@ -126,8 +130,8 @@ fun ShowQRDialog( .height(100.dp) .clip(shape = CircleShape) .border(3.dp, MaterialTheme.colorScheme.background, CircleShape), - loadProfilePicture = loadProfilePicture, - loadRobohash = loadRobohash, + loadProfilePicture = accountViewModel.settings.showProfilePictures.value, + loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, ) } Row( @@ -141,13 +145,34 @@ fun ShowQRDialog( fontSize = 18.sp, ) } + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxWidth().padding(top = 4.dp), + ) { + val nip05 = user.nip05() + if (nip05 != null) { + val nip05Verified = + nip05VerificationAsAState(user.info!!, user.pubkeyHex, accountViewModel) + + DisplayNIP05(nip05, nip05Verified, accountViewModel) + } else { + Text( + text = user.pubkeyDisplayHex(), + fontSize = 14.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } } Row( horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth().padding(horizontal = Size35dp), ) { - QrCodeDrawer("nostr:${user.pubkeyNpub()}") + QrCodeDrawer(user.toNostrUri()) } Row(modifier = Modifier.padding(horizontal = 30.dp)) { 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 776111758..a7d3e1aca 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 @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn +import android.content.Intent import androidx.compose.animation.core.tween import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image @@ -103,6 +104,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.core.content.ContextCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -136,6 +138,7 @@ import com.vitorpamplona.amethyst.ui.note.DrawPlayName import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog import com.vitorpamplona.amethyst.ui.note.LightningAddressIcon import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote +import com.vitorpamplona.amethyst.ui.note.externalLinkForUser import com.vitorpamplona.amethyst.ui.note.payViaIntent import com.vitorpamplona.amethyst.ui.qrcode.ShowQRDialog import com.vitorpamplona.amethyst.ui.screen.FeedState @@ -996,9 +999,8 @@ private fun DrawAdditionalInfo( if (dialogOpen) { ShowQRDialog( - user, - accountViewModel.settings.showProfilePictures.value, - loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, + user = user, + accountViewModel = accountViewModel, onScan = { dialogOpen = false nav(it) @@ -1870,6 +1872,32 @@ fun UserProfileDropDownMenu( }, ) + val actContext = LocalContext.current + + DropdownMenuItem( + text = { Text(stringRes(R.string.quick_action_share)) }, + onClick = { + val sendIntent = + Intent().apply { + action = Intent.ACTION_SEND + type = "text/plain" + putExtra( + Intent.EXTRA_TEXT, + externalLinkForUser(user), + ) + putExtra( + Intent.EXTRA_TITLE, + stringRes(actContext, R.string.quick_action_share_browser_link), + ) + } + + val shareIntent = + Intent.createChooser(sendIntent, stringRes(actContext, R.string.quick_action_share)) + ContextCompat.startActivity(actContext, shareIntent, null) + onDismiss() + }, + ) + if (accountViewModel.userProfile() != user) { HorizontalDivider(thickness = DividerThickness) if (accountViewModel.account.isHidden(user)) { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19Bech32.kt b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19Bech32.kt index 3a8cab829..951bfaa83 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19Bech32.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19Bech32.kt @@ -39,7 +39,9 @@ object Nip19Bech32 { ADDRESS, } - enum class TlvTypes(val id: Byte) { + enum class TlvTypes( + val id: Byte, + ) { SPECIAL(0), RELAY(1), AUTHOR(2), @@ -53,33 +55,59 @@ object Nip19Bech32 { ) @Immutable - data class ParseReturn(val entity: Entity, val additionalChars: String? = null) + data class ParseReturn( + val entity: Entity, + val additionalChars: String? = null, + ) interface Entity @Immutable - data class NSec(val hex: String) : Entity + data class NSec( + val hex: String, + ) : Entity @Immutable - data class NPub(val hex: String) : Entity + data class NPub( + val hex: String, + ) : Entity @Immutable - data class Note(val hex: String) : Entity + data class Note( + val hex: String, + ) : Entity @Immutable - data class NProfile(val hex: String, val relay: List) : Entity + data class NProfile( + val hex: String, + val relay: List, + ) : Entity @Immutable - data class NEvent(val hex: String, val relay: List, val author: String?, val kind: Int?) : Entity + data class NEvent( + val hex: String, + val relay: List, + val author: String?, + val kind: Int?, + ) : Entity @Immutable - data class NAddress(val atag: String, val relay: List, val author: String, val kind: Int) : Entity + data class NAddress( + val atag: String, + val relay: List, + val author: String, + val kind: Int, + ) : Entity @Immutable - data class NRelay(val relay: List) : Entity + data class NRelay( + val relay: List, + ) : Entity @Immutable - data class NEmbed(val event: Event) : Entity + data class NEmbed( + val event: Event, + ) : Entity fun uriToRoute(uri: String?): ParseReturn? { if (uri == null) return null @@ -108,8 +136,8 @@ object Nip19Bech32 { type: String, key: String?, additionalChars: String?, - ): ParseReturn? { - return try { + ): ParseReturn? = + try { val bytes = (type + key).bechToBytes() when (type.lowercase()) { @@ -129,7 +157,6 @@ object Nip19Bech32 { Log.w("NIP19 Parser", "Issue trying to Decode NIP19 $key: ${e.message}", e) null } - } private fun nembed(bytes: ByteArray): NEmbed? { if (bytes.isEmpty()) return null @@ -205,21 +232,30 @@ object Nip19Bech32 { author: String?, kind: Int?, relay: String?, - ): String { - return TlvBuilder() + ): String = + TlvBuilder() .apply { addHex(TlvTypes.SPECIAL, idHex) addStringIfNotNull(TlvTypes.RELAY, relay) addHexIfNotNull(TlvTypes.AUTHOR, author) addIntIfNotNull(TlvTypes.KIND, kind) - } - .build() + }.build() .toNEvent() - } - fun createNEmbed(event: Event): String { - return gzip(event.toJson()).toNEmbed() - } + fun createNProfile( + authorPubKeyHex: String, + relay: List, + ): String = + TlvBuilder() + .apply { + addHex(TlvTypes.SPECIAL, authorPubKeyHex) + relay.forEach { + addStringIfNotNull(TlvTypes.RELAY, it) + } + }.build() + .toNProfile() + + fun createNEmbed(event: Event): String = gzip(event.toJson()).toNEmbed() fun gzip(content: String): ByteArray { val bos = ByteArrayOutputStream() @@ -239,23 +275,24 @@ fun ByteArray.toNote() = Bech32.encodeBytes(hrp = "note", this, Bech32.Encoding. fun ByteArray.toNEvent() = Bech32.encodeBytes(hrp = "nevent", this, Bech32.Encoding.Bech32) +fun ByteArray.toNProfile() = Bech32.encodeBytes(hrp = "nprofile", this, Bech32.Encoding.Bech32) + fun ByteArray.toNAddress() = Bech32.encodeBytes(hrp = "naddr", this, Bech32.Encoding.Bech32) fun ByteArray.toLnUrl() = Bech32.encodeBytes(hrp = "lnurl", this, Bech32.Encoding.Bech32) fun ByteArray.toNEmbed() = Bech32.encodeBytes(hrp = "nembed", this, Bech32.Encoding.Bech32) -fun decodePublicKey(key: String): ByteArray { - return when (val parsed = Nip19Bech32.uriToRoute(key)?.entity) { +fun decodePublicKey(key: String): ByteArray = + when (val parsed = Nip19Bech32.uriToRoute(key)?.entity) { is Nip19Bech32.NSec -> KeyPair(privKey = key.bechToBytes()).pubKey is Nip19Bech32.NPub -> parsed.hex.hexToByteArray() is Nip19Bech32.NProfile -> parsed.hex.hexToByteArray() else -> Hex.decode(key) // crashes on purpose } -} -fun decodePrivateKeyAsHexOrNull(key: String): HexKey? { - return try { +fun decodePrivateKeyAsHexOrNull(key: String): HexKey? = + try { when (val parsed = Nip19Bech32.uriToRoute(key)?.entity) { is Nip19Bech32.NSec -> parsed.hex is Nip19Bech32.NPub -> null @@ -271,10 +308,9 @@ fun decodePrivateKeyAsHexOrNull(key: String): HexKey? { if (e is CancellationException) throw e null } -} -fun decodePublicKeyAsHexOrNull(key: String): HexKey? { - return try { +fun decodePublicKeyAsHexOrNull(key: String): HexKey? = + try { when (val parsed = Nip19Bech32.uriToRoute(key)?.entity) { is Nip19Bech32.NSec -> KeyPair(privKey = key.bechToBytes()).pubKey.toHexKey() is Nip19Bech32.NPub -> parsed.hex @@ -290,10 +326,9 @@ fun decodePublicKeyAsHexOrNull(key: String): HexKey? { if (e is CancellationException) throw e null } -} -fun decodeEventIdAsHexOrNull(key: String): HexKey? { - return try { +fun decodeEventIdAsHexOrNull(key: String): HexKey? = + try { when (val parsed = Nip19Bech32.uriToRoute(key)?.entity) { is Nip19Bech32.NSec -> null is Nip19Bech32.NPub -> null @@ -309,7 +344,6 @@ fun decodeEventIdAsHexOrNull(key: String): HexKey? { if (e is CancellationException) throw e null } -} fun TlvBuilder.addString( type: Nip19Bech32.TlvTypes, From 7a22b3df716221745c6fa50a09143b691d0c26da Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 21 Jun 2024 12:51:16 -0400 Subject: [PATCH 02/12] Realigning reactions --- .../amethyst/ui/actions/CrossfadeIfEnabled.kt | 86 ++++++- .../amethyst/ui/components/ClickableBox.kt | 26 +- .../amethyst/ui/note/ReactionsRow.kt | 236 ++++++++++-------- .../vitorpamplona/amethyst/ui/theme/Theme.kt | 44 ---- 4 files changed, 241 insertions(+), 151 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/CrossfadeIfEnabled.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/CrossfadeIfEnabled.kt index c0f20510c..232b31a20 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/CrossfadeIfEnabled.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/CrossfadeIfEnabled.kt @@ -20,12 +20,22 @@ */ package com.vitorpamplona.amethyst.ui.actions -import androidx.compose.animation.Crossfade +import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.FiniteAnimationSpec +import androidx.compose.animation.core.Transition +import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.tween +import androidx.compose.animation.core.updateTransition import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.util.fastForEach import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -33,16 +43,86 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel fun CrossfadeIfEnabled( targetState: T, modifier: Modifier = Modifier, + contentAlignment: Alignment = Alignment.TopStart, animationSpec: FiniteAnimationSpec = tween(), label: String = "Crossfade", accountViewModel: AccountViewModel, content: @Composable (T) -> Unit, ) { if (accountViewModel.settings.featureSet == FeatureSetType.PERFORMANCE) { - Box(modifier) { + Box(modifier, contentAlignment) { content(targetState) } } else { - Crossfade(targetState, modifier, animationSpec, label, content) + MyCrossfade(targetState, modifier, contentAlignment, animationSpec, label, content) + } +} + +@OptIn(ExperimentalAnimationApi::class) +@Composable +fun MyCrossfade( + targetState: T, + modifier: Modifier = Modifier, + contentAlignment: Alignment = Alignment.TopStart, + animationSpec: FiniteAnimationSpec = tween(), + label: String = "Crossfade", + content: @Composable (T) -> Unit, +) { + val transition = updateTransition(targetState, label) + transition.MyCrossfade(modifier, contentAlignment, animationSpec, content = content) +} + +@ExperimentalAnimationApi +@Composable +fun Transition.MyCrossfade( + modifier: Modifier = Modifier, + contentAlignment: Alignment = Alignment.TopStart, + animationSpec: FiniteAnimationSpec = tween(), + contentKey: (targetState: T) -> Any? = { it }, + content: @Composable (targetState: T) -> Unit, +) { + val currentlyVisible = remember { mutableStateListOf().apply { add(currentState) } } + val contentMap = + remember { + mutableMapOf Unit>() + } + if (currentState == targetState) { + // If not animating, just display the current state + if (currentlyVisible.size != 1 || currentlyVisible[0] != targetState) { + // Remove all the intermediate items from the list once the animation is finished. + currentlyVisible.removeAll { it != targetState } + contentMap.clear() + } + } + if (!contentMap.contains(targetState)) { + // Replace target with the same key if any + val replacementId = + currentlyVisible.indexOfFirst { + contentKey(it) == contentKey(targetState) + } + if (replacementId == -1) { + currentlyVisible.add(targetState) + } else { + currentlyVisible[replacementId] = targetState + } + contentMap.clear() + currentlyVisible.fastForEach { stateForContent -> + contentMap[stateForContent] = { + val alpha by animateFloat( + transitionSpec = { animationSpec }, + ) { if (it == stateForContent) 1f else 0f } + Box(Modifier.graphicsLayer { this.alpha = alpha }, contentAlignment) { + content(stateForContent) + } + } + } + } + + Box(modifier, contentAlignment) { + currentlyVisible.fastForEach { + key(contentKey(it)) { + contentMap[it]?.invoke() + } + } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableBox.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableBox.kt index 57410903e..b8e522911 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableBox.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableBox.kt @@ -20,7 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.components +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.material.ripple.rememberRipple @@ -33,7 +35,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size24dp @Composable fun ClickableBox( - modifier: Modifier, + modifier: Modifier = Modifier, onClick: () -> Unit, content: @Composable () -> Unit, ) { @@ -49,3 +51,25 @@ fun ClickableBox( content() } } + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun ClickableBox( + modifier: Modifier = Modifier, + onClick: () -> Unit, + onLongClick: () -> Unit, + content: @Composable () -> Unit, +) { + Box( + modifier.combinedClickable( + role = Role.Button, + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false, radius = Size24dp), + onClick = onClick, + onLongClick = onLongClick, + ), + contentAlignment = Alignment.Center, + ) { + content() + } +} 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 e75103ce7..91f2d25de 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 @@ -35,7 +35,6 @@ import androidx.compose.animation.slideOutVertically import androidx.compose.animation.togetherWith import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement @@ -49,13 +48,15 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Cancel import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.Icon import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ProgressIndicatorDefaults @@ -85,6 +86,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.TextUnit @@ -112,6 +114,7 @@ import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.DarkerGreen import com.vitorpamplona.amethyst.ui.theme.Font14SP import com.vitorpamplona.amethyst.ui.theme.HalfDoubleVertSpacer +import com.vitorpamplona.amethyst.ui.theme.HalfPadding import com.vitorpamplona.amethyst.ui.theme.ModifierWidth3dp import com.vitorpamplona.amethyst.ui.theme.NoSoTinyBorders import com.vitorpamplona.amethyst.ui.theme.ReactionRowExpandButton @@ -127,6 +130,8 @@ import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.Size20dp import com.vitorpamplona.amethyst.ui.theme.Size22Modifier import com.vitorpamplona.amethyst.ui.theme.Size24dp +import com.vitorpamplona.amethyst.ui.theme.SmallBorder +import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.TinyBorders import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink import com.vitorpamplona.amethyst.ui.theme.placeholderText @@ -135,6 +140,7 @@ import com.vitorpamplona.quartz.events.BaseTextNoteEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableSet import kotlinx.coroutines.Dispatchers @@ -831,29 +837,21 @@ fun LikeReaction( var wantsToChangeReactionSymbol by remember { mutableStateOf(false) } var wantsToReact by remember { mutableStateOf(false) } - Box( - contentAlignment = Center, - modifier = - Modifier - .combinedClickable( - role = Role.Button, - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false, radius = Size24dp), - onClick = { - likeClick( - accountViewModel, - baseNote, - onMultipleChoices = { wantsToReact = true }, - onWantsToSignReaction = { accountViewModel.reactToOrDelete(baseNote) }, - ) - }, - onLongClick = { wantsToChangeReactionSymbol = true }, - ), + ClickableBox( + onClick = { + likeClick( + accountViewModel, + baseNote, + onMultipleChoices = { wantsToReact = true }, + onWantsToSignReaction = { accountViewModel.reactToOrDelete(baseNote) }, + ) + }, + onLongClick = { wantsToChangeReactionSymbol = true }, ) { ObserveLikeIcon(baseNote, accountViewModel) { reactionType -> - CrossfadeIfEnabled(targetState = reactionType, label = "LikeIcon", accountViewModel = accountViewModel) { - if (it != null) { - RenderReactionType(it, heartSizeModifier, iconFontSize) + CrossfadeIfEnabled(targetState = reactionType, contentAlignment = Center, label = "LikeIcon", accountViewModel = accountViewModel) { + if (reactionType != null) { + RenderReactionType(reactionType, heartSizeModifier, iconFontSize) } else { LikeIcon(heartSizeModifier, grayTint) } @@ -1320,6 +1318,7 @@ fun ReactionChoicePopup( val account = accountState?.account ?: return val toRemove = remember { baseNote.reactedBy(account.userProfile()).toImmutableSet() } + val reactions = remember { account.reactionChoices.toImmutableList() } val iconSizePx = with(LocalDensity.current) { -iconSize.toPx().toInt() } @@ -1328,108 +1327,139 @@ fun ReactionChoicePopup( offset = IntOffset(0, iconSizePx), onDismissRequest = { onDismiss() }, ) { + ReactionChoicePopupContent( + reactions, + toRemove = toRemove, + onClick = { reactionType -> + accountViewModel.reactToOrDelete( + baseNote, + reactionType, + ) + onDismiss() + }, + onChangeAmount, + ) + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun ReactionChoicePopupContent( + listOfReactions: ImmutableList, + toRemove: ImmutableSet, + onClick: (reactionType: String) -> Unit, + onChangeAmount: () -> Unit, +) { + Box(HalfPadding, contentAlignment = Center) { ElevatedCard( - Modifier - .border(width = 1.dp, color = MaterialTheme.colorScheme.outline, shape = RoundedCornerShape(5.dp)), + shape = SmallBorder, elevation = CardDefaults.elevatedCardElevation(defaultElevation = 8.dp), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), ) { - Box(modifier = Modifier.padding(5.dp)) { - FlowRow(horizontalArrangement = Arrangement.Center) { - account.reactionChoices.forEach { reactionType -> - ActionableReactionButton( - baseNote, - reactionType, - accountViewModel, - onDismiss, - onChangeAmount, - toRemove, - ) - } + FlowRow( + modifier = HalfPadding, + horizontalArrangement = Arrangement.Center, + verticalArrangement = Arrangement.Center, + ) { + listOfReactions.forEach { reactionType -> + ActionableReactionButton( + reactionType = reactionType, + onClick = { onClick(reactionType) }, + onChangeAmount = onChangeAmount, + toRemove = toRemove, + ) } } } } } +@Preview() +@Composable +fun ReactionChoicePopupPeeview() { + ThemeComparisonColumn { + ReactionChoicePopupContent( + persistentListOf( + "\uD83D\uDE80", + "\uD83E\uDEC2", + "\uD83D\uDC40", + "\uD83D\uDE02", + "\uD83C\uDF89", + "\uD83E\uDD14", + "\uD83D\uDE31", + "\uD83E\uDD14", + "\uD83D\uDE31", + "+", + "-", + ), + onClick = {}, + onChangeAmount = {}, + toRemove = persistentSetOf(), + ) + } +} + @Composable @OptIn(ExperimentalFoundationApi::class) private fun ActionableReactionButton( - baseNote: Note, reactionType: String, - accountViewModel: AccountViewModel, - onDismiss: () -> Unit, + onClick: () -> Unit, onChangeAmount: () -> Unit, toRemove: ImmutableSet, ) { val thisModifier = - remember(reactionType) { - Modifier - .padding(horizontal = 3.dp) - .combinedClickable( - onClick = { - accountViewModel.reactToOrDelete( - baseNote, - reactionType, - ) - onDismiss() - }, - onLongClick = { onChangeAmount() }, - ) - .padding(5.dp) - } - - val removeSymbol = - remember(reactionType) { - if (reactionType in toRemove) { - " ✖" - } else { - "" - } - } - - if (reactionType.startsWith(":")) { - val noStartColon = reactionType.removePrefix(":") - val url = noStartColon.substringAfter(":") - - val renderable = - persistentListOf( - Nip30CustomEmoji.ImageUrlType(url), - Nip30CustomEmoji.TextType(removeSymbol), + Modifier + .padding(horizontal = 8.dp, vertical = 5.dp) + .height(Size20dp) + .combinedClickable( + role = Role.Button, + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false, radius = Size24dp), + onClick = onClick, + onLongClick = onChangeAmount, ) - InLineIconRenderer( - renderable, - style = SpanStyle(color = Color.White), - maxLines = 1, - modifier = thisModifier, - ) - } else { - when (reactionType) { - "+" -> { - LikedIcon(modifier = thisModifier.size(16.dp), tint = Color.White) + Row(thisModifier, verticalAlignment = Alignment.CenterVertically) { + if (reactionType.startsWith(":")) { + val noStartColon = reactionType.removePrefix(":") + val url = noStartColon.substringAfter(":") - Text( - text = removeSymbol, - color = Color.White, - textAlign = TextAlign.Center, - modifier = thisModifier, - ) + InLineIconRenderer( + persistentListOf( + Nip30CustomEmoji.ImageUrlType(url), + ), + style = SpanStyle(color = MaterialTheme.colorScheme.onBackground), + maxLines = 1, + ) + } else { + when (reactionType) { + "+" -> { + LikedIcon(modifier = Modifier.size(20.dp)) + } + + "-" -> { + Text( + text = "\uD83D\uDC4E", + color = MaterialTheme.colorScheme.onBackground, + maxLines = 1, + ) + } + else -> { + Text( + "$reactionType", + color = MaterialTheme.colorScheme.onBackground, + maxLines = 1, + ) + } } - "-" -> - 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, - ) + } + + if (reactionType in toRemove) { + Icon( + imageVector = Icons.Default.Cancel, + contentDescription = null, + modifier = Modifier.padding(start = 2.dp).size(14.dp), + ) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt index 440a16c1e..14ae768ab 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt @@ -94,18 +94,12 @@ private val LightLessImportantLink = LightColorPalette.primary.copy(alpha = 0.52 private val DarkMediumImportantLink = DarkColorPalette.primary.copy(alpha = 0.32f) private val LightMediumImportantLink = LightColorPalette.primary.copy(alpha = 0.32f) -private val DarkVeryImportantLink = DarkColorPalette.primary.copy(alpha = 0.12f) -private val LightVeryImportantLink = LightColorPalette.primary.copy(alpha = 0.12f) - private val DarkGrayText = DarkColorPalette.onSurface.copy(alpha = 0.52f) private val LightGrayText = LightColorPalette.onSurface.copy(alpha = 0.52f) private val DarkPlaceholderText = DarkColorPalette.onSurface.copy(alpha = 0.32f) private val LightPlaceholderText = LightColorPalette.onSurface.copy(alpha = 0.32f) -private val DarkPlaceholderTextColorFilter = ColorFilter.tint(DarkPlaceholderText) -private val LightPlaceholderTextColorFilter = ColorFilter.tint(LightPlaceholderText) - private val DarkOnBackgroundColorFilter = ColorFilter.tint(DarkColorPalette.onBackground) private val LightOnBackgroundColorFilter = ColorFilter.tint(LightColorPalette.onBackground) @@ -115,20 +109,9 @@ private val LightSubtleButton = LightColorPalette.onSurface.copy(alpha = 0.22f) private val DarkSubtleBorder = DarkColorPalette.onSurface.copy(alpha = 0.12f) private val LightSubtleBorder = LightColorPalette.onSurface.copy(alpha = 0.12f) -private val DarkReplyItemBackground = DarkColorPalette.onSurface.copy(alpha = 0.05f) -private val LightReplyItemBackground = LightColorPalette.onSurface.copy(alpha = 0.05f) - -private val DarkZapraiserBackground = - BitcoinOrange.copy(0.52f).compositeOver(DarkColorPalette.background) -private val LightZapraiserBackground = - BitcoinOrange.copy(0.52f).compositeOver(LightColorPalette.background) - private val DarkOverPictureBackground = DarkColorPalette.background.copy(0.62f) private val LightOverPictureBackground = LightColorPalette.background.copy(0.62f) -val RepostPictureBorderDark = Modifier.border(2.dp, DarkColorPalette.background, CircleShape) -val RepostPictureBorderLight = Modifier.border(2.dp, LightColorPalette.background, CircleShape) - val DarkImageModifier = Modifier .fillMaxWidth() @@ -225,16 +208,6 @@ val DarkLargeRelayIconModifier = .size(Size55dp) .clip(shape = CircleShape) -val LightBottomIconModifier = - Modifier - .size(Size10dp) - .clip(shape = CircleShape) - -val DarkBottomIconModifier = - Modifier - .size(Size10dp) - .clip(shape = CircleShape) - val RichTextDefaults = RichTextStyle().resolveDefaults() val MarkDownStyleOnDark = @@ -319,9 +292,6 @@ val ColorScheme.isLight: Boolean val ColorScheme.newItemBackgroundColor: Color get() = if (isLight) LightNewItemBackground else DarkNewItemBackground -val ColorScheme.replyBackground: Color - get() = if (isLight) LightReplyItemBackground else DarkReplyItemBackground - val ColorScheme.selectedNote: Color get() = if (isLight) LightSelectedNote else DarkSelectedNote @@ -331,13 +301,8 @@ val ColorScheme.secondaryButtonBackground: Color val ColorScheme.lessImportantLink: Color get() = if (isLight) LightLessImportantLink else DarkLessImportantLink -val ColorScheme.zapraiserBackground: Color - get() = if (isLight) LightZapraiserBackground else DarkZapraiserBackground - val ColorScheme.mediumImportanceLink: Color get() = if (isLight) LightMediumImportantLink else DarkMediumImportantLink -val ColorScheme.veryImportantLink: Color - get() = if (isLight) LightVeryImportantLink else DarkVeryImportantLink val ColorScheme.placeholderText: Color get() = if (isLight) LightPlaceholderText else DarkPlaceholderText @@ -345,9 +310,6 @@ val ColorScheme.placeholderText: Color val ColorScheme.nip05: Color get() = if (isLight) Nip05EmailColorLight else Nip05EmailColorDark -val ColorScheme.placeholderTextColorFilter: ColorFilter - get() = if (isLight) LightPlaceholderTextColorFilter else DarkPlaceholderTextColorFilter - val ColorScheme.onBackgroundColorFilter: ColorFilter get() = if (isLight) LightOnBackgroundColorFilter else DarkOnBackgroundColorFilter @@ -375,9 +337,6 @@ val ColorScheme.allGoodColor: Color val ColorScheme.markdownStyle: RichTextStyle get() = if (isLight) MarkDownStyleOnLight else MarkDownStyleOnDark -val ColorScheme.repostProfileBorder: Modifier - get() = if (isLight) RepostPictureBorderLight else RepostPictureBorderDark - val ColorScheme.imageModifier: Modifier get() = if (isLight) LightImageModifier else DarkImageModifier @@ -402,9 +361,6 @@ val ColorScheme.relayIconModifier: Modifier val ColorScheme.largeRelayIconModifier: Modifier get() = if (isLight) LightLargeRelayIconModifier else DarkLargeRelayIconModifier -val ColorScheme.bottomIconModifier: Modifier - get() = if (isLight) LightBottomIconModifier else DarkBottomIconModifier - val ColorScheme.chartStyle: ChartStyle get() { val defaultColors = if (isLight) DefaultColors.Light else DefaultColors.Dark From d2f06f53107598a2fb6db08776c722246ef7576c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 21 Jun 2024 13:18:48 -0400 Subject: [PATCH 03/12] Removing the X to delete reaction in favor of a Selected background --- .../amethyst/ui/note/ReactionsRow.kt | 98 ++++++++----------- .../ui/note/UpdateReactionTypeDialog.kt | 17 ++-- .../vitorpamplona/amethyst/ui/theme/Shape.kt | 6 ++ .../vitorpamplona/amethyst/ui/theme/Theme.kt | 20 ++++ 4 files changed, 77 insertions(+), 64 deletions(-) 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 91f2d25de..b37b34612 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 @@ -48,15 +48,12 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Cancel import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.Icon import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ProgressIndicatorDefaults @@ -135,6 +132,8 @@ import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.TinyBorders import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.amethyst.ui.theme.reactionBox +import com.vitorpamplona.amethyst.ui.theme.selectedReactionBoxModifier import com.vitorpamplona.quartz.encoders.Nip30CustomEmoji import com.vitorpamplona.quartz.events.BaseTextNoteEvent import kotlinx.collections.immutable.ImmutableList @@ -1305,7 +1304,6 @@ private fun BoostTypeChoicePopup( } } -@OptIn(ExperimentalLayoutApi::class) @Composable fun ReactionChoicePopup( baseNote: Note, @@ -1357,7 +1355,7 @@ fun ReactionChoicePopupContent( colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), ) { FlowRow( - modifier = HalfPadding, + modifier = Modifier.padding(horizontal = 3.dp), horizontalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center, ) { @@ -1394,72 +1392,60 @@ fun ReactionChoicePopupPeeview() { ), onClick = {}, onChangeAmount = {}, - toRemove = persistentSetOf(), + toRemove = persistentSetOf("\uD83D\uDE80"), ) } } @Composable -@OptIn(ExperimentalFoundationApi::class) private fun ActionableReactionButton( reactionType: String, onClick: () -> Unit, onChangeAmount: () -> Unit, toRemove: ImmutableSet, ) { - val thisModifier = - Modifier - .padding(horizontal = 8.dp, vertical = 5.dp) - .height(Size20dp) - .combinedClickable( - role = Role.Button, - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false, radius = Size24dp), - onClick = onClick, - onLongClick = onChangeAmount, - ) + ClickableBox( + modifier = if (reactionType in toRemove) MaterialTheme.colorScheme.selectedReactionBoxModifier else reactionBox, + onClick = onClick, + onLongClick = onChangeAmount, + ) { + RenderReaction(reactionType) + } +} - Row(thisModifier, verticalAlignment = Alignment.CenterVertically) { - if (reactionType.startsWith(":")) { - val noStartColon = reactionType.removePrefix(":") - val url = noStartColon.substringAfter(":") +@Composable +fun RenderReaction(reactionType: String) { + if (reactionType.startsWith(":")) { + val noStartColon = reactionType.removePrefix(":") + val url = noStartColon.substringAfter(":") - InLineIconRenderer( - persistentListOf( - Nip30CustomEmoji.ImageUrlType(url), - ), - style = SpanStyle(color = MaterialTheme.colorScheme.onBackground), - maxLines = 1, - ) - } else { - when (reactionType) { - "+" -> { - LikedIcon(modifier = Modifier.size(20.dp)) - } - - "-" -> { - Text( - text = "\uD83D\uDC4E", - color = MaterialTheme.colorScheme.onBackground, - maxLines = 1, - ) - } - else -> { - Text( - "$reactionType", - color = MaterialTheme.colorScheme.onBackground, - maxLines = 1, - ) - } + InLineIconRenderer( + persistentListOf( + Nip30CustomEmoji.ImageUrlType(url), + ), + style = SpanStyle(color = MaterialTheme.colorScheme.onBackground), + maxLines = 1, + ) + } else { + when (reactionType) { + "+" -> { + LikedIcon(modifier = Size20Modifier) } - } - if (reactionType in toRemove) { - Icon( - imageVector = Icons.Default.Cancel, - contentDescription = null, - modifier = Modifier.padding(start = 2.dp).size(14.dp), - ) + "-" -> { + Text( + text = "\uD83D\uDC4E", + color = MaterialTheme.colorScheme.onBackground, + maxLines = 1, + ) + } + else -> { + Text( + reactionType, + color = MaterialTheme.colorScheme.onBackground, + maxLines = 1, + ) + } } } } 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 ce474e245..27df7ae23 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 @@ -277,8 +277,7 @@ private fun RenderReactionOption( width = 1.dp, color = MaterialTheme.colorScheme.surfaceDim, shape = RoundedCornerShape(8.dp), - ) - .padding(8.dp), + ).padding(8.dp), ) { if (reactionType.startsWith(":")) { val noStartColon = reactionType.removePrefix(":") @@ -298,13 +297,15 @@ private fun RenderReactionOption( } else { when (reactionType) { "+" -> { - LikedIcon(modifier = Modifier.size(16.dp), tint = MaterialTheme.colorScheme.onBackground) + Row { + LikedIcon(modifier = Modifier.size(20.dp), tint = Color.Unspecified) - Text( - text = " ✖", - color = MaterialTheme.colorScheme.onBackground, - textAlign = TextAlign.Center, - ) + Text( + text = " ✖", + color = MaterialTheme.colorScheme.onBackground, + textAlign = TextAlign.Center, + ) + } } "-" -> Text( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt index dd4d233a6..1d98d30a0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt @@ -268,3 +268,9 @@ val hashVerifierMark = Modifier.width(40.dp).height(40.dp).padding(10.dp) val noteComposeRelayBox = Modifier.width(55.dp).heightIn(min = 17.dp).padding(start = 2.dp, end = 1.dp) val previewCardImageModifier = Modifier.fillMaxWidth().heightIn(max = 200.dp).padding(bottom = 5.dp) + +val reactionBox = + Modifier + .padding(horizontal = 3.dp, vertical = 6.dp) + .height(Size30dp) + .padding(5.dp) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt index 14ae768ab..95521724d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt @@ -28,6 +28,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape @@ -162,6 +163,22 @@ val LightInnerPostBorderModifier = .clip(shape = QuoteBorder) .border(1.dp, LightSubtleBorder, QuoteBorder) +val DarkSelectedReactionBoxModifier = + Modifier + .padding(horizontal = 3.dp, vertical = 6.dp) + .height(Size30dp) + .clip(shape = SmallBorder) + .background(DarkColorPalette.secondaryContainer) + .padding(5.dp) + +val LightSelectedReactionBoxModifier = + Modifier + .padding(horizontal = 3.dp, vertical = 6.dp) + .height(Size30dp) + .clip(shape = SmallBorder) + .background(LightColorPalette.secondaryContainer) + .padding(5.dp) + val DarkChannelNotePictureModifier = Modifier .size(30.dp) @@ -361,6 +378,9 @@ val ColorScheme.relayIconModifier: Modifier val ColorScheme.largeRelayIconModifier: Modifier get() = if (isLight) LightLargeRelayIconModifier else DarkLargeRelayIconModifier +val ColorScheme.selectedReactionBoxModifier: Modifier + get() = if (isLight) LightSelectedReactionBoxModifier else DarkSelectedReactionBoxModifier + val ColorScheme.chartStyle: ChartStyle get() { val defaultColors = if (isLight) DefaultColors.Light else DefaultColors.Dark From e86f0c54f3db91f14cc7735076047e06671cc1aa Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 21 Jun 2024 17:20:26 +0000 Subject: [PATCH 04/12] New Crowdin translations by GitHub Action --- app/src/main/res/values-hi-rIN/strings.xml | 2 +- app/src/main/res/values-zh-rCN/strings.xml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-hi-rIN/strings.xml b/app/src/main/res/values-hi-rIN/strings.xml index 9284d4216..b978d5c61 100644 --- a/app/src/main/res/values-hi-rIN/strings.xml +++ b/app/src/main/res/values-hi-rIN/strings.xml @@ -451,7 +451,7 @@ कभी नहीं सम्पूर्ण सरलीकृत - प्रदर्शन + वेगवान यन्त्रव्यवस्था प्रकाशवान अन्धकारमय diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 775ddbecf..b0fd146d6 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -451,6 +451,7 @@ 从不 完整版 简化版 + 极速版 系统 浅色 深色 From 8a1aefeda7fb16729e4290ed743442d53bda3aa0 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 21 Jun 2024 13:31:48 -0400 Subject: [PATCH 05/12] Adds an option to change reactions from the popup --- .../com/vitorpamplona/amethyst/ui/note/Icons.kt | 17 ++++++++++++++++- .../amethyst/ui/note/ReactionsRow.kt | 10 +++++++++- app/src/main/res/values/strings.xml | 2 ++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt index 905f36d81..46107c4f0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt @@ -31,6 +31,7 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.OpenInNew import androidx.compose.material.icons.automirrored.filled.VolumeOff import androidx.compose.material.icons.automirrored.filled.VolumeUp +import androidx.compose.material.icons.filled.AddReaction import androidx.compose.material.icons.filled.Bolt import androidx.compose.material.icons.filled.Cancel import androidx.compose.material.icons.filled.Clear @@ -44,6 +45,7 @@ import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.PushPin import androidx.compose.material.icons.filled.Report import androidx.compose.material.icons.filled.Share +import androidx.compose.material.icons.outlined.AddReaction import androidx.compose.material.icons.outlined.ArrowForwardIos import androidx.compose.material.icons.outlined.Bolt import androidx.compose.material.icons.outlined.PlayCircle @@ -164,7 +166,20 @@ fun LikedIcon( ) { Icon( imageVector = Liked, - stringRes(id = R.string.like_description), + contentDescription = stringRes(id = R.string.like_description), + modifier = modifier, + tint = tint, + ) +} + +@Composable +fun ChangeReactionIcon( + modifier: Modifier, + tint: Color = Color.Unspecified, +) { + Icon( + imageVector = Icons.Outlined.AddReaction, + contentDescription = stringRes(id = R.string.change_reaction), modifier = modifier, tint = tint, ) 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 b37b34612..26c4416e5 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 @@ -822,7 +822,6 @@ fun BoostText( SlidingAnimationCount(boostState, grayTint, accountViewModel) } -@OptIn(ExperimentalFoundationApi::class) @Composable fun LikeReaction( baseNote: Note, @@ -1367,6 +1366,10 @@ fun ReactionChoicePopupContent( toRemove = toRemove, ) } + + ClickableBox(modifier = reactionBox, onClick = onChangeAmount) { + ChangeReactionIcon(modifier = Size20Modifier, MaterialTheme.colorScheme.placeholderText) + } } } } @@ -1389,6 +1392,11 @@ fun ReactionChoicePopupPeeview() { "\uD83D\uDE31", "+", "-", + "\uD83C\uDF89", + "\uD83E\uDD14", + "\uD83D\uDE31", + "\uD83E\uDD14", + "\uD83D\uDE31", ), onClick = {}, onChangeAmount = {}, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b3e0e6f41..1130f841e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -785,6 +785,8 @@ Like Zap + Change Quick Reactions + Profile Picture of %1$s Relay %1$s Expand relay list From f8a77d634c9afc87337a253836a548a97d85fe80 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 21 Jun 2024 15:05:41 -0400 Subject: [PATCH 06/12] Adds a check to see if the receiver has NIP-17 relays setup and if so activates NIP-17 message. --- .../amethyst/ui/actions/NewPostViewModel.kt | 146 ++++++++++++------ 1 file changed, 102 insertions(+), 44 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index 854d96a96..2172bafa2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -53,6 +53,7 @@ import com.vitorpamplona.quartz.encoders.Hex import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.toNpub import com.vitorpamplona.quartz.events.AddressableEvent +import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent import com.vitorpamplona.quartz.events.BaseTextNoteEvent import com.vitorpamplona.quartz.events.ChatMessageEvent import com.vitorpamplona.quartz.events.ClassifiedsEvent @@ -88,7 +89,7 @@ enum class UserSuggestionAnchor { } @Stable -open class NewPostViewModel() : ViewModel() { +open class NewPostViewModel : ViewModel() { var draftTag: String by mutableStateOf(UUID.randomUUID().toString()) var accountViewModel: AccountViewModel? = null @@ -175,17 +176,11 @@ open class NewPostViewModel() : ViewModel() { val draftTextChanges = Channel(Channel.CONFLATED) - fun lnAddress(): String? { - return account?.userProfile()?.info?.lnAddress() - } + fun lnAddress(): String? = account?.userProfile()?.info?.lnAddress() - fun hasLnAddress(): Boolean { - return account?.userProfile()?.info?.lnAddress() != null - } + fun hasLnAddress(): Boolean = account?.userProfile()?.info?.lnAddress() != null - fun user(): User? { - return account?.userProfile() - } + fun user(): User? = account?.userProfile() open fun load( accountViewModel: AccountViewModel, @@ -368,15 +363,21 @@ open class NewPostViewModel() : ViewModel() { } originalNote = - draftEvent.tags().filter { it.size > 1 && (it[0] == "e" || it[0] == "a") && it.getOrNull(3) == "reply" }.map { - LocalCache.checkGetOrCreateNote(it[1]) - }.firstOrNull() + draftEvent + .tags() + .filter { it.size > 1 && (it[0] == "e" || it[0] == "a") && it.getOrNull(3) == "reply" } + .map { + LocalCache.checkGetOrCreateNote(it[1]) + }.firstOrNull() if (originalNote == null) { originalNote = - draftEvent.tags().filter { it.size > 1 && (it[0] == "e" || it[0] == "a") && it.getOrNull(3) == "root" }.map { - LocalCache.checkGetOrCreateNote(it[1]) - }.firstOrNull() + draftEvent + .tags() + .filter { it.size > 1 && (it[0] == "e" || it[0] == "a") && it.getOrNull(3) == "root" } + .map { + LocalCache.checkGetOrCreateNote(it[1]) + }.firstOrNull() } canUsePoll = originalNote?.event !is PrivateDmEvent && originalNote?.channelHex() == null @@ -403,12 +404,45 @@ open class NewPostViewModel() : ViewModel() { wantsProduct = draftEvent.kind() == 30402 - title = TextFieldValue(draftEvent.tags().filter { it.size > 1 && it[0] == "title" }.map { it[1] }?.firstOrNull() ?: "") - price = TextFieldValue(draftEvent.tags().filter { it.size > 1 && it[0] == "price" }.map { it[1] }?.firstOrNull() ?: "") - category = TextFieldValue(draftEvent.tags().filter { it.size > 1 && it[0] == "t" }.map { it[1] }?.firstOrNull() ?: "") - locationText = TextFieldValue(draftEvent.tags().filter { it.size > 1 && it[0] == "location" }.map { it[1] }?.firstOrNull() ?: "") + title = + TextFieldValue( + draftEvent + .tags() + .filter { it.size > 1 && it[0] == "title" } + .map { it[1] } + ?.firstOrNull() ?: "", + ) + price = + TextFieldValue( + draftEvent + .tags() + .filter { it.size > 1 && it[0] == "price" } + .map { it[1] } + ?.firstOrNull() ?: "", + ) + category = + TextFieldValue( + draftEvent + .tags() + .filter { it.size > 1 && it[0] == "t" } + .map { it[1] } + ?.firstOrNull() ?: "", + ) + locationText = + TextFieldValue( + draftEvent + .tags() + .filter { it.size > 1 && it[0] == "location" } + .map { it[1] } + ?.firstOrNull() ?: "", + ) condition = ClassifiedsEvent.CONDITION.entries.firstOrNull { - it.value == draftEvent.tags().filter { it.size > 1 && it[0] == "condition" }.map { it[1] }.firstOrNull() + it.value == + draftEvent + .tags() + .filter { it.size > 1 && it[0] == "condition" } + .map { it[1] } + .firstOrNull() } ?: ClassifiedsEvent.CONDITION.USED_LIKE_NEW wantsDirectMessage = draftEvent is PrivateDmEvent || draftEvent is ChatMessageEvent @@ -479,7 +513,8 @@ open class NewPostViewModel() : ViewModel() { if (split.percentage > 0.00001) { val homeRelay = accountViewModel?.getRelayListFor(split.key)?.writeRelays()?.firstOrNull() - ?: split.key.relaysBeingUsed.keys.firstOrNull { !it.contains("localhost") } + ?: split.key.relaysBeingUsed.keys + .firstOrNull { !it.contains("localhost") } ZapSplitSetup( lnAddressOrPubKeyHex = split.key.pubkeyHex, @@ -851,9 +886,7 @@ open class NewPostViewModel() : ViewModel() { } } - open fun findUrlInMessage(): String? { - return RichTextParser().parseValidUrls(message.text).firstOrNull() - } + open fun findUrlInMessage(): String? = RichTextParser().parseValidUrls(message.text).firstOrNull() open fun removeFromReplyList(userToRemove: User) { pTags = pTags?.filter { it != userToRemove } @@ -869,14 +902,18 @@ open class NewPostViewModel() : ViewModel() { if (it.selection.collapsed) { val lastWord = - it.text.substring(0, it.selection.end).substringAfterLast("\n").substringAfterLast(" ") + it.text + .substring(0, it.selection.end) + .substringAfterLast("\n") + .substringAfterLast(" ") userSuggestionAnchor = it.selection userSuggestionsMainMessage = UserSuggestionAnchor.MAIN_MESSAGE if (lastWord.startsWith("@") && lastWord.length > 2) { NostrSearchEventOrUserDataSource.search(lastWord.removePrefix("@")) viewModelScope.launch(Dispatchers.IO) { userSuggestions = - LocalCache.findUsersStartingWith(lastWord.removePrefix("@")) + LocalCache + .findUsersStartingWith(lastWord.removePrefix("@")) .sortedWith(compareBy({ account?.isFollowing(it) }, { it.toBestDisplayName() }, { it.pubkeyHex })) .reversed() } @@ -894,14 +931,18 @@ open class NewPostViewModel() : ViewModel() { if (it.selection.collapsed) { val lastWord = - it.text.substring(0, it.selection.end).substringAfterLast("\n").substringAfterLast(" ") + it.text + .substring(0, it.selection.end) + .substringAfterLast("\n") + .substringAfterLast(" ") userSuggestionAnchor = it.selection userSuggestionsMainMessage = UserSuggestionAnchor.TO_USERS if (lastWord.startsWith("@") && lastWord.length > 2) { NostrSearchEventOrUserDataSource.search(lastWord.removePrefix("@")) viewModelScope.launch(Dispatchers.IO) { userSuggestions = - LocalCache.findUsersStartingWith(lastWord.removePrefix("@")) + LocalCache + .findUsersStartingWith(lastWord.removePrefix("@")) .sortedWith(compareBy({ account?.isFollowing(it) }, { it.toBestDisplayName() }, { it.pubkeyHex })) .reversed() } @@ -928,15 +969,15 @@ open class NewPostViewModel() : ViewModel() { NostrSearchEventOrUserDataSource.search(lastWord.removePrefix("@")) viewModelScope.launch(Dispatchers.IO) { userSuggestions = - LocalCache.findUsersStartingWith(lastWord.removePrefix("@")) + LocalCache + .findUsersStartingWith(lastWord.removePrefix("@")) .sortedWith( compareBy( { account?.isFollowing(it) }, { it.toBestDisplayName() }, { it.pubkeyHex }, ), - ) - .reversed() + ).reversed() } } else { NostrSearchEventOrUserDataSource.clear() @@ -949,7 +990,10 @@ open class NewPostViewModel() : ViewModel() { userSuggestionAnchor?.let { if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) { val lastWord = - message.text.substring(0, it.end).substringAfterLast("\n").substringAfterLast(" ") + message.text + .substring(0, it.end) + .substringAfterLast("\n") + .substringAfterLast(" ") val lastWordStart = it.end - lastWord.length val wordToInsert = "@${item.pubkeyNpub()}" @@ -963,7 +1007,10 @@ open class NewPostViewModel() : ViewModel() { forwardZapToEditting = TextFieldValue("") } else if (userSuggestionsMainMessage == UserSuggestionAnchor.TO_USERS) { val lastWord = - toUsers.text.substring(0, it.end).substringAfterLast("\n").substringAfterLast(" ") + toUsers.text + .substring(0, it.end) + .substringAfterLast("\n") + .substringAfterLast(" ") val lastWordStart = it.end - lastWord.length val wordToInsert = "@${item.pubkeyNpub()}" @@ -972,6 +1019,9 @@ open class NewPostViewModel() : ViewModel() { toUsers.text.replaceRange(lastWordStart, it.end, wordToInsert), TextRange(lastWordStart + wordToInsert.length, lastWordStart + wordToInsert.length), ) + + val relayList = (LocalCache.getAddressableNoteIfExists(AdvertisedRelayListEvent.createAddressTag(item.pubkeyHex))?.event as? AdvertisedRelayListEvent)?.readRelays() + nip17 = relayList != null } userSuggestionAnchor = null @@ -982,12 +1032,10 @@ open class NewPostViewModel() : ViewModel() { saveDraft() } - private fun newStateMapPollOptions(): SnapshotStateMap { - return mutableStateMapOf(Pair(0, ""), Pair(1, "")) - } + private fun newStateMapPollOptions(): SnapshotStateMap = mutableStateMapOf(Pair(0, ""), Pair(1, "")) - fun canPost(): Boolean { - return message.text.isNotBlank() && + fun canPost(): Boolean = + message.text.isNotBlank() && !isUploadingImage && !wantsInvoice && (!wantsZapraiser || zapRaiserAmount != null) && @@ -1009,7 +1057,6 @@ open class NewPostViewModel() : ViewModel() { ) ) && contentToAddUrl == null - } suspend fun createNIP94Record( uploadingResult: Nip96Uploader.PartialEvent, @@ -1020,11 +1067,20 @@ open class NewPostViewModel() : ViewModel() { // Images don't seem to be ready immediately after upload val imageUrl = uploadingResult.tags?.firstOrNull { it.size > 1 && it[0] == "url" }?.get(1) val remoteMimeType = - uploadingResult.tags?.firstOrNull { it.size > 1 && it[0] == "m" }?.get(1)?.ifBlank { null } + uploadingResult.tags + ?.firstOrNull { it.size > 1 && it[0] == "m" } + ?.get(1) + ?.ifBlank { null } val originalHash = - uploadingResult.tags?.firstOrNull { it.size > 1 && it[0] == "ox" }?.get(1)?.ifBlank { null } + uploadingResult.tags + ?.firstOrNull { it.size > 1 && it[0] == "ox" } + ?.get(1) + ?.ifBlank { null } val dim = - uploadingResult.tags?.firstOrNull { it.size > 1 && it[0] == "dim" }?.get(1)?.ifBlank { null } + uploadingResult.tags + ?.firstOrNull { it.size > 1 && it[0] == "dim" } + ?.get(1) + ?.ifBlank { null } val magnet = uploadingResult.tags ?.firstOrNull { it.size > 1 && it[0] == "magnet" } @@ -1270,7 +1326,9 @@ open class NewPostViewModel() : ViewModel() { } } -enum class GeohashPrecision(val digits: Int) { +enum class GeohashPrecision( + val digits: Int, +) { KM_5000_X_5000(1), // 5,000km × 5,000km KM_1250_X_625(2), // 1,250km × 625km KM_156_X_156(3), // 156km × 156km From f7c60b374561bd1c811177d16c235923d7d7a5e2 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 21 Jun 2024 17:04:00 -0400 Subject: [PATCH 07/12] Making Metered Wifi behave like Mobile for heavy content. --- .../main/java/com/vitorpamplona/amethyst/model/Settings.kt | 2 +- .../main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt | 6 ++++-- .../com/vitorpamplona/amethyst/ui/components/TextSpinner.kt | 6 +++++- app/src/main/res/values/strings.xml | 1 + 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Settings.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Settings.kt index f1c433264..25798635a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Settings.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Settings.kt @@ -62,7 +62,7 @@ enum class ConnectivityType( val resourceId: Int, ) { ALWAYS(null, 0, R.string.connectivity_type_always), - WIFI_ONLY(true, 1, R.string.connectivity_type_wifi_only), + WIFI_ONLY(true, 1, R.string.connectivity_type_unmetered_wifi_only), NEVER(false, 2, R.string.connectivity_type_never), } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index ba1ead032..f59c2a2bd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -186,8 +186,10 @@ class MainActivity : AppCompatActivity() { } fun updateNetworkCapabilities(networkCapabilities: NetworkCapabilities): Boolean { - val isOnMobileData = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) - val isOnWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) + val unmetered = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) + + val isOnMobileData = !unmetered || networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) + val isOnWifi = unmetered && networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) var changedNetwork = false diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt index aceca0ecf..0dbc381c0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt @@ -77,6 +77,7 @@ fun TextSpinner( readOnly = true, label = { label?.let { Text(it) } }, modifier = modifier, + singleLine = true, ) } } @@ -200,4 +201,7 @@ fun SpinnerSelectionDialog( } } -@Immutable data class TitleExplainer(val title: String, val explainer: String? = null) +@Immutable data class TitleExplainer( + val title: String, + val explainer: String? = null, +) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1130f841e..63ed8c01e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -511,6 +511,7 @@ Settings Always Wifi-only + Unmetered WiFi Never Complete From a14ab59e7877b79924ae30205da924529a264374 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 21 Jun 2024 18:38:31 -0400 Subject: [PATCH 08/12] Converts LiveData for Content-Sensitivity and Transitive Hidden Users into Flow to avoid locking the main thread while scrolling. Requests flows on the Default thread. --- .../amethyst/LocalPreferences.kt | 6 +-- .../vitorpamplona/amethyst/model/Account.kt | 44 ++++++++++------ .../amethyst/service/relays/Client.kt | 22 +++----- .../ui/components/SensitivityWarning.kt | 21 +++++--- .../ui/dal/HiddenAccountsFeedFilter.kt | 51 ++++++++----------- .../amethyst/ui/navigation/AppTopBar.kt | 3 ++ .../amethyst/ui/note/elements/DropDownMenu.kt | 2 +- .../ui/screen/loggedIn/AccountViewModel.kt | 24 ++++----- 8 files changed, 88 insertions(+), 85 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 3b21ef66d..f0d8e2e98 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -328,10 +328,10 @@ object LocalPreferences { ) putStringSet(PrefKeys.HAS_DONATED_IN_VERSION, account.hasDonatedInVersion) - if (account.showSensitiveContent == null) { + if (account.showSensitiveContent.value == null) { remove(PrefKeys.SHOW_SENSITIVE_CONTENT) } else { - putBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, account.showSensitiveContent!!) + putBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, account.showSensitiveContent.value!!) } putString( @@ -597,7 +597,7 @@ object LocalPreferences { backupContactList = latestContactList, proxy = proxy, proxyPort = proxyPort, - showSensitiveContent = showSensitiveContent, + showSensitiveContent = MutableStateFlow(showSensitiveContent), warnAboutPostsWithReports = warnAboutReports, filterSpamFromStrangers = filterSpam, lastReadPerRoute = lastReadPerRoute, 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 81a9f568e..c9b1ecf34 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -26,7 +26,6 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.core.os.ConfigurationCompat import androidx.lifecycle.LiveData -import androidx.lifecycle.asFlow import androidx.lifecycle.asLiveData import androidx.lifecycle.liveData import androidx.lifecycle.switchMap @@ -194,7 +193,7 @@ class Account( var backupContactList: ContactListEvent? = null, var proxy: Proxy? = null, var proxyPort: Int = 9050, - var showSensitiveContent: Boolean? = null, + var showSensitiveContent: MutableStateFlow = MutableStateFlow(null), var warnAboutPostsWithReports: Boolean = true, var filterSpamFromStrangers: Boolean = true, var lastReadPerRoute: Map = mapOf(), @@ -202,7 +201,7 @@ class Account( var pendingAttestations: MutableStateFlow> = MutableStateFlow>(mapOf()), val scope: CoroutineScope = Amethyst.instance.applicationIOScope, ) { - var transientHiddenUsers: Set = setOf() + var transientHiddenUsers: MutableStateFlow> = MutableStateFlow(setOf()) data class PaymentRequest( val relayUrl: String, @@ -238,6 +237,8 @@ class Account( getPrivateOutboxRelayListFlow(), userProfile().flow().relays.stateFlow, ) { nip65RelayList, dmRelayList, searchRelayList, privateOutBox, userProfile -> + checkNotInMainThread() + val baseRelaySet = activeRelays() ?: convertLocalRelays() val newDMRelaySet = (dmRelayList.note.event as? ChatMessageRelayListEvent)?.relays()?.map { RelayUrlFormatter.normalize(it) }?.toSet() ?: emptySet() val searchRelaySet = (searchRelayList.note.event as? SearchRelayListEvent)?.relays()?.map { RelayUrlFormatter.normalize(it) }?.toSet() ?: Constants.defaultSearchRelaySet @@ -358,6 +359,7 @@ class Account( @OptIn(ExperimentalCoroutinesApi::class) val liveKind3FollowsFlow: Flow = userProfile().flow().follows.stateFlow.transformLatest { + checkNotInMainThread() emit( LiveFollowLists( it.user.cachedFollowingKeySet(), @@ -394,6 +396,7 @@ class Account( peopleListFollowsSource: Flow, ): Flow = combineTransform(kind3FollowsSource, peopleListFollowsSource) { kind3Follows, peopleListFollows -> + checkNotInMainThread() if (peopleListFollows.listName == GLOBAL_FOLLOWS) { emit(null) } else if (peopleListFollows.listName == KIND3_FOLLOWS) { @@ -500,10 +503,11 @@ class Account( val flowHiddenUsers: StateFlow by lazy { combineTransform( - live.asFlow(), + transientHiddenUsers, + showSensitiveContent, getBlockListNote().flow().metadata.stateFlow, getMuteListNote().flow().metadata.stateFlow, - ) { localLive, blockList, muteList -> + ) { transientHiddenUsers, showSensitiveContent, blockList, muteList -> checkNotInMainThread() val resultBlockList = @@ -532,8 +536,8 @@ class Account( LiveHiddenUsers( hiddenUsers = (resultBlockList.users + resultMuteList.users), hiddenWords = hiddenWords, - spammers = localLive.account.transientHiddenUsers, - showSensitiveContent = localLive.account.showSensitiveContent, + spammers = transientHiddenUsers, + showSensitiveContent = showSensitiveContent, ), ) }.stateIn( @@ -542,8 +546,8 @@ class Account( LiveHiddenUsers( hiddenUsers = setOf(), hiddenWords = setOf(), - spammers = transientHiddenUsers, - showSensitiveContent = showSensitiveContent, + spammers = transientHiddenUsers.value, + showSensitiveContent = showSensitiveContent.value, ), ) } @@ -596,7 +600,9 @@ class Account( filterSpamFromStrangers = filterSpam LocalCache.antiSpam.active = filterSpamFromStrangers if (!filterSpamFromStrangers) { - transientHiddenUsers = setOf() + transientHiddenUsers.update { + emptySet() + } } live.invalidateData() saveable.invalidateData() @@ -2377,7 +2383,9 @@ class Account( } } - transientHiddenUsers = (transientHiddenUsers - pubkeyHex) + transientHiddenUsers.update { + it - pubkeyHex + } live.invalidateData() saveable.invalidateData() } @@ -2889,7 +2897,9 @@ class Account( } fun updateShowSensitiveContent(show: Boolean?) { - showSensitiveContent = show + showSensitiveContent.update { + show + } saveable.invalidateData() live.invalidateData() } @@ -2928,10 +2938,12 @@ class Account( // imports transient blocks due to spam. LocalCache.antiSpam.liveSpam.observeForever { GlobalScope.launch(Dispatchers.IO) { - it.cache.spamMessages.snapshot().values.forEach { - if (it.pubkeyHex !in transientHiddenUsers && it.duplicatedMessages.size >= 5) { - if (it.pubkeyHex != userProfile().pubkeyHex && it.pubkeyHex !in followingKeySet()) { - transientHiddenUsers = (transientHiddenUsers + it.pubkeyHex) + it.cache.spamMessages.snapshot().values.forEach { spammer -> + if (spammer.pubkeyHex !in transientHiddenUsers.value && spammer.duplicatedMessages.size >= 5) { + if (spammer.pubkeyHex != userProfile().pubkeyHex && spammer.pubkeyHex !in followingKeySet()) { + transientHiddenUsers.update { + it + spammer.pubkeyHex + } live.invalidateData() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt index 22d449c4c..edfe68d00 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt @@ -179,9 +179,7 @@ object Client : RelayPool.Listener { subscriptions = subscriptions.minus(subscriptionId) } - fun isActive(subscriptionId: String): Boolean { - return subscriptions.contains(subscriptionId) - } + fun isActive(subscriptionId: String): Boolean = subscriptions.contains(subscriptionId) @OptIn(DelicateCoroutinesApi::class) override fun onEvent( @@ -205,9 +203,9 @@ object Client : RelayPool.Listener { ) { // Releases the Web thread for the new payload. // May need to add a processing queue if processing new events become too costly. - GlobalScope.launch(Dispatchers.Default) { - listeners.forEach { it.onRelayStateChange(type, relay, channel) } - } + // GlobalScope.launch(Dispatchers.Default) { + listeners.forEach { it.onRelayStateChange(type, relay, channel) } + // } } @OptIn(DelicateCoroutinesApi::class) @@ -249,21 +247,15 @@ object Client : RelayPool.Listener { listeners = listeners.plus(listener) } - fun isSubscribed(listener: Listener): Boolean { - return listeners.contains(listener) - } + fun isSubscribed(listener: Listener): Boolean = listeners.contains(listener) fun unsubscribe(listener: Listener) { listeners = listeners.minus(listener) } - fun allSubscriptions(): Map> { - return subscriptions - } + fun allSubscriptions(): Map> = subscriptions - fun getSubscriptionFilters(subId: String): List { - return subscriptions[subId] ?: emptyList() - } + fun getSubscriptionFilters(subId: String): List = subscriptions[subId] ?: emptyList() abstract class Listener { /** A new message was received */ diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt index 3866f3447..0dfafb94e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt @@ -39,7 +39,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable 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 @@ -50,6 +49,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled @@ -97,10 +97,9 @@ fun SensitivityWarning( accountViewModel: AccountViewModel, content: @Composable () -> Unit, ) { - val accountState by accountViewModel.accountLiveData.observeAsState() + val accountState = accountViewModel.account.showSensitiveContent.collectAsStateWithLifecycle() - var showContentWarningNote by - remember(accountState) { mutableStateOf(accountState?.account?.showSensitiveContent != true) } + var showContentWarningNote by remember(accountState) { mutableStateOf(accountState.value != true) } CrossfadeIfEnabled(targetState = showContentWarningNote, accountViewModel = accountViewModel) { if (it) { @@ -118,18 +117,26 @@ fun ContentWarningNote(onDismiss: () -> Unit) { Column(modifier = Modifier.padding(start = 10.dp)) { Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { Box( - Modifier.height(80.dp).width(90.dp), + Modifier + .height(80.dp) + .width(90.dp), ) { Icon( imageVector = Icons.Default.Visibility, contentDescription = stringRes(R.string.content_warning), - modifier = Modifier.size(70.dp).align(Alignment.BottomStart), + modifier = + Modifier + .size(70.dp) + .align(Alignment.BottomStart), tint = MaterialTheme.colorScheme.onBackground, ) Icon( imageVector = Icons.Rounded.Warning, contentDescription = stringRes(R.string.content_warning), - modifier = Modifier.size(30.dp).align(Alignment.TopEnd), + modifier = + Modifier + .size(30.dp) + .align(Alignment.TopEnd), tint = MaterialTheme.colorScheme.onBackground, ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt index 58fedc1d0..1a04da166 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt @@ -26,17 +26,15 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User import kotlinx.coroutines.CancellationException -class HiddenAccountsFeedFilter(val account: Account) : FeedFilter() { - override fun feedKey(): String { - return account.userProfile().pubkeyHex - } +class HiddenAccountsFeedFilter( + val account: Account, +) : FeedFilter() { + override fun feedKey(): String = account.userProfile().pubkeyHex - override fun showHiddenKey(): Boolean { - return true - } + override fun showHiddenKey(): Boolean = true - override fun feed(): List { - return account.flowHiddenUsers.value.hiddenUsers.reversed().mapNotNull { + override fun feed(): List = + account.flowHiddenUsers.value.hiddenUsers.reversed().mapNotNull { try { LocalCache.getOrCreateUser(it) } catch (e: Exception) { @@ -45,33 +43,26 @@ class HiddenAccountsFeedFilter(val account: Account) : FeedFilter() { null } } - } } -class HiddenWordsFeedFilter(val account: Account) : FeedFilter() { - override fun feedKey(): String { - return account.userProfile().pubkeyHex - } +class HiddenWordsFeedFilter( + val account: Account, +) : FeedFilter() { + override fun feedKey(): String = account.userProfile().pubkeyHex - override fun showHiddenKey(): Boolean { - return true - } + override fun showHiddenKey(): Boolean = true - override fun feed(): List { - return account.flowHiddenUsers.value.hiddenWords.toList() - } + override fun feed(): List = + account.flowHiddenUsers.value.hiddenWords + .toList() } -class SpammerAccountsFeedFilter(val account: Account) : FeedFilter() { - override fun feedKey(): String { - return account.userProfile().pubkeyHex - } +class SpammerAccountsFeedFilter( + val account: Account, +) : FeedFilter() { + override fun feedKey(): String = account.userProfile().pubkeyHex - override fun showHiddenKey(): Boolean { - return true - } + override fun showHiddenKey(): Boolean = true - override fun feed(): List { - return (account.transientHiddenUsers).map { LocalCache.getOrCreateUser(it) } - } + override fun feed(): List = account.transientHiddenUsers.value.map { LocalCache.getOrCreateUser(it) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt index f8faac59e..82aabddcc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt @@ -97,6 +97,7 @@ import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource import com.vitorpamplona.amethyst.service.NostrThreadDataSource import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource import com.vitorpamplona.amethyst.service.NostrVideoDataSource +import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.RelayPool import com.vitorpamplona.amethyst.ui.components.LoadNote @@ -758,6 +759,7 @@ class FollowListViewModel( private val _kind3GlobalPeopleRoutes = combineTransform(livePeopleListsFlow, liveKind3FollowsFlow) { myLivePeopleListsFlow, myLiveKind3FollowsFlow -> + checkNotInMainThread() emit( listOf(listOf(kind3Follow, globalFollow), myLivePeopleListsFlow, myLiveKind3FollowsFlow, listOf(muteListFollow)) .flatten() @@ -768,6 +770,7 @@ class FollowListViewModel( private val _kind3GlobalPeople = combineTransform(livePeopleListsFlow, liveKind3FollowsFlow) { myLivePeopleListsFlow, myLiveKind3FollowsFlow -> + checkNotInMainThread() emit( listOf(listOf(kind3Follow, globalFollow), myLivePeopleListsFlow, listOf(muteListFollow)) .flatten() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt index ffa6e8a74..374f8bc8a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt @@ -414,7 +414,7 @@ fun WatchBookmarksFollowsAndAccount( isPublicBookmarkNote = accountViewModel.isInPublicBookmarks(note), isLoggedUser = accountViewModel.isLoggedUser(note.author), isSensitive = note.event?.isSensitive() ?: false, - showSensitiveContent = showSensitiveContent, + showSensitiveContent = showSensitiveContent.value, ) launch(Dispatchers.Main) { 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 23371bf3c..f24d669cd 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 @@ -111,6 +111,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combineTransform +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.joinAll @@ -308,22 +309,19 @@ class AccountViewModel( noteIsHiddenFlows.get(note) ?: combineTransform( account.flowHiddenUsers, - account.liveKind3FollowsFlow, + account.liveKind3Follows, note.flow().metadata.stateFlow, note.flow().reports.stateFlow, ) { hiddenUsers, followingUsers, metadata, reports -> - val isAcceptable = - withContext(Dispatchers.IO) { - isNoteAcceptable(metadata.note, hiddenUsers, followingUsers.users) - } - emit(isAcceptable) - }.stateIn( - viewModelScope, - SharingStarted.Eagerly, - NoteComposeReportState(), - ).also { - noteIsHiddenFlows.put(note, it) - } + emit(isNoteAcceptable(metadata.note, hiddenUsers, followingUsers.users)) + }.flowOn(Dispatchers.Default) + .stateIn( + viewModelScope, + SharingStarted.Eagerly, + NoteComposeReportState(), + ).also { + noteIsHiddenFlows.put(note, it) + } private val noteMustShowExpandButtonFlows = LruCache>(300) From 08da97e36d633607ec26112f5624a6d563fb2156 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 21 Jun 2024 18:39:27 -0400 Subject: [PATCH 09/12] Updates version of FirebaseBom --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 78eae2453..f7ae275e1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ coil = "2.6.0" composeBom = "2024.06.00" coreKtx = "1.13.1" espressoCore = "3.5.1" -firebaseBom = "33.1.0" +firebaseBom = "33.1.1" fragmentKtx = "1.8.0" gms = "4.4.1" jacksonModuleKotlin = "2.17.1" From ed80511cb68d0a73dbb62eb78253da5c94a1538f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 21 Jun 2024 19:02:10 -0400 Subject: [PATCH 10/12] v0.88.3 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index cb9b24d5d..ab54c26a5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 34 - versionCode 379 - versionName "0.88.2" + versionCode 380 + versionName "0.88.3" buildConfigField "String", "RELEASE_NOTES_ID", "\"2a34cbadd03212c8162e1ff896ba12641821088a2ec8d5e40d54aa80c0510800\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" From d6764e08fdf06c0f90a08b40f822e6ba179113e3 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 21 Jun 2024 19:53:02 -0400 Subject: [PATCH 11/12] Bigger reaction panel --- .../amethyst/ui/navigation/AppTopBar.kt | 6 ++++-- .../vitorpamplona/amethyst/ui/note/ReactionsRow.kt | 13 +++++++++---- .../com/vitorpamplona/amethyst/ui/theme/Shape.kt | 5 +++-- .../com/vitorpamplona/amethyst/ui/theme/Theme.kt | 9 ++++----- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt index 82aabddcc..5f3311940 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt @@ -146,10 +146,12 @@ import com.vitorpamplona.quartz.events.PeopleListEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combineTransform +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transformLatest @@ -766,7 +768,7 @@ class FollowListViewModel( .toImmutableList(), ) } - val kind3GlobalPeopleRoutes = _kind3GlobalPeopleRoutes.stateIn(viewModelScope, SharingStarted.Eagerly, defaultLists) + val kind3GlobalPeopleRoutes = _kind3GlobalPeopleRoutes.flowOn(Dispatchers.IO).stateIn(viewModelScope, SharingStarted.Eagerly, defaultLists) private val _kind3GlobalPeople = combineTransform(livePeopleListsFlow, liveKind3FollowsFlow) { myLivePeopleListsFlow, myLiveKind3FollowsFlow -> @@ -778,7 +780,7 @@ class FollowListViewModel( ) } - val kind3GlobalPeople = _kind3GlobalPeople.stateIn(viewModelScope, SharingStarted.Eagerly, defaultLists) + val kind3GlobalPeople = _kind3GlobalPeople.flowOn(Dispatchers.IO).stateIn(viewModelScope, SharingStarted.Eagerly, defaultLists) class Factory( val account: Account, 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 26c4416e5..ad91195e3 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 @@ -88,6 +88,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Popup import androidx.core.content.ContextCompat import androidx.lifecycle.LiveData @@ -127,6 +128,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.Size20dp import com.vitorpamplona.amethyst.ui.theme.Size22Modifier import com.vitorpamplona.amethyst.ui.theme.Size24dp +import com.vitorpamplona.amethyst.ui.theme.Size28Modifier import com.vitorpamplona.amethyst.ui.theme.SmallBorder import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.TinyBorders @@ -1354,7 +1356,7 @@ fun ReactionChoicePopupContent( colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), ) { FlowRow( - modifier = Modifier.padding(horizontal = 3.dp), + modifier = Modifier.padding(horizontal = 5.dp, vertical = 5.dp), horizontalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center, ) { @@ -1368,7 +1370,7 @@ fun ReactionChoicePopupContent( } ClickableBox(modifier = reactionBox, onClick = onChangeAmount) { - ChangeReactionIcon(modifier = Size20Modifier, MaterialTheme.colorScheme.placeholderText) + ChangeReactionIcon(modifier = Size28Modifier, MaterialTheme.colorScheme.placeholderText) } } } @@ -1400,7 +1402,7 @@ fun ReactionChoicePopupPeeview() { ), onClick = {}, onChangeAmount = {}, - toRemove = persistentSetOf("\uD83D\uDE80"), + toRemove = persistentSetOf("\uD83D\uDE80", "\uD83E\uDD14", "\uD83D\uDE31"), ) } } @@ -1433,11 +1435,12 @@ fun RenderReaction(reactionType: String) { ), style = SpanStyle(color = MaterialTheme.colorScheme.onBackground), maxLines = 1, + fontSize = 22.sp, ) } else { when (reactionType) { "+" -> { - LikedIcon(modifier = Size20Modifier) + LikedIcon(modifier = Size28Modifier) } "-" -> { @@ -1445,6 +1448,7 @@ fun RenderReaction(reactionType: String) { text = "\uD83D\uDC4E", color = MaterialTheme.colorScheme.onBackground, maxLines = 1, + fontSize = 22.sp, ) } else -> { @@ -1452,6 +1456,7 @@ fun RenderReaction(reactionType: String) { reactionType, color = MaterialTheme.colorScheme.onBackground, maxLines = 1, + fontSize = 22.sp, ) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt index 1d98d30a0..2af199445 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt @@ -135,6 +135,7 @@ val Size22Modifier = Modifier.size(22.dp) val Size24Modifier = Modifier.size(24.dp) val Size25Modifier = Modifier.size(25.dp) val Size26Modifier = Modifier.size(26.dp) +val Size28Modifier = Modifier.size(28.dp) val Size30Modifier = Modifier.size(30.dp) val Size35Modifier = Modifier.size(35.dp) val Size39Modifier = Modifier.size(39.dp) @@ -271,6 +272,6 @@ val previewCardImageModifier = Modifier.fillMaxWidth().heightIn(max = 200.dp).pa val reactionBox = Modifier - .padding(horizontal = 3.dp, vertical = 6.dp) - .height(Size30dp) + .padding(horizontal = 6.dp, vertical = 6.dp) + .size(Size40dp) .padding(5.dp) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt index 95521724d..ef220986b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt @@ -28,7 +28,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape @@ -165,16 +164,16 @@ val LightInnerPostBorderModifier = val DarkSelectedReactionBoxModifier = Modifier - .padding(horizontal = 3.dp, vertical = 6.dp) - .height(Size30dp) + .padding(horizontal = 5.dp, vertical = 5.dp) + .size(Size40dp) .clip(shape = SmallBorder) .background(DarkColorPalette.secondaryContainer) .padding(5.dp) val LightSelectedReactionBoxModifier = Modifier - .padding(horizontal = 3.dp, vertical = 6.dp) - .height(Size30dp) + .padding(horizontal = 5.dp, vertical = 5.dp) + .size(Size40dp) .clip(shape = SmallBorder) .background(LightColorPalette.secondaryContainer) .padding(5.dp) From c37adfc6a2d72a0721b30e99a88ea813a80de63e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 21 Jun 2024 19:53:49 -0400 Subject: [PATCH 12/12] v0.88.4 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index ab54c26a5..8f9b63997 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 34 - versionCode 380 - versionName "0.88.3" + versionCode 381 + versionName "0.88.4" buildConfigField "String", "RELEASE_NOTES_ID", "\"2a34cbadd03212c8162e1ff896ba12641821088a2ec8d5e40d54aa80c0510800\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"