diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 0fc311313..217e5c51f 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/README.md b/README.md index f5374fa5e..6030feb40 100644 --- a/README.md +++ b/README.md @@ -60,12 +60,12 @@ Or get the latest APK from the [Releases Section](https://github.com/vitorpamplo - [x] Audio Tracks (zapstr.live) (NIP-TBD) - [x] Push Notifications (Zaps and Messages) - [x] Generic Tags (NIP-12) +- [x] Sensitive Content (NIP-36) - [ ] Marketplace (NIP-15) - [ ] Image/Video Capture in the app - [ ] Local Database - [ ] View Individual Reactions (Like, Boost, Zaps, Reports) per Post - [ ] Bookmarks, Pinned Posts, Muted Events (NIP-51) -- [ ] Sensitive Content (NIP-36) - [ ] Relay Pages (NIP-11) - [ ] Proof of Work in the Phone (NIP-13, NIP-20) - [ ] Events with a Subject (NIP-14) diff --git a/app/build.gradle b/app/build.gradle index bf17bcf25..6f2fd2298 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 33 - versionCode 172 - versionName "0.50.4" + versionCode 182 + versionName "0.52.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -24,6 +24,7 @@ android { buildTypes { release { + // TODO: Make sure all of JSON parsers work when activating these. //minifyEnabled true //proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' resValue "string", "app_name", "@string/app_name_release" @@ -71,7 +72,7 @@ android { } composeOptions { - kotlinCompilerExtensionVersion "1.4.3" + kotlinCompilerExtensionVersion "1.4.7" } packagingOptions { @@ -82,12 +83,11 @@ android { lint { disable 'MissingTranslation' } - } dependencies { implementation 'androidx.core:core-ktx:1.10.1' - implementation 'androidx.activity:activity-compose:1.7.1' + implementation 'androidx.activity:activity-compose:1.7.2' implementation "androidx.compose.ui:ui:$compose_ui_version" implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version" @@ -128,17 +128,18 @@ dependencies { implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11' // Json Serialization TODO: We might need to converge between gson and Jackson (we are usin both) - implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.15.0' + implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.15.1' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' // link preview - implementation 'tw.com.oneup.www:Baha-UrlPreview:1.0.1' + implementation 'org.jsoup:jsoup:1.16.1' + //implementation 'tw.com.oneup.www:Baha-UrlPreview:1.0.1' // Encrypted Key Storage implementation 'androidx.security:security-crypto-ktx:1.1.0-alpha06' // view videos - implementation 'com.google.android.exoplayer:exoplayer:2.18.6' + implementation 'com.google.android.exoplayer:exoplayer:2.18.7' // Load images from the web. implementation "io.coil-kt:coil-compose:$coil_version" @@ -183,8 +184,11 @@ dependencies { implementation "com.patrykandpatrick.vico:views:${vico_version}" implementation "com.patrykandpatrick.vico:compose-m2:${vico_version}" + // immutable collections to avoid recomposition + implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5") + // Automatic memory leak detection - debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10' + debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.11' testImplementation 'junit:junit:4.13.2' testImplementation 'io.mockk:mockk:1.13.5' diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt index 5f4659d44..a8cb1e8d9 100644 --- a/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt @@ -3,7 +3,6 @@ package com.vitorpamplona.amethyst.ui.components import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.navigation.NavController import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable @@ -14,7 +13,7 @@ fun TranslatableRichTextViewer( tags: List>?, backgroundColor: Color, accountViewModel: AccountViewModel, - navController: NavController + nav: (String) -> Unit ) = ExpandableRichTextViewer( content, canPreview, @@ -22,5 +21,5 @@ fun TranslatableRichTextViewer( tags, backgroundColor, accountViewModel, - navController + nav ) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7ba5daf74..a386157e9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -35,6 +35,7 @@ android:name=".ui.MainActivity" android:exported="true" android:windowSoftInputMode="adjustResize" + android:configChanges="orientation|screenSize|screenLayout" android:theme="@style/Theme.Amethyst"> diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 55421207f..68547124e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -58,6 +58,7 @@ private object PrefKeys { const val HIDE_BLOCK_ALERT_DIALOG = "hide_block_alert_dialog" const val USE_PROXY = "use_proxy" const val PROXY_PORT = "proxy_port" + const val SHOW_SENSITIVE_CONTENT = "show_sensitive_content" val LAST_READ: (String) -> String = { route -> "last_read_route_$route" } } @@ -214,6 +215,12 @@ object LocalPreferences { putBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, account.hideBlockAlertDialog) putBoolean(PrefKeys.USE_PROXY, account.proxy != null) putInt(PrefKeys.PROXY_PORT, account.proxyPort) + + if (account.showSensitiveContent == null) { + remove(PrefKeys.SHOW_SENSITIVE_CONTENT) + } else { + putBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, account.showSensitiveContent!!) + } }.apply() } @@ -251,9 +258,9 @@ object LocalPreferences { ) ?: LnZapEvent.ZapType.PUBLIC val defaultFileServer = gson.fromJson( - getString(PrefKeys.DEFAULT_FILE_SERVER, "IMGUR"), + getString(PrefKeys.DEFAULT_FILE_SERVER, "NOSTR_BUILD"), object : TypeToken() {}.type - ) ?: ServersAvailable.IMGUR + ) ?: ServersAvailable.NOSTR_BUILD val zapPaymentRequestServer = try { getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)?.let { @@ -292,6 +299,12 @@ object LocalPreferences { val proxyPort = getInt(PrefKeys.PROXY_PORT, 9050) val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort) + val showSensitiveContent = if (contains(PrefKeys.SHOW_SENSITIVE_CONTENT)) { + getBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, false) + } else { + null + } + val a = Account( Persona(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray()), followingChannels, @@ -311,7 +324,8 @@ object LocalPreferences { hideBlockAlertDialog, latestContactList, proxy, - proxyPort + proxyPort, + showSensitiveContent ) return a diff --git a/app/src/main/java/com/vitorpamplona/amethyst/NotificationCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/NotificationCache.kt index 2bb193fba..40e815cfe 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/NotificationCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/NotificationCache.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch object NotificationCache { + // TODO: This must be account-based val lastReadByRoute = mutableMapOf() fun markAsRead(route: String, timestampInSecs: Long) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt b/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt index f283fa812..3f0eab359 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt @@ -15,7 +15,6 @@ import com.vitorpamplona.amethyst.service.NostrThreadDataSource import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource import com.vitorpamplona.amethyst.service.NostrVideoDataSource import com.vitorpamplona.amethyst.service.relays.Client -import com.vitorpamplona.amethyst.service.relays.Constants object ServiceManager { private var account: Account? = null @@ -46,9 +45,6 @@ object ServiceManager { NostrSingleEventDataSource.start() NostrSingleChannelDataSource.start() NostrSingleUserDataSource.start() - } else { - // if not logged in yet, start a basic service wit default relays - Client.connect(Constants.convertDefaultRelays()) } } 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 38d71ebda..91ac9fb5f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -2,6 +2,8 @@ package com.vitorpamplona.amethyst.model import android.content.res.Resources import android.util.Log +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable import androidx.core.os.ConfigurationCompat import androidx.lifecycle.LiveData import com.vitorpamplona.amethyst.service.FileHeader @@ -43,6 +45,7 @@ val GLOBAL_FOLLOWS = " Global " val KIND3_FOLLOWS = " All Follows " @OptIn(DelicateCoroutinesApi::class) +@Stable class Account( val loggedIn: Persona, var followingChannels: Set = DefaultChannels, @@ -53,7 +56,7 @@ class Account( var translateTo: String = Locale.getDefault().language, var zapAmountChoices: List = listOf(500L, 1000L, 5000L), var defaultZapType: LnZapEvent.ZapType = LnZapEvent.ZapType.PRIVATE, - var defaultFileServer: ServersAvailable = ServersAvailable.IMGUR, + var defaultFileServer: ServersAvailable = ServersAvailable.NOSTR_BUILD, var defaultHomeFollowList: String = KIND3_FOLLOWS, var defaultStoriesFollowList: String = GLOBAL_FOLLOWS, var defaultNotificationFollowList: String = GLOBAL_FOLLOWS, @@ -62,7 +65,8 @@ class Account( var hideBlockAlertDialog: Boolean = false, var backupContactList: ContactListEvent? = null, var proxy: Proxy?, - var proxyPort: Int + var proxyPort: Int, + var showSensitiveContent: Boolean? = null ) { var transientHiddenUsers: Set = setOf() @@ -217,7 +221,12 @@ class Account( zapPaymentRequest?.let { nip47 -> val event = LnZapPaymentRequestEvent.create(bolt11, nip47.pubKeyHex, nip47.secret?.hexToByteArray() ?: loggedIn.privKey!!) - val wcListener = NostrLnZapPaymentResponseDataSource(nip47.pubKeyHex, event.pubKey, event.id) + val wcListener = NostrLnZapPaymentResponseDataSource( + fromServiceHex = nip47.pubKeyHex, + toUserHex = event.pubKey, + replyingToHex = event.id, + authSigningKey = nip47.secret?.hexToByteArray() ?: loggedIn.privKey!! + ) wcListener.start() LocalCache.consume(event, zappedNote) { @@ -472,7 +481,14 @@ class Account( return LocalCache.notes[signedEvent.id] } - fun sendPost(message: String, replyTo: List?, mentions: List?, tags: List? = null, zapReceiver: String? = null) { + fun sendPost( + message: String, + replyTo: List?, + mentions: List?, + tags: List? = null, + zapReceiver: String? = null, + wantsToMarkAsSensitive: Boolean + ) { if (!isWriteable()) return val repliesToHex = replyTo?.filter { it.address() == null }?.map { it.idHex } @@ -486,6 +502,7 @@ class Account( addresses = addresses, extraTags = tags, zapReceiver = zapReceiver, + markAsSensitive = wantsToMarkAsSensitive, privateKey = loggedIn.privKey!! ) @@ -502,7 +519,8 @@ class Account( valueMinimum: Int?, consensusThreshold: Int?, closedAt: Int?, - zapReceiver: String? = null + zapReceiver: String? = null, + wantsToMarkAsSensitive: Boolean ) { if (!isWriteable()) return @@ -521,14 +539,15 @@ class Account( valueMinimum = valueMinimum, consensusThreshold = consensusThreshold, closedAt = closedAt, - zapReceiver = zapReceiver + zapReceiver = zapReceiver, + markAsSensitive = wantsToMarkAsSensitive ) // println("Sending new PollNoteEvent: %s".format(signedEvent.toJson())) Client.send(signedEvent) LocalCache.consume(signedEvent) } - fun sendChannelMessage(message: String, toChannel: String, replyTo: List?, mentions: List?, zapReceiver: String? = null) { + fun sendChannelMessage(message: String, toChannel: String, replyTo: List?, mentions: List?, zapReceiver: String? = null, wantsToMarkAsSensitive: Boolean) { if (!isWriteable()) return // val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null } @@ -541,13 +560,14 @@ class Account( replyTos = repliesToHex, mentions = mentionsHex, zapReceiver = zapReceiver, + markAsSensitive = wantsToMarkAsSensitive, privateKey = loggedIn.privKey!! ) Client.send(signedEvent) LocalCache.consume(signedEvent, null) } - fun sendPrivateMessage(message: String, toUser: String, replyingTo: Note? = null, mentions: List?, zapReceiver: String? = null) { + fun sendPrivateMessage(message: String, toUser: String, replyingTo: Note? = null, mentions: List?, zapReceiver: String? = null, wantsToMarkAsSensitive: Boolean) { if (!isWriteable()) return val user = LocalCache.users[toUser] ?: return @@ -561,6 +581,7 @@ class Account( replyTos = repliesToHex, mentions = mentionsHex, zapReceiver = zapReceiver, + markAsSensitive = wantsToMarkAsSensitive, privateKey = loggedIn.privKey!!, advertiseNip18 = false ) @@ -1106,6 +1127,12 @@ class Account( saveable.invalidateData() } + fun updateShowSensitiveContent(show: Boolean?) { + showSensitiveContent = show + saveable.invalidateData() + live.invalidateData() + } + fun registerObservers() { // Observes relays to restart connections userProfile().live().relays.observeForever { @@ -1161,4 +1188,5 @@ class AccountLiveData(private val account: Account) : LiveData(Acc } } +@Immutable class AccountState(val account: Account) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index e8920e69f..c6c691b15 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -1,5 +1,7 @@ package com.vitorpamplona.amethyst.model +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable import androidx.lifecycle.LiveData import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil @@ -20,6 +22,7 @@ import java.util.regex.Pattern val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]") +@Stable class AddressableNote(val address: ATag) : Note(address.toTag()) { override fun idNote() = address.toNAddr() override fun toNEvent() = address.toNAddr() @@ -28,6 +31,7 @@ class AddressableNote(val address: ATag) : Note(address.toTag()) { override fun createdAt() = (event as? LongTextNoteEvent)?.publishedAt() ?: event?.createdAt() } +@Stable open class Note(val idHex: String) { // These fields are only available after the Text Note event is received. // They are immutable after that. @@ -228,7 +232,7 @@ open class Note(val idHex: String) { fun isZappedBy(user: User, account: Account): Boolean { // Zaps who the requester was the user return zaps.any { - it.key.author === user || account.decryptZapContentAuthor(it.key)?.pubKey == user.pubkeyHex + it.key.author?.pubkeyHex == user.pubkeyHex || account.decryptZapContentAuthor(it.key)?.pubKey == user.pubkeyHex } || zapPayments.any { val zapResponseEvent = it.value?.event as? LnZapPaymentResponseEvent val response = if (zapResponseEvent != null) { @@ -241,11 +245,11 @@ open class Note(val idHex: String) { } fun isReactedBy(user: User): Boolean { - return reactions.any { it.author === user } + return reactions.any { it.author?.pubkeyHex == user.pubkeyHex } } fun isBoostedBy(user: User): Boolean { - return boosts.any { it.author === user } + return boosts.any { it.author?.pubkeyHex == user.pubkeyHex } } fun reportsBy(user: User): Set { @@ -439,4 +443,5 @@ class NoteLiveData(val note: Note) : LiveData(NoteState(note)) { } } +@Immutable class NoteState(val note: Note) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/UrlCachedPreviewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/UrlCachedPreviewer.kt index 833d1155b..e13a06396 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/UrlCachedPreviewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/UrlCachedPreviewer.kt @@ -1,8 +1,8 @@ package com.vitorpamplona.amethyst.model -import com.baha.url.preview.BahaUrlPreview -import com.baha.url.preview.IUrlPreviewCallback -import com.baha.url.preview.UrlInfoItem +import com.vitorpamplona.amethyst.service.previews.BahaUrlPreview +import com.vitorpamplona.amethyst.service.previews.IUrlPreviewCallback +import com.vitorpamplona.amethyst.service.previews.UrlInfoItem import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job 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 7d5877952..e5ff8b1a7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -1,5 +1,7 @@ package com.vitorpamplona.amethyst.model +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable import androidx.lifecycle.LiveData import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource import com.vitorpamplona.amethyst.service.model.BookmarkListEvent @@ -20,6 +22,7 @@ import java.util.regex.Pattern val lnurlpPattern = Pattern.compile("(?i:http|https):\\/\\/((.+)\\/)*\\.well-known\\/lnurlp\\/(.*)") +@Stable class User(val pubkeyHex: String) { var info: UserMetadata? = null @@ -296,7 +299,7 @@ class User(val pubkeyHex: String) { fun hasSentMessagesTo(user: User?): Boolean { val messagesToUser = privateChatrooms[user] ?: return false - return messagesToUser.roomMessages.any { this === it.author } + return messagesToUser.roomMessages.any { this.pubkeyHex == it.author?.pubkeyHex } } fun hasReport(loggedIn: User, type: ReportEvent.ReportType): Boolean { @@ -358,6 +361,7 @@ data class RelayInfo( data class Chatroom(var roomMessages: Set) +@Stable class UserMetadata { var name: String? = null var username: String? = null @@ -417,4 +421,5 @@ class UserLiveData(val user: User) : LiveData(UserState(user)) { } } +@Immutable class UserState(val user: User) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrLnZapPaymentResponseDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrLnZapPaymentResponseDataSource.kt index 57de8be6e..7993306f5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrLnZapPaymentResponseDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrLnZapPaymentResponseDataSource.kt @@ -1,14 +1,18 @@ package com.vitorpamplona.amethyst.service import com.vitorpamplona.amethyst.service.model.LnZapPaymentResponseEvent +import com.vitorpamplona.amethyst.service.model.RelayAuthEvent +import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.JsonFilter +import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.service.relays.TypedFilter class NostrLnZapPaymentResponseDataSource( - private var fromServiceHex: String, - private var toUserHex: String, - private var replyingToHex: String + private val fromServiceHex: String, + private val toUserHex: String, + private val replyingToHex: String, + private val authSigningKey: ByteArray ) : NostrDataSource("LnZapPaymentResponseFeed") { val feedTypes = setOf(FeedType.WALLET_CONNECT) @@ -36,4 +40,14 @@ class NostrLnZapPaymentResponseDataSource( channel.typedFilters = listOfNotNull(wc).ifEmpty { null } } + + override fun auth(relay: Relay, challenge: String) { + super.auth(relay, challenge) + + val event = RelayAuthEvent.create(relay.url, challenge, authSigningKey) + Client.send( + event, + relay.url + ) + } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt index 24d705835..10749bc4c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt @@ -1,6 +1,7 @@ package com.vitorpamplona.amethyst.service.model import android.util.Log +import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.hexToByteArray import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.service.nip19.Tlv @@ -9,6 +10,7 @@ import nostr.postr.Bech32 import nostr.postr.bechToBytes import nostr.postr.toByteArray +@Immutable data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String, val relay: String?) { fun toTag() = "$kind:$pubKeyHex:$dTag" diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/AudioTrackEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/AudioTrackEvent.kt index 82dc30aa0..e7c543dd2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/AudioTrackEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/AudioTrackEvent.kt @@ -1,10 +1,12 @@ package com.vitorpamplona.amethyst.service.model +import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.toHexKey import nostr.postr.Utils import java.util.Date +@Immutable class AudioTrackEvent( id: HexKey, pubKey: HexKey, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/BadgeAwardEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/BadgeAwardEvent.kt index c16abc20e..082322b11 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/BadgeAwardEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/BadgeAwardEvent.kt @@ -1,7 +1,9 @@ package com.vitorpamplona.amethyst.service.model +import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.HexKey +@Immutable class BadgeAwardEvent( id: HexKey, pubKey: HexKey, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/BadgeDefinitionEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/BadgeDefinitionEvent.kt index 5e8be5350..5a4d25c16 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/BadgeDefinitionEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/BadgeDefinitionEvent.kt @@ -1,7 +1,9 @@ package com.vitorpamplona.amethyst.service.model +import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.HexKey +@Immutable class BadgeDefinitionEvent( id: HexKey, pubKey: HexKey, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/BadgeProfilesEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/BadgeProfilesEvent.kt index f616bb7a4..3acb994f7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/BadgeProfilesEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/BadgeProfilesEvent.kt @@ -1,7 +1,9 @@ package com.vitorpamplona.amethyst.service.model +import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.HexKey +@Immutable class BadgeProfilesEvent( id: HexKey, pubKey: HexKey, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/BaseTextNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/BaseTextNoteEvent.kt index 93f576f05..201622544 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/BaseTextNoteEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/BaseTextNoteEvent.kt @@ -1,11 +1,13 @@ package com.vitorpamplona.amethyst.service.model import android.util.Log +import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.tagSearch import com.vitorpamplona.amethyst.service.nip19.Nip19 import com.vitorpamplona.amethyst.service.nip19.Nip19.nip19regex +@Immutable open class BaseTextNoteEvent( id: HexKey, pubKey: HexKey, @@ -19,6 +21,7 @@ open class BaseTextNoteEvent( open fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } private var citedUsersCache: Set? = null + private var citedNotesCache: Set? = null fun citedUsers(): Set { citedUsersCache?.let { return it } @@ -61,8 +64,10 @@ open class BaseTextNoteEvent( return returningList } - fun findCitations(): Set { - var citations = mutableSetOf() + fun findCitations(): Set { + citedNotesCache?.let { return it } + + val citations = mutableSetOf() // Removes citations from replies: val matcher = tagSearch.matcher(content) while (matcher.find()) { @@ -102,6 +107,7 @@ open class BaseTextNoteEvent( } } + citedNotesCache = citations return citations } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/BookmarkListEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/BookmarkListEvent.kt index 71f1ad7e6..d11b079a1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/BookmarkListEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/BookmarkListEvent.kt @@ -1,10 +1,12 @@ package com.vitorpamplona.amethyst.service.model +import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.toHexKey import nostr.postr.Utils import java.util.Date +@Immutable class BookmarkListEvent( id: HexKey, pubKey: HexKey, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelCreateEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelCreateEvent.kt index f9205b61a..8123144b7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelCreateEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelCreateEvent.kt @@ -1,11 +1,13 @@ package com.vitorpamplona.amethyst.service.model import android.util.Log +import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.toHexKey import nostr.postr.Utils import java.util.Date +@Immutable class ChannelCreateEvent( id: HexKey, pubKey: HexKey, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelHideMessageEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelHideMessageEvent.kt index 5de80f0b6..b2ed4c4b4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelHideMessageEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelHideMessageEvent.kt @@ -1,10 +1,12 @@ package com.vitorpamplona.amethyst.service.model +import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.toHexKey import nostr.postr.Utils import java.util.Date +@Immutable class ChannelHideMessageEvent( id: HexKey, pubKey: HexKey, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMessageEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMessageEvent.kt index 5fb98d1e3..f953c3045 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMessageEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMessageEvent.kt @@ -1,10 +1,12 @@ package com.vitorpamplona.amethyst.service.model +import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.toHexKey import nostr.postr.Utils import java.util.Date +@Immutable class ChannelMessageEvent( id: HexKey, pubKey: HexKey, @@ -20,7 +22,16 @@ class ChannelMessageEvent( companion object { const val kind = 42 - fun create(message: String, channel: String, replyTos: List? = null, mentions: List? = null, zapReceiver: String?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelMessageEvent { + fun create( + message: String, + channel: String, + replyTos: List? = null, + mentions: List? = null, + zapReceiver: String?, + privateKey: ByteArray, + createdAt: Long = Date().time / 1000, + markAsSensitive: Boolean + ): ChannelMessageEvent { val content = message val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags = mutableListOf( @@ -35,6 +46,9 @@ class ChannelMessageEvent( zapReceiver?.let { tags.add(listOf("zap", it)) } + if (markAsSensitive) { + tags.add(listOf("content-warning", "")) + } val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMetadataEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMetadataEvent.kt index 6382e588c..b3db7553e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMetadataEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMetadataEvent.kt @@ -1,11 +1,13 @@ package com.vitorpamplona.amethyst.service.model import android.util.Log +import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.toHexKey import nostr.postr.Utils import java.util.Date +@Immutable class ChannelMetadataEvent( id: HexKey, pubKey: HexKey, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMuteUserEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMuteUserEvent.kt index 79988b4de..626dbf529 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMuteUserEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMuteUserEvent.kt @@ -1,10 +1,12 @@ package com.vitorpamplona.amethyst.service.model +import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.toHexKey import nostr.postr.Utils import java.util.Date +@Immutable class ChannelMuteUserEvent( id: HexKey, pubKey: HexKey, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ContactListEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ContactListEvent.kt index d141c3fd8..b10099073 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ContactListEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ContactListEvent.kt @@ -1,6 +1,7 @@ package com.vitorpamplona.amethyst.service.model import android.util.Log +import androidx.compose.runtime.Immutable import com.google.gson.reflect.TypeToken import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.decodePublicKey @@ -8,6 +9,7 @@ import com.vitorpamplona.amethyst.model.toHexKey import nostr.postr.Utils import java.util.Date +@Immutable data class Contact(val pubKeyHex: String, val relayUri: String?) class ContactListEvent( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/DeletionEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/DeletionEvent.kt index 4d7170f61..9c3613569 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/DeletionEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/DeletionEvent.kt @@ -1,10 +1,12 @@ package com.vitorpamplona.amethyst.service.model +import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.toHexKey import nostr.postr.Utils import java.util.Date +@Immutable class DeletionEvent( id: HexKey, pubKey: HexKey, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt index 91b825e8d..946f0bcba 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt @@ -1,6 +1,7 @@ package com.vitorpamplona.amethyst.service.model import android.util.Log +import androidx.compose.runtime.Immutable import com.google.gson.* import com.google.gson.annotations.SerializedName import com.vitorpamplona.amethyst.model.HexKey @@ -14,6 +15,7 @@ import java.math.BigDecimal import java.security.MessageDigest import java.util.* +@Immutable open class Event( val id: HexKey, @SerializedName("pubkey") val pubKey: HexKey, @@ -46,6 +48,12 @@ open class Event( fun taggedUrls() = tags.filter { it.size > 1 && it[0] == "r" }.map { it[1] } + override fun isSensitive() = tags.any { + (it.size > 0 && it[0].equals("content-warning", true)) || + (it.size > 1 && it[0] == "t" && it[1].equals("nsfw", true)) || + (it.size > 1 && it[0] == "t" && it[1].equals("nude", true)) + } + override fun zapAddress() = tags.firstOrNull { it.size > 1 && it[0] == "zap" }?.get(1) fun taggedAddresses() = tags.filter { it.size > 1 && it[0] == "a" }.mapNotNull { @@ -285,6 +293,7 @@ open class Event( } } +@Immutable interface AddressableEvent { fun dTag(): String fun address(): ATag diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventInterface.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventInterface.kt index 5a4e1db57..75e49d3c5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventInterface.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventInterface.kt @@ -1,8 +1,10 @@ package com.vitorpamplona.amethyst.service.model +import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.HexKey import java.math.BigDecimal +@Immutable interface EventInterface { fun id(): HexKey @@ -35,4 +37,5 @@ interface EventInterface { fun getPoWRank(): Int fun zapAddress(): String? + fun isSensitive(): Boolean } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileHeaderEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileHeaderEvent.kt index a33344af6..ca72864f9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileHeaderEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileHeaderEvent.kt @@ -1,10 +1,12 @@ package com.vitorpamplona.amethyst.service.model +import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.toHexKey import nostr.postr.Utils import java.util.Date +@Immutable class FileHeaderEvent( id: HexKey, pubKey: HexKey, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileStorageEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileStorageEvent.kt index af25b4152..13fc5d03d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileStorageEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileStorageEvent.kt @@ -1,12 +1,14 @@ package com.vitorpamplona.amethyst.service.model import android.util.Log +import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.toHexKey import nostr.postr.Utils import java.util.Base64 import java.util.Date +@Immutable class FileStorageEvent( id: HexKey, pubKey: HexKey, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileStorageHeaderEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileStorageHeaderEvent.kt index f2ba729bb..c84de54ad 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileStorageHeaderEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileStorageHeaderEvent.kt @@ -1,10 +1,12 @@ package com.vitorpamplona.amethyst.service.model +import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.toHexKey import nostr.postr.Utils import java.util.Date +@Immutable class FileStorageHeaderEvent( id: HexKey, pubKey: HexKey, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/GeneralListEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/GeneralListEvent.kt index 57cadcd66..2594c74f5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/GeneralListEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/GeneralListEvent.kt @@ -1,11 +1,13 @@ package com.vitorpamplona.amethyst.service.model import android.util.Log +import androidx.compose.runtime.Immutable import com.google.gson.reflect.TypeToken import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.hexToByteArray import nostr.postr.Utils +@Immutable abstract class GeneralListEvent( id: HexKey, pubKey: HexKey, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/HighlightEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/HighlightEvent.kt index 1d3d1acf6..6bb49cb08 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/HighlightEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/HighlightEvent.kt @@ -1,10 +1,12 @@ package com.vitorpamplona.amethyst.service.model +import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.toHexKey import nostr.postr.Utils import java.util.Date +@Immutable class HighlightEvent( id: HexKey, pubKey: HexKey, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt index a326f5795..59e61c309 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt @@ -1,10 +1,12 @@ package com.vitorpamplona.amethyst.service.model import android.util.Log +import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil import com.vitorpamplona.amethyst.service.relays.Client +@Immutable class LnZapEvent( id: HexKey, pubKey: HexKey, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEventInterface.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEventInterface.kt index 934dba55b..13921deee 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEventInterface.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEventInterface.kt @@ -1,7 +1,9 @@ package com.vitorpamplona.amethyst.service.model +import androidx.compose.runtime.Immutable import java.math.BigDecimal +@Immutable interface LnZapEventInterface : EventInterface { fun zappedPost(): List diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapPaymentRequestEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapPaymentRequestEvent.kt index 4946fb4c9..9975ae814 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapPaymentRequestEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapPaymentRequestEvent.kt @@ -1,6 +1,7 @@ package com.vitorpamplona.amethyst.service.model import android.util.Log +import androidx.compose.runtime.Immutable import com.google.gson.JsonDeserializationContext import com.google.gson.JsonDeserializer import com.google.gson.JsonElement @@ -12,6 +13,7 @@ import nostr.postr.Utils import java.lang.reflect.Type import java.util.Date +@Immutable class LnZapPaymentRequestEvent( id: HexKey, pubKey: HexKey, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapPaymentResponseEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapPaymentResponseEvent.kt index 5d8ec2e95..4e4c41a90 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapPaymentResponseEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapPaymentResponseEvent.kt @@ -1,6 +1,7 @@ package com.vitorpamplona.amethyst.service.model import android.util.Log +import androidx.compose.runtime.Immutable import com.google.gson.JsonDeserializationContext import com.google.gson.JsonDeserializer import com.google.gson.JsonElement @@ -10,6 +11,7 @@ import com.vitorpamplona.amethyst.model.HexKey import nostr.postr.Utils import java.lang.reflect.Type +@Immutable class LnZapPaymentResponseEvent( id: HexKey, pubKey: HexKey, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt index eeb2bb81a..aa60dcb6f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt @@ -1,5 +1,6 @@ package com.vitorpamplona.amethyst.service.model +import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.* import nostr.postr.Bech32 import nostr.postr.Utils @@ -11,6 +12,7 @@ import javax.crypto.Cipher import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec +@Immutable class LnZapRequestEvent( id: HexKey, pubKey: HexKey, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt index 529c96fd3..f2fe3de38 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt @@ -1,10 +1,12 @@ package com.vitorpamplona.amethyst.service.model +import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.toHexKey import nostr.postr.Utils import java.util.Date +@Immutable class LongTextNoteEvent( id: HexKey, pubKey: HexKey, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/MetadataEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/MetadataEvent.kt index 7cec19bca..766d1e79d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/MetadataEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/MetadataEvent.kt @@ -1,6 +1,7 @@ package com.vitorpamplona.amethyst.service.model import android.util.Log +import androidx.compose.runtime.Immutable import com.google.gson.Gson import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.HexKey @@ -8,6 +9,7 @@ import com.vitorpamplona.amethyst.model.toHexKey import nostr.postr.Utils import java.util.Date +@Immutable data class ContactMetaData( val name: String, val picture: String, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/MuteListEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/MuteListEvent.kt index 977488836..afd65cfb7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/MuteListEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/MuteListEvent.kt @@ -1,6 +1,7 @@ package com.vitorpamplona.amethyst.service.model import android.util.Log +import androidx.compose.runtime.Immutable import com.google.gson.reflect.TypeToken import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.hexToByteArray @@ -8,6 +9,7 @@ import com.vitorpamplona.amethyst.model.toHexKey import nostr.postr.Utils import java.util.Date +@Immutable class MuteListEvent( id: HexKey, pubKey: HexKey, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PeopleListEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PeopleListEvent.kt index 583f534b5..51d9fa305 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PeopleListEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PeopleListEvent.kt @@ -1,10 +1,12 @@ package com.vitorpamplona.amethyst.service.model +import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.toHexKey import nostr.postr.Utils import java.util.Date +@Immutable class PeopleListEvent( id: HexKey, pubKey: HexKey, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PinListEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PinListEvent.kt index 100aaf981..e5f6e69a2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PinListEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PinListEvent.kt @@ -1,10 +1,12 @@ package com.vitorpamplona.amethyst.service.model +import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.toHexKey import nostr.postr.Utils import java.util.Date +@Immutable class PinListEvent( id: HexKey, pubKey: HexKey, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt index 0cb71f805..cd0dfb67d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt @@ -1,5 +1,6 @@ package com.vitorpamplona.amethyst.service.model +import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.toHexKey import nostr.postr.Utils @@ -11,6 +12,7 @@ const val VALUE_MINIMUM = "value_minimum" const val CONSENSUS_THRESHOLD = "consensus_threshold" const val CLOSED_AT = "closed_at" +@Immutable class PollNoteEvent( id: HexKey, pubKey: HexKey, @@ -49,7 +51,8 @@ class PollNoteEvent( valueMinimum: Int?, consensusThreshold: Int?, closedAt: Int?, - zapReceiver: String? + zapReceiver: String?, + markAsSensitive: Boolean ): PollNoteEvent { val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags = mutableListOf>() @@ -73,6 +76,9 @@ class PollNoteEvent( if (zapReceiver != null) { tags.add(listOf("zap", zapReceiver)) } + if (markAsSensitive) { + tags.add(listOf("content-warning", "")) + } val id = generateId(pubKey, createdAt, kind, tags, msg) val sig = Utils.sign(id, privateKey) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PrivateDmEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PrivateDmEvent.kt index 20e4bc696..837747f8e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PrivateDmEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PrivateDmEvent.kt @@ -1,6 +1,7 @@ package com.vitorpamplona.amethyst.service.model import android.util.Log +import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.toHexKey import fr.acinq.secp256k1.Hex @@ -8,6 +9,7 @@ import nostr.postr.Utils import nostr.postr.toHex import java.util.Date +@Immutable class PrivateDmEvent( id: HexKey, pubKey: HexKey, @@ -37,7 +39,7 @@ class PrivateDmEvent( fun with(pubkeyHex: String): Boolean { return pubkeyHex == pubKey || - tags.firstOrNull { it.size > 1 && it[0] == "p" }?.getOrNull(1) == pubkeyHex + tags.any { it.size > 1 && it[0] == "p" && it[1] == pubkeyHex } } fun plainContent(privKey: ByteArray, pubKey: ByteArray): String? { @@ -71,7 +73,8 @@ class PrivateDmEvent( privateKey: ByteArray, createdAt: Long = Date().time / 1000, publishedRecipientPubKey: ByteArray? = null, - advertiseNip18: Boolean = true + advertiseNip18: Boolean = true, + markAsSensitive: Boolean ): PrivateDmEvent { val content = Utils.encrypt( if (advertiseNip18) { nip18Advertisement } else { "" } + msg, @@ -92,6 +95,9 @@ class PrivateDmEvent( zapReceiver?.let { tags.add(listOf("zap", it)) } + if (markAsSensitive) { + tags.add(listOf("content-warning", "")) + } val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) return PrivateDmEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt index 91b34d4e7..e2a23ecb7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt @@ -1,10 +1,12 @@ package com.vitorpamplona.amethyst.service.model +import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.toHexKey import nostr.postr.Utils import java.util.Date +@Immutable class ReactionEvent( id: HexKey, pubKey: HexKey, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RecommendRelayEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RecommendRelayEvent.kt index 117931b99..8cdc833dc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RecommendRelayEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RecommendRelayEvent.kt @@ -1,11 +1,13 @@ package com.vitorpamplona.amethyst.service.model +import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.toHexKey import nostr.postr.Utils import java.net.URI import java.util.Date +@Immutable class RecommendRelayEvent( id: HexKey, pubKey: HexKey, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RelayAuthEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RelayAuthEvent.kt index 6a7e04228..388daefdc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RelayAuthEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RelayAuthEvent.kt @@ -1,10 +1,12 @@ package com.vitorpamplona.amethyst.service.model +import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.toHexKey import nostr.postr.Utils import java.util.Date +@Immutable class RelayAuthEvent( id: HexKey, pubKey: HexKey, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt index b05e55870..cdb075f3a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt @@ -1,13 +1,16 @@ package com.vitorpamplona.amethyst.service.model +import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.toHexKey import nostr.postr.Utils import java.util.Date +@Immutable data class ReportedKey(val key: String, val reportType: ReportEvent.ReportType) // NIP 56 event. +@Immutable class ReportEvent( id: HexKey, pubKey: HexKey, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt index c00c67617..6b2053afd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt @@ -1,11 +1,13 @@ package com.vitorpamplona.amethyst.service.model +import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.service.relays.Client import nostr.postr.Utils import java.util.Date +@Immutable class RepostEvent( id: HexKey, pubKey: HexKey, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt index aa80ac016..616f40d7c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt @@ -1,5 +1,6 @@ package com.vitorpamplona.amethyst.service.model +import androidx.compose.runtime.Immutable import com.linkedin.urls.detection.UrlDetector import com.linkedin.urls.detection.UrlDetectorOptions import com.vitorpamplona.amethyst.model.HexKey @@ -8,6 +9,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.findHashtags import nostr.postr.Utils import java.util.Date +@Immutable class TextNoteEvent( id: HexKey, pubKey: HexKey, @@ -27,6 +29,7 @@ class TextNoteEvent( addresses: List?, extraTags: List?, zapReceiver: String?, + markAsSensitive: Boolean, privateKey: ByteArray, createdAt: Long = Date().time / 1000 ): TextNoteEvent { @@ -54,6 +57,9 @@ class TextNoteEvent( findURLs(msg).forEach { tags.add(listOf("r", it)) } + if (markAsSensitive) { + tags.add(listOf("content-warning", "")) + } val id = generateId(pubKey, createdAt, kind, tags, msg) val sig = Utils.sign(id, privateKey) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/previews/BahaUrlPreview.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/previews/BahaUrlPreview.kt new file mode 100644 index 000000000..e56493ceb --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/previews/BahaUrlPreview.kt @@ -0,0 +1,48 @@ +package com.vitorpamplona.amethyst.service.previews + +import android.net.Uri +import kotlinx.coroutines.* +import java.util.* + +class BahaUrlPreview(val url: String, var callback: IUrlPreviewCallback?) { + val scope = CoroutineScope(Job() + Dispatchers.Main) + private val imageExtensionArray = arrayOf(".gif", ".png", ".jpg", ".jpeg", ".bmp", ".webp") + + fun fetchUrlPreview(timeOut: Int = 30000) { + val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> + callback?.onFailed(throwable) + } + scope.launch(exceptionHandler) { + fetch(timeOut) + } + } + + private suspend fun fetch(timeOut: Int = 30000) { + lateinit var urlInfoItem: UrlInfoItem + if (checkIsImageUrl()) { + urlInfoItem = UrlInfoItem(url = url, image = url) + } else { + val document = getDocument(url, timeOut) + urlInfoItem = parseHtml(document) + urlInfoItem.url = url + } + callback?.onComplete(urlInfoItem) + } + + private fun checkIsImageUrl(): Boolean { + val uri = Uri.parse(url) + var isImage = false + for (imageExtension in imageExtensionArray) { + if (uri.path != null && uri.path!!.toLowerCase(Locale.getDefault()).endsWith(imageExtension)) { + isImage = true + break + } + } + return isImage + } + + fun cleanUp() { + scope.cancel() + callback = null + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/previews/IUrlPreviewCallback.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/previews/IUrlPreviewCallback.kt new file mode 100644 index 000000000..6d9778312 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/previews/IUrlPreviewCallback.kt @@ -0,0 +1,6 @@ +package com.vitorpamplona.amethyst.service.previews + +interface IUrlPreviewCallback { + fun onComplete(urlInfo: UrlInfoItem) + fun onFailed(throwable: Throwable) +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlInfoItem.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlInfoItem.kt new file mode 100644 index 000000000..553f9b1c1 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlInfoItem.kt @@ -0,0 +1,12 @@ +package com.vitorpamplona.amethyst.service.previews + +data class UrlInfoItem( + var url: String = "", + var title: String = "", + var description: String = "", + var image: String = "" +) { + fun allFetchComplete(): Boolean { + return title.isNotEmpty() && description.isNotEmpty() && image.isNotEmpty() + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlPreviewUtils.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlPreviewUtils.kt new file mode 100644 index 000000000..62c6e83e9 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlPreviewUtils.kt @@ -0,0 +1,116 @@ +package com.vitorpamplona.amethyst.service.previews + +import com.vitorpamplona.amethyst.service.HttpClient +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.jsoup.Jsoup +import org.jsoup.nodes.Document + +private const val ELEMENT_TAG_META = "meta" +private const val ATTRIBUTE_VALUE_PROPERTY = "property" +private const val ATTRIBUTE_VALUE_NAME = "name" +private const val ATTRIBUTE_VALUE_ITEMPROP = "itemprop" + +/* for if (urlInfo.title.isEmpty()) urlInfo.title = it.attr(CONTENT) + in META_OG_DESCRIPTION -> if (urlInfo.description.isEmpty()) { + urlInfo.description = + it.attr(CONTENT) + } + in META_OG_IMAGE -> if (urlInfo.image.isEmpty()) urlInfo.image = it.attr(CONTENT) + } + + when (it.attr(ATTRIBUTE_VALUE_NAME)) { + in META_NAME_TITLE -> if (urlInfo.title.isEmpty()) urlInfo.title = it.attr(CONTENT) + in META_NAME_DESCRIPTION -> if (urlInfo.description.isEmpty()) { + urlInfo.description = + it.attr(CONTENT) + } + in META_OG_IMAGE -> if (urlInfo.image.isEmpty()) urlInfo.image = it.attr(CONTENT) + } + + when (it.attr(ATTRIBUTE_VALUE_ITEMPROP)) { + in META_ITEMPROP_TITLE -> if (urlInfo.title.isEmpty()) { + urlInfo.title = + it.attr(CONTENT) + } + in META_ITEMPROP_DESCRIPTION -> if (urlInfo.description.isEmpty()) { + urlInfo.description = + it.attr( + CONTENT + ) + } + in META_ITEMPROP_IMAGE -> if (urlInfo.image.isEmpty()) { + urlInfo.image = it.attr( + CONTENT + ) + } + } + if (urlInfo.allFetchComplete()) { + return@withContext urlInfo + } + } + return@withContext urlInfo + } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt index b4e0a9341..41fe2823b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt @@ -30,6 +30,7 @@ class Relay( ) { val seconds = if (proxy != null) 20L else 10L val duration = Duration.ofSeconds(seconds) + private val httpClient = OkHttpClient.Builder() .proxy(proxy) .readTimeout(duration) 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 f09b0735c..f758617e8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -20,6 +20,7 @@ import coil.decode.SvgDecoder import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.ServiceManager import com.vitorpamplona.amethyst.VideoCache +import com.vitorpamplona.amethyst.service.HttpClient import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent @@ -46,35 +47,7 @@ class MainActivity : FragmentActivity() { val uri = intent?.data?.toString() - val startingPage = if (uri.equals("nostr:Notifications", true)) { - Route.Notification.route.replace("{scrollToTop}", "true") - } else { - val nip19 = Nip19.uriToRoute(uri) - when (nip19?.type) { - Nip19.Type.USER -> "User/${nip19.hex}" - Nip19.Type.NOTE -> "Note/${nip19.hex}" - Nip19.Type.EVENT -> { - if (nip19.kind == PrivateDmEvent.kind) { - "Room/${nip19.author}" - } else if (nip19.kind == ChannelMessageEvent.kind || nip19.kind == ChannelCreateEvent.kind || nip19.kind == ChannelMetadataEvent.kind) { - "Channel/${nip19.hex}" - } else { - "Event/${nip19.hex}" - } - } - - Nip19.Type.ADDRESS -> "Note/${nip19.hex}" - else -> null - } - } ?: try { - uri?.let { - Nip47.parse(it) - val encodedUri = URLEncoder.encode(it, StandardCharsets.UTF_8.toString()) - Route.Home.base + "?nip47=" + encodedUri - } - } catch (e: Exception) { - null - } + val startingPage = uriToRoute(uri) // Initializes video cache. VideoCache.init(this.applicationContext) @@ -88,6 +61,7 @@ class MainActivity : FragmentActivity() { } add(SvgDecoder.Factory()) } // .logger(DebugLogger()) + .okHttpClient { HttpClient.getHttpClient() } .respectCacheHeaders(false) .build() } @@ -150,3 +124,39 @@ class GetMediaActivityResultContract : ActivityResultContracts.GetContent() { } } } + +fun uriToRoute(uri: String?): String? { + return if (uri.equals("nostr:Notifications", true)) { + Route.Notification.route.replace("{scrollToTop}", "true") + } else { + if (uri?.startsWith("nostr:Hashtag?id=") == true) { + Route.Hashtag.route.replace("{id}", uri.removePrefix("nostr:Hashtag?id=")) + } else { + val nip19 = Nip19.uriToRoute(uri) + when (nip19?.type) { + Nip19.Type.USER -> "User/${nip19.hex}" + Nip19.Type.NOTE -> "Note/${nip19.hex}" + Nip19.Type.EVENT -> { + if (nip19.kind == PrivateDmEvent.kind) { + "Room/${nip19.author}" + } else if (nip19.kind == ChannelMessageEvent.kind || nip19.kind == ChannelCreateEvent.kind || nip19.kind == ChannelMetadataEvent.kind) { + "Channel/${nip19.hex}" + } else { + "Event/${nip19.hex}" + } + } + + Nip19.Type.ADDRESS -> "Note/${nip19.hex}" + else -> null + } + } ?: try { + uri?.let { + Nip47.parse(it) + val encodedUri = URLEncoder.encode(it, StandardCharsets.UTF_8.toString()) + Route.Home.base + "?nip47=" + encodedUri + } + } catch (e: Exception) { + null + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt index 4f0d840af..3a0806ff2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt @@ -40,9 +40,9 @@ object ImageUploader { "Can't open the image input stream" } val myServer = when (server) { - ServersAvailable.IMGUR, ServersAvailable.IMGUR_NIP_94 -> { - ImgurServer() - } + // ServersAvailable.IMGUR, ServersAvailable.IMGUR_NIP_94 -> { + // ImgurServer() + // } ServersAvailable.NOSTRIMG, ServersAvailable.NOSTRIMG_NIP_94 -> { NostrImgServer() } @@ -53,7 +53,7 @@ object ImageUploader { NostrFilesDevServer() } else -> { - ImgurServer() + NostrBuildServer() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/JoinUserOrChannelView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/JoinUserOrChannelView.kt index eda028bf0..08ff4090e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/JoinUserOrChannelView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/JoinUserOrChannelView.kt @@ -51,7 +51,6 @@ import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavController import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache @@ -73,7 +72,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @Composable -fun JoinUserOrChannelView(onClose: () -> Unit, account: Account, navController: NavController) { +fun JoinUserOrChannelView(onClose: () -> Unit, account: Account, nav: (String) -> Unit) { val searchBarViewModel: SearchBarViewModel = viewModel() searchBarViewModel.account = account @@ -116,7 +115,7 @@ fun JoinUserOrChannelView(onClose: () -> Unit, account: Account, navController: Spacer(modifier = Modifier.height(15.dp)) - RenderSeach(searchBarViewModel, account, navController) + RenderSeach(searchBarViewModel, account, nav) } } } @@ -127,7 +126,7 @@ fun JoinUserOrChannelView(onClose: () -> Unit, account: Account, navController: private fun RenderSeach( searchBarViewModel: SearchBarViewModel, account: Account, - navController: NavController + nav: (String) -> Unit ) { val scope = rememberCoroutineScope() val listState = rememberLazyListState() @@ -275,7 +274,7 @@ private fun RenderSeach( UserComposeForChat( item, account = account, - navController = navController + nav = nav ) } @@ -295,7 +294,7 @@ private fun RenderSeach( channelLastTime = null, channelLastContent = item.info.about, false, - onClick = { navController.navigate("Channel/${item.idHex}") } + onClick = { nav("Channel/${item.idHex}") } ) } } @@ -307,12 +306,12 @@ private fun RenderSeach( fun UserComposeForChat( baseUser: User, account: Account, - navController: NavController + nav: (String) -> Unit ) { Column( modifier = Modifier.clickable( - onClick = { navController.navigate("Room/${baseUser.pubkeyHex}") } + onClick = { nav("Room/${baseUser.pubkeyHex}") } ) ) { Row( @@ -324,7 +323,7 @@ fun UserComposeForChat( ), verticalAlignment = Alignment.CenterVertically ) { - UserPicture(baseUser, navController, account.userProfile(), 55.dp) + UserPicture(baseUser, nav, account.userProfile(), 55.dp) Column(modifier = Modifier.padding(start = 10.dp).weight(1f)) { Row(verticalAlignment = Alignment.CenterVertically) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt index e8a6398ec..4aae8742a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt @@ -38,9 +38,10 @@ open class NewMediaModel : ViewModel() { this.mediaType = contentType this.selectedServer = defaultServer() - if (selectedServer == ServersAvailable.IMGUR) { - selectedServer = ServersAvailable.IMGUR_NIP_94 - } else if (selectedServer == ServersAvailable.NOSTRIMG) { + // if (selectedServer == ServersAvailable.IMGUR) { + // selectedServer = ServersAvailable.IMGUR_NIP_94 + // } else + if (selectedServer == ServersAvailable.NOSTRIMG) { selectedServer = ServersAvailable.NOSTRIMG_NIP_94 } else if (selectedServer == ServersAvailable.NOSTR_BUILD) { selectedServer = ServersAvailable.NOSTR_BUILD_NIP_94 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt index 955e0a4fc..efd74fb55 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt @@ -28,7 +28,6 @@ import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties -import androidx.navigation.NavController import coil.compose.AsyncImage import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account @@ -39,7 +38,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @Composable -fun NewMediaView(uri: Uri, onClose: () -> Unit, postViewModel: NewMediaModel, accountViewModel: AccountViewModel, navController: NavController) { +fun NewMediaView(uri: Uri, onClose: () -> Unit, postViewModel: NewMediaModel, accountViewModel: AccountViewModel, nav: (String) -> Unit) { val account = accountViewModel.accountLiveData.value?.account ?: return val resolver = LocalContext.current.contentResolver val context = LocalContext.current @@ -111,7 +110,7 @@ fun NewMediaView(uri: Uri, onClose: () -> Unit, postViewModel: NewMediaModel, ac fun isNIP94Server(selectedServer: ServersAvailable?): Boolean { return selectedServer == ServersAvailable.NOSTRIMG_NIP_94 || - selectedServer == ServersAvailable.IMGUR_NIP_94 || + // selectedServer == ServersAvailable.IMGUR_NIP_94 || selectedServer == ServersAvailable.NOSTR_BUILD_NIP_94 || selectedServer == ServersAvailable.NOSTRFILES_DEV_NIP_94 } @@ -121,7 +120,7 @@ fun ImageVideoPost(postViewModel: NewMediaModel, acc: Account) { val scope = rememberCoroutineScope() val fileServers = listOf( - Triple(ServersAvailable.IMGUR_NIP_94, stringResource(id = R.string.upload_server_imgur_nip94), stringResource(id = R.string.upload_server_imgur_nip94_explainer)), + // Triple(ServersAvailable.IMGUR_NIP_94, stringResource(id = R.string.upload_server_imgur_nip94), stringResource(id = R.string.upload_server_imgur_nip94_explainer)), Triple(ServersAvailable.NOSTRIMG_NIP_94, stringResource(id = R.string.upload_server_nostrimg_nip94), stringResource(id = R.string.upload_server_nostrimg_nip94_explainer)), Triple(ServersAvailable.NOSTR_BUILD_NIP_94, stringResource(id = R.string.upload_server_nostrbuild_nip94), stringResource(id = R.string.upload_server_nostrbuild_nip94_explainer)), Triple(ServersAvailable.NOSTRFILES_DEV_NIP_94, stringResource(id = R.string.upload_server_nostrfilesdev_nip94), stringResource(id = R.string.upload_server_nostrfilesdev_nip94_explainer)), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index dc8a01489..73dd8ab3a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -22,8 +22,12 @@ import androidx.compose.material.icons.filled.ArrowForwardIos import androidx.compose.material.icons.filled.Bolt import androidx.compose.material.icons.filled.Cancel import androidx.compose.material.icons.filled.CurrencyBitcoin +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material.icons.filled.Warning import androidx.compose.material.icons.outlined.ArrowForwardIos import androidx.compose.material.icons.outlined.Bolt +import androidx.compose.material.icons.rounded.Warning import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -57,7 +61,6 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavController import coil.compose.AsyncImage import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account @@ -75,7 +78,7 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalComposeUiApi::class) @Composable -fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = null, account: Account, accountViewModel: AccountViewModel, navController: NavController) { +fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = null, account: Account, accountViewModel: AccountViewModel, nav: (String) -> Unit) { val postViewModel: NewPostViewModel = viewModel() val context = LocalContext.current @@ -84,7 +87,7 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n val focusRequester = remember { FocusRequester() } val keyboardController = LocalSoftwareKeyboardController.current - val scroolState = rememberScrollState() + val scrollState = rememberScrollState() val scope = rememberCoroutineScope() LaunchedEffect(Unit) { @@ -154,7 +157,7 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n Column( modifier = Modifier .fillMaxWidth() - .verticalScroll(scroolState) + .verticalScroll(scrollState) ) { Notifying(postViewModel.mentions) { postViewModel.removeFromReplyList(it) @@ -292,7 +295,7 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n true, MaterialTheme.colors.background, accountViewModel, - navController + nav ) } else if (noProtocolUrlValidator.matcher(myUrlPreview).matches()) { UrlPreview("https://$myUrlPreview", myUrlPreview) @@ -336,10 +339,11 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n } if (postViewModel.canUsePoll) { - val hashtag = stringResource(R.string.poll_hashtag) + // These should be hashtag recommendations the user selects in the future. + // val hashtag = stringResource(R.string.poll_hashtag) + // postViewModel.includePollHashtagInMessage(postViewModel.wantsPoll, hashtag) AddPollButton(postViewModel.wantsPoll) { postViewModel.wantsPoll = !postViewModel.wantsPoll - postViewModel.includePollHashtagInMessage(postViewModel.wantsPoll, hashtag) } } @@ -349,6 +353,10 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n } } + MarkAsSensitive(postViewModel) { + postViewModel.wantsToMarkAsSensitive = !postViewModel.wantsToMarkAsSensitive + } + ForwardZapTo(postViewModel) { postViewModel.wantsForwardZapTo = !postViewModel.wantsForwardZapTo } @@ -538,6 +546,60 @@ private fun ForwardZapTo( } } +@Composable +private fun MarkAsSensitive( + postViewModel: NewPostViewModel, + onClick: () -> Unit +) { + IconButton( + onClick = { + onClick() + } + ) { + Box( + Modifier + .height(20.dp) + .width(23.dp) + ) { + if (!postViewModel.wantsToMarkAsSensitive) { + Icon( + imageVector = Icons.Default.Visibility, + contentDescription = stringResource(R.string.content_warning), + modifier = Modifier + .size(18.dp) + .align(Alignment.BottomStart), + tint = MaterialTheme.colors.onBackground + ) + Icon( + imageVector = Icons.Rounded.Warning, + contentDescription = stringResource(R.string.content_warning), + modifier = Modifier + .size(10.dp) + .align(Alignment.TopEnd), + tint = MaterialTheme.colors.onBackground + ) + } else { + Icon( + imageVector = Icons.Default.VisibilityOff, + contentDescription = stringResource(id = R.string.content_warning), + modifier = Modifier + .size(18.dp) + .align(Alignment.BottomStart), + tint = Color.Red + ) + Icon( + imageVector = Icons.Rounded.Warning, + contentDescription = stringResource(id = R.string.content_warning), + modifier = Modifier + .size(10.dp) + .align(Alignment.TopEnd), + tint = Color.Yellow + ) + } + } + } +} + @Composable fun CloseButton(onCancel: () -> Unit) { Button( @@ -641,11 +703,12 @@ fun SearchButton(onPost: () -> Unit = {}, isActive: Boolean, modifier: Modifier } enum class ServersAvailable { - IMGUR, + // IMGUR, NOSTR_BUILD, NOSTRIMG, NOSTRFILES_DEV, - IMGUR_NIP_94, + + // IMGUR_NIP_94, NOSTRIMG_NIP_94, NOSTR_BUILD_NIP_94, NOSTRFILES_DEV_NIP_94, @@ -668,11 +731,11 @@ fun ImageVideoDescription( val isVideo = mediaType.startsWith("video") val fileServers = listOf( - Triple(ServersAvailable.IMGUR, stringResource(id = R.string.upload_server_imgur), stringResource(id = R.string.upload_server_imgur_explainer)), + // Triple(ServersAvailable.IMGUR, stringResource(id = R.string.upload_server_imgur), stringResource(id = R.string.upload_server_imgur_explainer)), Triple(ServersAvailable.NOSTRIMG, stringResource(id = R.string.upload_server_nostrimg), stringResource(id = R.string.upload_server_nostrimg_explainer)), Triple(ServersAvailable.NOSTR_BUILD, stringResource(id = R.string.upload_server_nostrbuild), stringResource(id = R.string.upload_server_nostrbuild_explainer)), Triple(ServersAvailable.NOSTRFILES_DEV, stringResource(id = R.string.upload_server_nostrfilesdev), stringResource(id = R.string.upload_server_nostrfilesdev_explainer)), - Triple(ServersAvailable.IMGUR_NIP_94, stringResource(id = R.string.upload_server_imgur_nip94), stringResource(id = R.string.upload_server_imgur_nip94_explainer)), + // Triple(ServersAvailable.IMGUR_NIP_94, stringResource(id = R.string.upload_server_imgur_nip94), stringResource(id = R.string.upload_server_imgur_nip94_explainer)), Triple(ServersAvailable.NOSTRIMG_NIP_94, stringResource(id = R.string.upload_server_nostrimg_nip94), stringResource(id = R.string.upload_server_nostrimg_nip94_explainer)), Triple(ServersAvailable.NOSTR_BUILD_NIP_94, stringResource(id = R.string.upload_server_nostrbuild_nip94), stringResource(id = R.string.upload_server_nostrbuild_nip94_explainer)), Triple(ServersAvailable.NOSTRFILES_DEV_NIP_94, stringResource(id = R.string.upload_server_nostrfilesdev_nip94), stringResource(id = R.string.upload_server_nostrfilesdev_nip94_explainer)), 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 c22115261..327843b60 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 @@ -67,6 +67,9 @@ open class NewPostViewModel : ViewModel() { var forwardZapTo by mutableStateOf(null) var forwardZapToEditting by mutableStateOf(TextFieldValue("")) + // NSFW, Sensitive + var wantsToMarkAsSensitive by mutableStateOf(false) + open fun load(account: Account, replyingTo: Note?, quote: Note?) { originalNote = replyingTo replyingTo?.let { replyNote -> @@ -97,6 +100,7 @@ open class NewPostViewModel : ViewModel() { contentToAddUrl = null wantsForwardZapTo = false + wantsToMarkAsSensitive = false forwardZapTo = null forwardZapToEditting = TextFieldValue("") @@ -118,13 +122,13 @@ open class NewPostViewModel : ViewModel() { } if (wantsPoll) { - account?.sendPoll(tagger.message, tagger.replyTos, tagger.mentions, pollOptions, valueMaximum, valueMinimum, consensusThreshold, closedAt, zapReceiver) + account?.sendPoll(tagger.message, tagger.replyTos, tagger.mentions, pollOptions, valueMaximum, valueMinimum, consensusThreshold, closedAt, zapReceiver, wantsToMarkAsSensitive) } else if (originalNote?.channel() != null) { - account?.sendChannelMessage(tagger.message, tagger.channel!!.idHex, tagger.replyTos, tagger.mentions, zapReceiver) + account?.sendChannelMessage(tagger.message, tagger.channel!!.idHex, tagger.replyTos, tagger.mentions, zapReceiver, wantsToMarkAsSensitive) } else if (originalNote?.event is PrivateDmEvent) { - account?.sendPrivateMessage(tagger.message, originalNote!!.author!!.pubkeyHex, originalNote!!, tagger.mentions, zapReceiver) + account?.sendPrivateMessage(tagger.message, originalNote!!.author!!.pubkeyHex, originalNote!!, tagger.mentions, zapReceiver, wantsToMarkAsSensitive) } else { - account?.sendPost(tagger.message, tagger.replyTos, tagger.mentions, null, zapReceiver) + account?.sendPost(tagger.message, tagger.replyTos, tagger.mentions, null, zapReceiver, wantsToMarkAsSensitive) } cancel() @@ -183,6 +187,7 @@ open class NewPostViewModel : ViewModel() { wantsInvoice = false wantsForwardZapTo = false + wantsToMarkAsSensitive = false forwardZapTo = null forwardZapToEditting = TextFieldValue("") diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/ChannelFabColumn.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/ChannelFabColumn.kt index 7a4d3dad8..84e292087 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/ChannelFabColumn.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/ChannelFabColumn.kt @@ -15,14 +15,13 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.actions.JoinUserOrChannelView import com.vitorpamplona.amethyst.ui.actions.NewChannelView @Composable -fun ChannelFabColumn(account: Account, navController: NavController) { +fun ChannelFabColumn(account: Account, nav: (String) -> Unit) { var isOpen by remember { mutableStateOf(false) } @@ -40,7 +39,7 @@ fun ChannelFabColumn(account: Account, navController: NavController) { } if (wantsToJoinChannelOrUser) { - JoinUserOrChannelView({ wantsToJoinChannelOrUser = false }, account = account, navController = navController) + JoinUserOrChannelView({ wantsToJoinChannelOrUser = false }, account = account, nav = nav) } Column() { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewNoteButton.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewNoteButton.kt index 0fe83a465..d03c1d01c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewNoteButton.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewNoteButton.kt @@ -16,20 +16,19 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.actions.NewPostView import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable -fun NewNoteButton(account: Account, accountViewModel: AccountViewModel, navController: NavController) { +fun NewNoteButton(account: Account, accountViewModel: AccountViewModel, nav: (String) -> Unit) { var wantsToPost by remember { mutableStateOf(false) } if (wantsToPost) { - NewPostView({ wantsToPost = false }, account = account, accountViewModel = accountViewModel, navController = navController) + NewPostView({ wantsToPost = false }, account = account, accountViewModel = accountViewModel, nav = nav) } OutlinedButton( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/BundledUpdate.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/BundledUpdate.kt index 2a1036fb9..9b5f60f9f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/BundledUpdate.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/BundledUpdate.kt @@ -22,9 +22,11 @@ class BundledUpdate( private var onlyOneInBlock = AtomicBoolean() private var invalidatesAgain = false - fun invalidate() { + fun invalidate(ignoreIfDoing: Boolean = false) { if (onlyOneInBlock.getAndSet(true)) { - invalidatesAgain = true + if (!ignoreIfDoing) { + invalidatesAgain = true + } return } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableNoteTag.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableNoteTag.kt index 8962fc219..cd48515fa 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableNoteTag.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableNoteTag.kt @@ -5,18 +5,17 @@ import androidx.compose.material.LocalTextStyle import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.text.AnnotatedString -import androidx.navigation.NavController import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.note.toShortenHex @Composable fun ClickableNoteTag( baseNote: Note, - navController: NavController + nav: (String) -> Unit ) { ClickableText( text = AnnotatedString("@${baseNote.idNote().toShortenHex()}"), - onClick = { navController.navigate("Note/${baseNote.idHex}") }, + onClick = { nav("Note/${baseNote.idHex}") }, style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary) ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt index 9bd2753fb..0da6a33d7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt @@ -37,7 +37,6 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.navigation.NavController import coil.compose.AsyncImage import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note @@ -52,16 +51,16 @@ import kotlinx.coroutines.withContext @Composable fun ClickableRoute( nip19: Nip19.Return, - navController: NavController + nav: (String) -> Unit ) { if (nip19.type == Nip19.Type.USER) { - DisplayUser(nip19, navController) + DisplayUser(nip19, nav) } else if (nip19.type == Nip19.Type.ADDRESS) { - DisplayAddress(nip19, navController) + DisplayAddress(nip19, nav) } else if (nip19.type == Nip19.Type.NOTE) { - DisplayNote(nip19, navController) + DisplayNote(nip19, nav) } else if (nip19.type == Nip19.Type.EVENT) { - DisplayEvent(nip19, navController) + DisplayEvent(nip19, nav) } else { Text( "@${nip19.hex}${nip19.additionalChars} " @@ -72,9 +71,9 @@ fun ClickableRoute( @Composable private fun DisplayEvent( nip19: Nip19.Return, - navController: NavController + nav: (String) -> Unit ) { - var noteBase by remember { mutableStateOf(null) } + var noteBase by remember(nip19) { mutableStateOf(null) } LaunchedEffect(key1 = nip19.hex) { withContext(Dispatchers.IO) { @@ -84,36 +83,36 @@ private fun DisplayEvent( noteBase?.let { val noteState by it.live().metadata.observeAsState() - val note = noteState?.note ?: return - val channel = note.channel() + val note = remember(noteState) { noteState?.note } ?: return + val channel = remember(noteState) { note.channel() } if (note.event is ChannelCreateEvent) { CreateClickableText( clickablePart = "@${note.idDisplayNote()}", suffix = "${nip19.additionalChars} ", route = "Channel/${nip19.hex}", - navController = navController + nav = nav ) } else if (note.event is PrivateDmEvent) { CreateClickableText( clickablePart = "@${note.idDisplayNote()}", suffix = "${nip19.additionalChars} ", route = "Room/${note.author?.pubkeyHex}", - navController = navController + nav = nav ) } else if (channel != null) { CreateClickableText( clickablePart = channel.toBestDisplayName(), suffix = "${nip19.additionalChars} ", - route = "Channel/${note.channel()?.idHex}", - navController = navController + route = "Channel/${channel.idHex}", + nav = nav ) } else { CreateClickableText( clickablePart = "@${note.idDisplayNote()}", suffix = "${nip19.additionalChars} ", route = "Event/${nip19.hex}", - navController = navController + nav = nav ) } } @@ -128,9 +127,9 @@ private fun DisplayEvent( @Composable private fun DisplayNote( nip19: Nip19.Return, - navController: NavController + nav: (String) -> Unit ) { - var noteBase by remember { mutableStateOf(null) } + var noteBase by remember(nip19) { mutableStateOf(null) } LaunchedEffect(key1 = nip19.hex) { withContext(Dispatchers.IO) { @@ -148,28 +147,28 @@ private fun DisplayNote( clickablePart = "@${note.idDisplayNote()}", suffix = "${nip19.additionalChars} ", route = "Channel/${nip19.hex}", - navController = navController + nav = nav ) } else if (note.event is PrivateDmEvent) { CreateClickableText( clickablePart = "@${note.idDisplayNote()}", suffix = "${nip19.additionalChars} ", route = "Room/${note.author?.pubkeyHex}", - navController = navController + nav = nav ) } else if (channel != null) { CreateClickableText( clickablePart = channel.toBestDisplayName(), suffix = "${nip19.additionalChars} ", route = "Channel/${note.channel()?.idHex}", - navController = navController + nav = nav ) } else { CreateClickableText( clickablePart = "@${note.idDisplayNote()}", suffix = "${nip19.additionalChars} ", route = "Note/${nip19.hex}", - navController = navController + nav = nav ) } } @@ -184,9 +183,9 @@ private fun DisplayNote( @Composable private fun DisplayAddress( nip19: Nip19.Return, - navController: NavController + nav: (String) -> Unit ) { - var noteBase by remember { mutableStateOf(null) } + var noteBase by remember(nip19) { mutableStateOf(null) } LaunchedEffect(key1 = nip19.hex) { withContext(Dispatchers.IO) { @@ -196,13 +195,13 @@ private fun DisplayAddress( noteBase?.let { val noteState by it.live().metadata.observeAsState() - val note = noteState?.note ?: return + val note = remember(noteState) { noteState?.note } ?: return CreateClickableText( clickablePart = "@${note.idDisplayNote()}", suffix = "${nip19.additionalChars} ", route = "Note/${nip19.hex}", - navController = navController + nav = nav ) } @@ -216,9 +215,9 @@ private fun DisplayAddress( @Composable private fun DisplayUser( nip19: Nip19.Return, - navController: NavController + nav: (String) -> Unit ) { - var userBase by remember { mutableStateOf(null) } + var userBase by remember(nip19) { mutableStateOf(null) } LaunchedEffect(key1 = nip19.hex) { withContext(Dispatchers.IO) { @@ -228,7 +227,7 @@ private fun DisplayUser( userBase?.let { val userState by it.live().metadata.observeAsState() - val route = remember { "User/${it.pubkeyHex}" } + val route = remember(userState) { "User/${it.pubkeyHex}" } val userDisplayName = remember(userState) { userState?.user?.toBestDisplayName() } val userTags = remember(userState) { userState?.user?.info?.latestMetadata?.tags } @@ -238,7 +237,7 @@ private fun DisplayUser( suffix = "${nip19.additionalChars} ", tags = userTags, route = route, - navController = navController + nav = nav ) } } @@ -257,7 +256,7 @@ fun CreateClickableText( overrideColor: Color? = null, fontWeight: FontWeight = FontWeight.Normal, route: String, - navController: NavController + nav: (String) -> Unit ) { ClickableText( text = buildAnnotatedString { @@ -272,7 +271,7 @@ fun CreateClickableText( append(suffix) } }, - onClick = { navController.navigate(route) } + onClick = { nav(route) } ) } @@ -378,21 +377,21 @@ fun CreateClickableTextWithEmoji( overrideColor: Color? = null, fontWeight: FontWeight = FontWeight.Normal, route: String, - navController: NavController + nav: (String) -> Unit ) { val emojis = remember(tags) { tags?.filter { it.size > 2 && it[0] == "emoji" }?.associate { ":${it[1]}:" to it[2] } ?: emptyMap() } if (emojis.isEmpty()) { - CreateClickableText(clickablePart, suffix, overrideColor, fontWeight, route, navController) + CreateClickableText(clickablePart, suffix, overrideColor, fontWeight, route, nav) } else { val myList = remember { assembleAnnotatedList(clickablePart, emojis) } ClickableInLineIconRenderer(myList, LocalTextStyle.current.copy(color = overrideColor ?: MaterialTheme.colors.primary, fontWeight = fontWeight).toSpanStyle()) { - navController.navigate(route) + nav(route) } val myList2 = remember { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableUserTag.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableUserTag.kt index c56c654cc..8b8ea0ff4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableUserTag.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableUserTag.kt @@ -7,18 +7,17 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.text.AnnotatedString -import androidx.navigation.NavController import com.vitorpamplona.amethyst.model.User @Composable fun ClickableUserTag( user: User, - navController: NavController + nav: (String) -> Unit ) { val innerUserState by user.live().metadata.observeAsState() ClickableText( text = AnnotatedString("@${innerUserState?.user?.toBestDisplayName()}"), - onClick = { navController.navigate("User/${innerUserState?.user?.pubkeyHex}") }, + onClick = { nav("User/${innerUserState?.user?.pubkeyHex}") }, style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary) ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt index 8d622093f..b0f948fb3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt @@ -25,7 +25,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -39,7 +38,7 @@ fun ExpandableRichTextViewer( tags: List>?, backgroundColor: Color, accountViewModel: AccountViewModel, - navController: NavController + nav: (String) -> Unit ) { var showFullText by remember { mutableStateOf(false) } @@ -69,7 +68,7 @@ fun ExpandableRichTextViewer( tags, backgroundColor, accountViewModel, - navController + nav ) if (content.length > whereToCut && !showFullText) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index b2f1b8674..52ad41e47 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -19,14 +19,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.compositeOver +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.navigation.NavController import com.google.accompanist.flowlayout.FlowRow import com.halilibo.richtext.markdown.Markdown import com.halilibo.richtext.markdown.MarkdownParseOptions @@ -43,6 +42,7 @@ import com.vitorpamplona.amethyst.service.lnurl.LnWithdrawalUtil import com.vitorpamplona.amethyst.service.nip19.Nip19 import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.uriToRoute import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -93,19 +93,20 @@ fun RichTextViewer( tags: List>?, backgroundColor: Color, accountViewModel: AccountViewModel, - navController: NavController + nav: (String) -> Unit ) { - val isMarkdown = remember { isMarkdown(content) } + val isMarkdown = remember(content) { isMarkdown(content) } Column(modifier = modifier) { if (isMarkdown) { - RenderContentAsMarkdown(content, backgroundColor) + RenderContentAsMarkdown(content, backgroundColor, tags, nav) } else { - RenderRegular(content, tags, canPreview, backgroundColor, accountViewModel, navController) + RenderRegular(content, tags, canPreview, backgroundColor, accountViewModel, nav) } } } +@Stable class RichTextViewerState( val content: String, val urlSet: Set, @@ -121,10 +122,10 @@ private fun RenderRegular( canPreview: Boolean, backgroundColor: Color, accountViewModel: AccountViewModel, - navController: NavController + nav: (String) -> Unit ) { - var processedState by remember { - mutableStateOf(RichTextViewerState(content, emptySet(), emptyMap(), emptyList(), emptyMap())) + var state by remember(content) { + mutableStateOf(RichTextViewerState(content, emptySet(), emptyMap(), emptyList(), emptyMap())) } val scope = rememberCoroutineScope() @@ -148,80 +149,73 @@ private fun RenderRegular( val emojiMap = tags?.filter { it.size > 2 && it[0] == "emoji" }?.associate { ":${it[1]}:" to it[2] } ?: emptyMap() if (urlSet.isNotEmpty() || emojiMap.isNotEmpty()) { - processedState = RichTextViewerState(content, urlSet, imagesForPager, imageList, emojiMap) + state = RichTextViewerState(content, urlSet, imagesForPager, imageList, emojiMap) } } } // FlowRow doesn't work well with paragraphs. So we need to split them - processedState?.let { state -> - content.split('\n').forEach { paragraph -> - FlowRow() { - val s = if (isArabic(paragraph)) { - paragraph.trim().split(' ') - .reversed() - } else { - paragraph.trim().split(' ') - } - s.forEach { word: String -> - if (canPreview) { - // Explicit URL - val img = state.imagesForPager[word] - if (img != null) { - ZoomableContentView(img, state.imageList) - } else if (state.urlSet.contains(word)) { - UrlPreview(word, "$word ") - } else if (state.customEmoji.any { word.contains(it.key) }) { - RenderCustomEmoji(word, state.customEmoji) - } else if (word.startsWith("lnbc", true)) { - MayBeInvoicePreview(word) - } else if (word.startsWith("lnurl", true)) { - MayBeWithdrawal(word) - } else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) { - ClickableEmail(word) - } else if (word.length > 6 && Patterns.PHONE.matcher(word).matches()) { - ClickablePhone(word) - } else if (isBechLink(word)) { - BechLink( + content.split('\n').forEach { paragraph -> + FlowRow() { + val s = if (isArabic(paragraph)) { + paragraph.trim().split(' ') + .reversed() + } else { + paragraph.trim().split(' ') + } + s.forEach { word: String -> + if (canPreview) { + // Explicit URL + val img = state.imagesForPager[word] + if (img != null) { + ZoomableContentView(img, state.imageList) + } else if (state.urlSet.contains(word)) { + UrlPreview(word, "$word ") + } else if (state.customEmoji.any { word.contains(it.key) }) { + RenderCustomEmoji(word, state.customEmoji) + } else if (word.startsWith("lnbc", true)) { + MayBeInvoicePreview(word) + } else if (word.startsWith("lnurl", true)) { + MayBeWithdrawal(word) + } else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) { + ClickableEmail(word) + } else if (word.length > 6 && Patterns.PHONE.matcher(word).matches()) { + ClickablePhone(word) + } else if (isBechLink(word)) { + BechLink( + word, + canPreview, + backgroundColor, + accountViewModel, + nav + ) + } else if (word.startsWith("#")) { + if (tagIndex.matcher(word).matches() && tags != null) { + TagLink( word, + tags, canPreview, backgroundColor, accountViewModel, - navController + nav ) - } else if (word.startsWith("#")) { - if (tagIndex.matcher(word).matches() && tags != null) { - TagLink( - word, - tags, - canPreview, - backgroundColor, - accountViewModel, - navController - ) - } else if (hashTagsPattern.matcher(word).matches()) { - HashTag(word, navController) - } else { - Text( - text = "$word ", - style = LocalTextStyle.current.copy(textDirection = TextDirection.Content) - ) - } - } else if (noProtocolUrlValidator.matcher(word).matches()) { - val matcher = noProtocolUrlValidator.matcher(word) - matcher.find() - val url = matcher.group(1) // url - val additionalChars = matcher.group(4) ?: "" // additional chars + } else if (hashTagsPattern.matcher(word).matches()) { + HashTag(word, nav) + } else { + Text( + text = "$word ", + style = LocalTextStyle.current.copy(textDirection = TextDirection.Content) + ) + } + } else if (noProtocolUrlValidator.matcher(word).matches()) { + val matcher = noProtocolUrlValidator.matcher(word) + matcher.find() + val url = matcher.group(1) // url + val additionalChars = matcher.group(4) ?: "" // additional chars - if (url != null) { - ClickableUrl(url, "https://$url") - Text("$additionalChars ") - } else { - Text( - text = "$word ", - style = LocalTextStyle.current.copy(textDirection = TextDirection.Content) - ) - } + if (url != null) { + ClickableUrl(url, "https://$url") + Text("$additionalChars ") } else { Text( text = "$word ", @@ -229,71 +223,76 @@ private fun RenderRegular( ) } } else { - if (state.urlSet.contains(word)) { - ClickableUrl("$word ", word) - } else if (word.startsWith("lnurl", true)) { - val lnWithdrawal = LnWithdrawalUtil.findWithdrawal(word) - if (lnWithdrawal != null) { - ClickableWithdrawal(withdrawalString = lnWithdrawal) - } else { - Text( - text = "$word ", - style = LocalTextStyle.current.copy(textDirection = TextDirection.Content) - ) - } - } else if (state.customEmoji.any { word.contains(it.key) }) { - RenderCustomEmoji(word, state.customEmoji) - } else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) { - ClickableEmail(word) - } else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) { - ClickablePhone(word) - } else if (isBechLink(word)) { - BechLink( - word, - canPreview, - backgroundColor, - accountViewModel, - navController - ) - } else if (word.startsWith("#")) { - if (tagIndex.matcher(word).matches() && tags != null) { - TagLink( - word, - tags, - canPreview, - backgroundColor, - accountViewModel, - navController - ) - } else if (hashTagsPattern.matcher(word).matches()) { - HashTag(word, navController) - } else { - Text( - text = "$word ", - style = LocalTextStyle.current.copy(textDirection = TextDirection.Content) - ) - } - } else if (noProtocolUrlValidator.matcher(word).matches()) { - val matcher = noProtocolUrlValidator.matcher(word) - matcher.find() - val url = matcher.group(1) // url - val additionalChars = matcher.group(4) ?: "" // additional chars - - if (url != null) { - ClickableUrl(url, "https://$url") - Text("$additionalChars ") - } else { - Text( - text = "$word ", - style = LocalTextStyle.current.copy(textDirection = TextDirection.Content) - ) - } + Text( + text = "$word ", + style = LocalTextStyle.current.copy(textDirection = TextDirection.Content) + ) + } + } else { + if (state.urlSet.contains(word)) { + ClickableUrl("$word ", word) + } else if (word.startsWith("lnurl", true)) { + val lnWithdrawal = LnWithdrawalUtil.findWithdrawal(word) + if (lnWithdrawal != null) { + ClickableWithdrawal(withdrawalString = lnWithdrawal) } else { Text( text = "$word ", style = LocalTextStyle.current.copy(textDirection = TextDirection.Content) ) } + } else if (state.customEmoji.any { word.contains(it.key) }) { + RenderCustomEmoji(word, state.customEmoji) + } else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) { + ClickableEmail(word) + } else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) { + ClickablePhone(word) + } else if (isBechLink(word)) { + BechLink( + word, + canPreview, + backgroundColor, + accountViewModel, + nav + ) + } else if (word.startsWith("#")) { + if (tagIndex.matcher(word).matches() && tags != null) { + TagLink( + word, + tags, + canPreview, + backgroundColor, + accountViewModel, + nav + ) + } else if (hashTagsPattern.matcher(word).matches()) { + HashTag(word, nav) + } else { + Text( + text = "$word ", + style = LocalTextStyle.current.copy(textDirection = TextDirection.Content) + ) + } + } else if (noProtocolUrlValidator.matcher(word).matches()) { + val matcher = noProtocolUrlValidator.matcher(word) + matcher.find() + val url = matcher.group(1) // url + val additionalChars = matcher.group(4) ?: "" // additional chars + + if (url != null) { + ClickableUrl(url, "https://$url") + Text("$additionalChars ") + } else { + Text( + text = "$word ", + style = LocalTextStyle.current.copy(textDirection = TextDirection.Content) + ) + } + } else { + Text( + text = "$word ", + style = LocalTextStyle.current.copy(textDirection = TextDirection.Content) + ) } } } @@ -310,7 +309,7 @@ fun RenderCustomEmoji(word: String, customEmoji: Map) { } @Composable -private fun RenderContentAsMarkdown(content: String, backgroundColor: Color) { +private fun RenderContentAsMarkdown(content: String, backgroundColor: Color, tags: List>?, nav: (String) -> Unit) { val myMarkDownStyle = richTextDefaults.copy( codeBlockStyle = richTextDefaults.codeBlockStyle?.copy( textStyle = TextStyle( @@ -334,7 +333,6 @@ private fun RenderContentAsMarkdown(content: String, backgroundColor: Color) { ), stringStyle = richTextDefaults.stringStyle?.copy( linkStyle = SpanStyle( - textDecoration = TextDecoration.Underline, color = MaterialTheme.colors.primary ), codeStyle = SpanStyle( @@ -346,20 +344,20 @@ private fun RenderContentAsMarkdown(content: String, backgroundColor: Color) { ) ) - var markdownWithSpecialContent by remember { mutableStateOf(null) } - var nip19References by remember { mutableStateOf>(emptyList()) } - var refresh by remember { mutableStateOf(0) } + var markdownWithSpecialContent by remember(content) { mutableStateOf(null) } + var nip19References by remember(content) { mutableStateOf>(emptyList()) } + var refresh by remember(content) { mutableStateOf(0) } LaunchedEffect(key1 = content) { withContext(Dispatchers.IO) { - nip19References = returnNIP19References(content) - markdownWithSpecialContent = returnMarkdownWithSpecialContent(content) + nip19References = returnNIP19References(content, tags) + markdownWithSpecialContent = returnMarkdownWithSpecialContent(content, tags) } } LaunchedEffect(key1 = refresh) { withContext(Dispatchers.IO) { - val newMarkdownWithSpecialContent = returnMarkdownWithSpecialContent(content) + val newMarkdownWithSpecialContent = returnMarkdownWithSpecialContent(content, tags) if (markdownWithSpecialContent != newMarkdownWithSpecialContent) { markdownWithSpecialContent = newMarkdownWithSpecialContent } @@ -367,8 +365,8 @@ private fun RenderContentAsMarkdown(content: String, backgroundColor: Color) { } nip19References.forEach { - var baseUser by remember { mutableStateOf(null) } - var baseNote by remember { mutableStateOf(null) } + var baseUser by remember(it) { mutableStateOf(null) } + var baseNote by remember(it) { mutableStateOf(null) } LaunchedEffect(key1 = it.hex) { withContext(Dispatchers.IO) { @@ -389,17 +387,23 @@ private fun RenderContentAsMarkdown(content: String, backgroundColor: Color) { baseNote?.let { val noteState by it.live().metadata.observeAsState() if (noteState?.note?.event != null) { - refresh++ + LaunchedEffect(key1 = noteState) { + refresh++ + } } } baseUser?.let { val userState by it.live().metadata.observeAsState() if (userState?.user?.info != null) { - refresh++ + LaunchedEffect(key1 = userState) { + refresh++ + } } } } + val uri = LocalUriHandler.current + markdownWithSpecialContent?.let { MaterialRichText( style = myMarkDownStyle @@ -407,26 +411,70 @@ private fun RenderContentAsMarkdown(content: String, backgroundColor: Color) { Markdown( content = it, markdownParseOptions = MarkdownParseOptions.Default - ) + ) { link -> + val route = uriToRoute(link) + if (route != null) { + nav(route) + } else { + runCatching { uri.openUri(link) } + } + } } } } -private fun getDisplayNameFromNip19(nip19: Nip19.Return): String? { - if (nip19.type == Nip19.Type.USER) { - return LocalCache.users[nip19.hex]?.toBestDisplayName() - } else if (nip19.type == Nip19.Type.NOTE) { - return LocalCache.notes[nip19.hex]?.idDisplayNote() - } else if (nip19.type == Nip19.Type.ADDRESS) { - return LocalCache.addressables[nip19.hex]?.idDisplayNote() - } else if (nip19.type == Nip19.Type.EVENT) { - return LocalCache.notes[nip19.hex]?.idDisplayNote() ?: LocalCache.addressables[nip19.hex]?.idDisplayNote() - } else { - return null +private fun getDisplayNameAndNIP19FromTag(tag: String, tags: List>): Pair? { + val matcher = tagIndex.matcher(tag) + val (index, suffix) = try { + matcher.find() + Pair(matcher.group(1)?.toInt(), matcher.group(2) ?: "") + } catch (e: Exception) { + Log.w("Tag Parser", "Couldn't link tag $tag", e) + Pair(null, null) } + + if (index != null && index >= 0 && index < tags.size) { + val tag = tags[index] + + if (tag.size > 1) { + if (tag[0] == "p") { + LocalCache.checkGetOrCreateUser(tag[1])?.let { + return Pair(it.toBestDisplayName(), it.pubkeyNpub()) + } + } else if (tag[0] == "e" || tag[0] == "a") { + LocalCache.checkGetOrCreateNote(tag[1])?.let { + return Pair(it.idDisplayNote(), it.toNEvent()) + } + } + } + } + + return null } -private fun returnNIP19References(content: String): List { +private fun getDisplayNameFromNip19(nip19: Nip19.Return): Pair? { + if (nip19.type == Nip19.Type.USER) { + LocalCache.users[nip19.hex]?.let { + return Pair(it.toBestDisplayName(), it.pubkeyNpub()) + } + } else if (nip19.type == Nip19.Type.NOTE) { + LocalCache.notes[nip19.hex]?.let { + return Pair(it.idDisplayNote(), it.toNEvent()) + } + } else if (nip19.type == Nip19.Type.ADDRESS) { + LocalCache.addressables[nip19.hex]?.let { + return Pair(it.idDisplayNote(), it.toNEvent()) + } + } else if (nip19.type == Nip19.Type.EVENT) { + LocalCache.notes[nip19.hex]?.let { + return Pair(it.idDisplayNote(), it.toNEvent()) + } + } + + return null +} + +private fun returnNIP19References(content: String, tags: List>?): List { val listOfReferences = mutableListOf() content.split('\n').forEach { paragraph -> paragraph.split(' ').forEach { word: String -> @@ -438,10 +486,21 @@ private fun returnNIP19References(content: String): List { } } } + + tags?.forEach { + if (it[0] == "p" && it.size > 1) { + listOfReferences.add(Nip19.Return(Nip19.Type.USER, it[1], null, null, null, "")) + } else if (it[0] == "e" && it.size > 1) { + listOfReferences.add(Nip19.Return(Nip19.Type.NOTE, it[1], null, null, null, "")) + } else if (it[0] == "a" && it.size > 1) { + listOfReferences.add(Nip19.Return(Nip19.Type.ADDRESS, it[1], null, null, null, "")) + } + } + return listOfReferences } -private fun returnMarkdownWithSpecialContent(content: String): String { +private fun returnMarkdownWithSpecialContent(content: String, tags: List>?): String { var returnContent = "" content.split('\n').forEach { paragraph -> paragraph.split(' ').forEach { word: String -> @@ -454,15 +513,43 @@ private fun returnMarkdownWithSpecialContent(content: String): String { } else if (isBechLink(word)) { val parsedNip19 = Nip19.uriToRoute(word) returnContent += if (parsedNip19 !== null) { - val displayName = getDisplayNameFromNip19(parsedNip19) - if (displayName != null) { - "[@$displayName](nostr://$word) " + val pair = getDisplayNameFromNip19(parsedNip19) + if (pair != null) { + val (displayName, nip19) = pair + "[$displayName](nostr:$nip19) " } else { "$word " } } else { "$word " } + } else if (word.startsWith("#")) { + if (tagIndex.matcher(word).matches() && tags != null) { + val pair = getDisplayNameAndNIP19FromTag(word, tags) + if (pair != null) { + returnContent += "[${pair.first}](nostr:${pair.second}) " + } else { + returnContent += "$word " + } + } else if (hashTagsPattern.matcher(word).matches()) { + val hashtagMatcher = hashTagsPattern.matcher(word) + + val (myTag, mySuffix) = try { + hashtagMatcher.find() + Pair(hashtagMatcher.group(1), hashtagMatcher.group(2)) + } catch (e: Exception) { + Log.e("Hashtag Parser", "Couldn't link hashtag $word", e) + Pair(null, null) + } + + if (myTag != null) { + returnContent += "[#$myTag](nostr:Hashtag?id=$myTag)$mySuffix " + } else { + returnContent += "$word " + } + } else { + returnContent += "$word " + } } else { returnContent += "$word " } @@ -483,12 +570,14 @@ fun isBechLink(word: String): Boolean { } @Composable -fun BechLink(word: String, canPreview: Boolean, backgroundColor: Color, accountViewModel: AccountViewModel, navController: NavController) { +fun BechLink(word: String, canPreview: Boolean, backgroundColor: Color, accountViewModel: AccountViewModel, nav: (String) -> Unit) { var nip19Route by remember { mutableStateOf(null) } var baseNotePair by remember { mutableStateOf?>(null) } + val scope = rememberCoroutineScope() + LaunchedEffect(key1 = word) { - withContext(Dispatchers.IO) { + scope.launch(Dispatchers.IO) { Nip19.uriToRoute(word)?.let { if (it.type == Nip19.Type.NOTE || it.type == Nip19.Type.EVENT || it.type == Nip19.Type.ADDRESS) { LocalCache.checkGetOrCreateNote(it.hex)?.let { note -> @@ -517,7 +606,7 @@ fun BechLink(word: String, canPreview: Boolean, backgroundColor: Color, accountV ), parentBackgroundColor = backgroundColor, isQuotedNote = true, - navController = navController + nav = nav ) if (!it.second.isNullOrEmpty()) { Text( @@ -525,21 +614,23 @@ fun BechLink(word: String, canPreview: Boolean, backgroundColor: Color, accountV ) } } ?: nip19Route?.let { - ClickableRoute(it, navController) + ClickableRoute(it, nav) } ?: Text(text = "$word ") } else { nip19Route?.let { - ClickableRoute(it, navController) + ClickableRoute(it, nav) } ?: Text(text = "$word ") } } @Composable -fun HashTag(word: String, navController: NavController) { +fun HashTag(word: String, nav: (String) -> Unit) { var tagSuffixPair by remember { mutableStateOf?>(null) } + val scope = rememberCoroutineScope() + LaunchedEffect(key1 = word) { - withContext(Dispatchers.IO) { + scope.launch(Dispatchers.IO) { val hashtagMatcher = hashTagsPattern.matcher(word) val (myTag, mySuffix) = try { @@ -557,7 +648,7 @@ fun HashTag(word: String, navController: NavController) { } tagSuffixPair?.let { tagPair -> - val hashtagIcon = checkForHashtagWithIcon(tagPair.first) + val hashtagIcon = remember(tagPair.first) { checkForHashtagWithIcon(tagPair.first) } ClickableText( text = buildAnnotatedString { withStyle( @@ -566,7 +657,7 @@ fun HashTag(word: String, navController: NavController) { append("#${tagPair.first}") } }, - onClick = { navController.navigate("Hashtag/${tagPair.first}") } + onClick = { nav("Hashtag/${tagPair.first}") } ) if (hashtagIcon != null) { @@ -612,7 +703,7 @@ fun HashTag(word: String, navController: NavController) { } @Composable -fun TagLink(word: String, tags: List>, canPreview: Boolean, backgroundColor: Color, accountViewModel: AccountViewModel, navController: NavController) { +fun TagLink(word: String, tags: List>, canPreview: Boolean, backgroundColor: Color, accountViewModel: AccountViewModel, nav: (String) -> Unit) { var baseUserPair by remember { mutableStateOf?>(null) } var baseNotePair by remember { mutableStateOf?>(null) } @@ -664,7 +755,7 @@ fun TagLink(word: String, tags: List>, canPreview: Boolean, backgro suffix = "${it.second} ", tags = userTags, route = route, - navController = navController + nav = nav ) } @@ -684,13 +775,13 @@ fun TagLink(word: String, tags: List>, canPreview: Boolean, backgro ), parentBackgroundColor = backgroundColor, isQuotedNote = true, - navController = navController + nav = nav ) it.second?.ifBlank { null }?.let { Text(text = "$it ") } } else { - ClickableNoteTag(it.first, navController) + ClickableNoteTag(it.first, nav) Text(text = "${it.second} ") } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt index c387bfa5d..fdf9c3099 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt @@ -34,7 +34,9 @@ fun RobohashAsyncImage( ) { val context = LocalContext.current val size = with(LocalDensity.current) { - robotSize.roundToPx() + remember { + robotSize.roundToPx() + } } val imageRequest = remember(robotSize, robot) { @@ -132,6 +134,7 @@ fun RobohashAsyncImageProxy( filterQuality: FilterQuality = DrawScope.DefaultFilterQuality ) { val proxy = remember(model) { model.proxyUrl() } + if (proxy == null) { RobohashAsyncImage( robot = robot, 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 new file mode 100644 index 000000000..f0951a307 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt @@ -0,0 +1,127 @@ +package com.vitorpamplona.amethyst.ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +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.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.rounded.Warning +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 +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +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 com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel + +@Composable +fun SensitivityWarning( + hasSensitiveContent: Boolean, + accountViewModel: AccountViewModel, + content: @Composable () -> Unit +) { + val accountState by accountViewModel.accountLiveData.observeAsState() + + var showContentWarningNote by remember(accountState) { + mutableStateOf(accountState?.account?.showSensitiveContent != true && hasSensitiveContent) + } + + if (showContentWarningNote) { + ContentWarningNote() { + showContentWarningNote = false + } + } else { + content() + } +} + +@Composable +fun ContentWarningNote(onDismiss: () -> Unit) { + Column() { + Row(modifier = Modifier.padding(horizontal = 12.dp)) { + Column(modifier = Modifier.padding(start = 10.dp)) { + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { + Box( + Modifier + .height(80.dp) + .width(90.dp) + ) { + Icon( + imageVector = Icons.Default.Visibility, + contentDescription = stringResource(R.string.content_warning), + modifier = Modifier + .size(70.dp) + .align(Alignment.BottomStart), + tint = MaterialTheme.colors.onBackground + ) + Icon( + imageVector = Icons.Rounded.Warning, + contentDescription = stringResource(R.string.content_warning), + modifier = Modifier + .size(30.dp) + .align(Alignment.TopEnd), + tint = MaterialTheme.colors.onBackground + ) + } + } + + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { + Text( + text = stringResource(R.string.content_warning), + fontWeight = FontWeight.Bold, + fontSize = 18.sp + ) + } + + Row() { + Text( + text = stringResource(R.string.content_warning_explanation), + color = Color.Gray, + modifier = Modifier.padding(top = 10.dp), + textAlign = TextAlign.Center + ) + } + + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { + Button( + modifier = Modifier.padding(top = 10.dp), + onClick = onDismiss, + shape = RoundedCornerShape(20.dp), + colors = ButtonDefaults + .buttonColors( + backgroundColor = MaterialTheme.colors.primary + ), + contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp) + ) { + Text( + text = stringResource(R.string.show_anyway), + color = Color.White + ) + } + } + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreview.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreview.kt index 14fb7bd9f..30bdb93fe 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreview.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreview.kt @@ -10,25 +10,28 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext -import com.baha.url.preview.IUrlPreviewCallback -import com.baha.url.preview.UrlInfoItem import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.UrlCachedPreviewer +import com.vitorpamplona.amethyst.service.previews.IUrlPreviewCallback +import com.vitorpamplona.amethyst.service.previews.UrlInfoItem import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @Composable fun UrlPreview(url: String, urlText: String) { - val default = UrlCachedPreviewer.cache[url]?.let { - if (it.allFetchComplete() && it.url == url) { - UrlPreviewState.Loaded(it) - } else { - UrlPreviewState.Empty - } - } ?: UrlPreviewState.Loading val context = LocalContext.current - var urlPreviewState by remember { mutableStateOf(default) } + var urlPreviewState by remember(url) { + val default = UrlCachedPreviewer.cache[url]?.let { + if (it.allFetchComplete() && it.url == url) { + UrlPreviewState.Loaded(it) + } else { + UrlPreviewState.Empty + } + } ?: UrlPreviewState.Loading + + mutableStateOf(default) + } val scope = rememberCoroutineScope() // Doesn't use a viewModel because of viewModel reusing issues (too many UrlPreview are created). diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewCard.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewCard.kt index 88de0195b..54046bde9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewCard.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewCard.kt @@ -20,8 +20,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil.compose.AsyncImage -import com.baha.url.preview.UrlInfoItem import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.service.previews.UrlInfoItem import java.net.URL @Composable @@ -42,10 +42,10 @@ fun UrlPreviewCard( ) ) { Column { - val validatedUrl = remember { URL(previewInfo.url) } + val validatedUrl = remember(url) { URL(previewInfo.url) } // correctly treating relative images - val imageUrl = remember { + val imageUrl = remember(url) { if (previewInfo.image.startsWith("/")) { URL(validatedUrl, previewInfo.image).toString() } else { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewState.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewState.kt index ee0089e56..e992924e1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewState.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewState.kt @@ -1,6 +1,6 @@ package com.vitorpamplona.amethyst.ui.components -import com.baha.url.preview.UrlInfoItem +import com.vitorpamplona.amethyst.service.previews.UrlInfoItem sealed class UrlPreviewState { object Loading : UrlPreviewState() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt index 4aad1f481..3f8d98a35 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt @@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.ui.dal import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS +import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.model.* object NotificationFeedFilter : AdditiveFeedFilter() { @@ -36,7 +36,7 @@ object NotificationFeedFilter : AdditiveFeedFilter() { (isGlobal || it.author?.pubkeyHex in followingKeySet) && it.event?.isTaggedUser(loggedInUserHex) ?: false && (it.author == null || !account.isHidden(it.author!!.pubkeyHex)) && - tagsAnEventByUser(it, loggedInUser) + tagsAnEventByUser(it, loggedInUserHex) }.toSet() } @@ -44,27 +44,27 @@ object NotificationFeedFilter : AdditiveFeedFilter() { return collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed() } - fun tagsAnEventByUser(note: Note, author: User): Boolean { + fun tagsAnEventByUser(note: Note, authorHex: HexKey): Boolean { val event = note.event if (event is BaseTextNoteEvent) { val isAuthoredPostCited = event.findCitations().any { - LocalCache.notes[it]?.author === author || LocalCache.addressables[it]?.author === author + LocalCache.notes[it]?.author?.pubkeyHex == authorHex || LocalCache.addressables[it]?.author?.pubkeyHex == authorHex } return isAuthoredPostCited || ( - event.citedUsers().contains(author.pubkeyHex) || - note.replyTo?.any { it.author === author } == true + event.citedUsers().contains(authorHex) || + note.replyTo?.any { it.author?.pubkeyHex == authorHex } == true ) } if (event is ReactionEvent) { - return note.replyTo?.lastOrNull()?.author === author + return note.replyTo?.lastOrNull()?.author?.pubkeyHex == authorHex } if (event is RepostEvent) { - return note.replyTo?.lastOrNull()?.author === author + return note.replyTo?.lastOrNull()?.author?.pubkeyHex == authorHex } return true diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt index 0d715b80e..50fe73049 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt @@ -5,6 +5,7 @@ import android.view.ViewTreeObserver import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -33,9 +34,11 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavHostController +import androidx.navigation.compose.currentBackStackEntryAsState import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -82,11 +85,7 @@ fun keyboardAsState(): State { @Composable fun AppBottomBar(navController: NavHostController, accountViewModel: AccountViewModel) { - val currentRoute = currentRoute(navController) - val currentRouteBase = currentRoute?.substringBefore("?") - val coroutineScope = rememberCoroutineScope() val isKeyboardOpen by keyboardAsState() - if (isKeyboardOpen == Keyboard.Closed) { Column() { Divider( @@ -98,37 +97,7 @@ fun AppBottomBar(navController: NavHostController, accountViewModel: AccountView backgroundColor = MaterialTheme.colors.background ) { bottomNavigationItems.forEach { item -> - val selected = currentRouteBase == item.base - - BottomNavigationItem( - icon = { NotifiableIcon(item, selected, accountViewModel) }, - selected = selected, - onClick = { - coroutineScope.launch { - if (currentRouteBase != item.base) { - navController.navigate(item.base) { - navController.graph.startDestinationRoute?.let { start -> - popUpTo(start) - restoreState = true - } - launchSingleTop = true - restoreState = true - } - } else { - val route = currentRoute.replace("{scrollToTop}", "true") - navController.navigate(route) { - navController.graph.startDestinationRoute?.let { start -> - popUpTo(start) { inclusive = item.route == Route.Home.route } - restoreState = true - } - - launchSingleTop = true - restoreState = true - } - } - } - } - ) + HasNewItemsIcon(item, accountViewModel, navController) } } } @@ -136,38 +105,117 @@ fun AppBottomBar(navController: NavHostController, accountViewModel: AccountView } @Composable -private fun NotifiableIcon(route: Route, selected: Boolean, accountViewModel: AccountViewModel) { +private fun RowScope.HasNewItemsIcon( + route: Route, + accountViewModel: AccountViewModel, + navController: NavHostController +) { val scope = rememberCoroutineScope() - Box(Modifier.size(if ("Home" == route.base) 25.dp else 23.dp)) { - Icon( - painter = painterResource(id = route.icon), - contentDescription = null, - modifier = Modifier.size(if ("Home" == route.base) 24.dp else 20.dp), - tint = if (selected) MaterialTheme.colors.primary else Color.Unspecified - ) + val accountState by accountViewModel.accountLiveData.observeAsState() + val account = remember(accountState) { accountState?.account } ?: return - val accountState by accountViewModel.accountLiveData.observeAsState() - val account = accountState?.account ?: return + val notifState by NotificationCache.live.observeAsState() + val notif = remember(notifState) { notifState?.cache } ?: return - val notifState = NotificationCache.live.observeAsState() - val notif = notifState.value ?: return + var hasNewItems by remember { mutableStateOf(false) } - var hasNewItems by remember { mutableStateOf(false) } - - LaunchedEffect(key1 = notif) { - scope.launch(Dispatchers.IO) { - hasNewItems = route.hasNewItems(account, notif.cache, emptySet()) + LaunchedEffect(key1 = notifState, key2 = accountState) { + scope.launch(Dispatchers.IO) { + val newHasNewItems = route.hasNewItems(account, notif, emptySet()) + if (newHasNewItems != hasNewItems) { + hasNewItems = newHasNewItems } } + } - LaunchedEffect(Unit) { - scope.launch(Dispatchers.IO) { - LocalCache.live.newEventBundles.collect { - hasNewItems = route.hasNewItems(account, notif.cache, it) + LaunchedEffect(accountState) { + scope.launch(Dispatchers.IO) { + LocalCache.live.newEventBundles.collect { + val newHasNewItems = route.hasNewItems(account, notif, it) + if (newHasNewItems != hasNewItems) { + hasNewItems = newHasNewItems } } } + } + + BottomIcon( + icon = route.icon, + size = if ("Home" == route.base) 25.dp else 23.dp, + iconSize = if ("Home" == route.base) 24.dp else 20.dp, + base = route.base, + hasNewItems = hasNewItems, + navController + ) { selected -> + scope.launch { + if (!selected) { + navController.navigate(route.base) { + navController.graph.startDestinationRoute?.let { start -> + popUpTo(start) + restoreState = true + } + launchSingleTop = true + restoreState = true + } + } else { + val newRoute = route.route.replace("{scrollToTop}", "true") + navController.navigate(newRoute) { + navController.graph.startDestinationRoute?.let { start -> + popUpTo(start) { inclusive = route.route == Route.Home.route } + restoreState = true + } + + launchSingleTop = true + restoreState = true + } + } + } + } +} + +@Composable +private fun RowScope.BottomIcon( + icon: Int, + size: Dp, + iconSize: Dp, + base: String, + hasNewItems: Boolean, + navController: NavHostController, + onClick: (Boolean) -> Unit +) { + val navBackStackEntry by navController.currentBackStackEntryAsState() + + navBackStackEntry?.let { + val selected = remember(it) { + it.destination.route?.substringBefore("?") == base + } + + BottomNavigationItem( + icon = { + NotifiableIcon( + icon, + size, + iconSize, + selected, + hasNewItems + ) + }, + selected = selected, + onClick = { onClick(selected) } + ) + } +} + +@Composable +private fun NotifiableIcon(icon: Int, size: Dp, iconSize: Dp, selected: Boolean, hasNewItems: Boolean) { + Box(Modifier.size(size)) { + Icon( + painter = painterResource(id = icon), + contentDescription = null, + modifier = Modifier.size(iconSize), + tint = if (selected) MaterialTheme.colors.primary else Color.Unspecified + ) if (hasNewItems) { Box( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index d1005265b..2d5b94007 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -40,6 +40,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.SearchScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.ThreadScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.VideoScreen import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @@ -82,6 +83,14 @@ fun AppNavigation( } } + val nav = remember { + { route: String -> + if (getRouteWithArguments(navController) != route) { + navController.navigate(route) + } + } + } + NavHost(navController, startDestination = Route.Home.route) { Route.Video.let { route -> composable(route.route, route.arguments, content = { @@ -90,13 +99,19 @@ fun AppNavigation( VideoScreen( videoFeedView = videoFeedViewModel, accountViewModel = accountViewModel, - navController = navController, + nav = nav, scrollToTop = scrollToTop ) // Avoids running scroll to top when back button is pressed + // Changes this on a thread to avoid changing before it finishes the composition if (scrollToTop) { - it.arguments?.remove("scrollToTop") + LaunchedEffect(key1 = Unit) { + scope.launch { + delay(1000) + it.arguments?.remove("scrollToTop") + } + } } }) } @@ -108,13 +123,19 @@ fun AppNavigation( SearchScreen( searchFeedViewModel = searchFeedViewModel, accountViewModel = accountViewModel, - navController = navController, + nav = nav, scrollToTop = scrollToTop ) // Avoids running scroll to top when back button is pressed + // Changes this on a thread to avoid changing before it finishes the composition if (scrollToTop) { - it.arguments?.remove("scrollToTop") + LaunchedEffect(key1 = Unit) { + scope.launch { + delay(1000) + it.arguments?.remove("scrollToTop") + } + } } }) } @@ -128,7 +149,7 @@ fun AppNavigation( homeFeedViewModel = homeFeedViewModel, repliesFeedViewModel = repliesFeedViewModel, accountViewModel = accountViewModel, - navController = navController, + nav = nav, pagerState = homePagerState, scrollToTop = scrollToTop, nip47 = nip47 @@ -136,10 +157,20 @@ fun AppNavigation( // Avoids running scroll to top when back button is pressed if (scrollToTop) { - it.arguments?.remove("scrollToTop") + LaunchedEffect(key1 = Unit) { + scope.launch { + delay(1000) + it.arguments?.remove("scrollToTop") + } + } } if (nip47 != null) { - it.arguments?.remove("nip47") + LaunchedEffect(key1 = Unit) { + scope.launch { + delay(1000) + it.arguments?.remove("nip47") + } + } } }) } @@ -152,27 +183,33 @@ fun AppNavigation( notifFeedViewModel = notifFeedViewModel, userReactionsStatsModel = userReactionsStatsModel, accountViewModel = accountViewModel, - navController = navController, + nav = nav, scrollToTop = scrollToTop ) // Avoids running scroll to top when back button is pressed + // Changes this on a thread to avoid changing before it finishes the composition if (scrollToTop) { - it.arguments?.remove("scrollToTop") + LaunchedEffect(key1 = Unit) { + scope.launch { + delay(1000) + it.arguments?.remove("scrollToTop") + } + } } }) } - composable(Route.Message.route, content = { ChatroomListScreen(accountViewModel, navController) }) - composable(Route.BlockedUsers.route, content = { HiddenUsersScreen(accountViewModel, navController) }) - composable(Route.Bookmarks.route, content = { BookmarkListScreen(accountViewModel, navController) }) + composable(Route.Message.route, content = { ChatroomListScreen(accountViewModel, nav) }) + composable(Route.BlockedUsers.route, content = { HiddenUsersScreen(accountViewModel, nav) }) + composable(Route.Bookmarks.route, content = { BookmarkListScreen(accountViewModel, nav) }) Route.Profile.let { route -> composable(route.route, route.arguments, content = { ProfileScreen( userId = it.arguments?.getString("id"), accountViewModel = accountViewModel, - navController = navController + nav = nav ) }) } @@ -182,7 +219,7 @@ fun AppNavigation( ThreadScreen( noteId = it.arguments?.getString("id"), accountViewModel = accountViewModel, - navController = navController + nav = nav ) }) } @@ -192,7 +229,7 @@ fun AppNavigation( HashtagScreen( tag = it.arguments?.getString("id"), accountViewModel = accountViewModel, - navController = navController + nav = nav ) }) } @@ -202,7 +239,7 @@ fun AppNavigation( ChatroomScreen( userId = it.arguments?.getString("id"), accountViewModel = accountViewModel, - navController = navController + nav = nav ) }) } @@ -212,7 +249,7 @@ fun AppNavigation( ChannelScreen( channelId = it.arguments?.getString("id"), accountViewModel = accountViewModel, - navController = navController + nav = nav ) }) } @@ -230,7 +267,7 @@ fun AppNavigation( actionableNextPage?.let { LaunchedEffect(it) { - navController.navigate(it) + nav(it) } actionableNextPage = null } 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 cecad69ec..2c98f68e1 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 @@ -83,7 +83,7 @@ import kotlinx.coroutines.withContext @Composable fun AppTopBar(followLists: FollowListViewModel, navController: NavHostController, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) { when (currentRoute(navController)?.substringBefore("?")) { - // Route.Profile.route -> TopBarWithBackButton(navController) + // Route.Profile.route -> TopBarWithBackButton(nav) Route.Home.base -> HomeTopBar(followLists, scaffoldState, accountViewModel) Route.Video.base -> StoriesTopBar(followLists, scaffoldState, accountViewModel) Route.Notification.base -> NotificationTopBar(followLists, scaffoldState, accountViewModel) 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 31166e172..ac088468b 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 @@ -48,8 +48,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.navigation.NavController -import androidx.navigation.NavHostController import coil.compose.AsyncImage import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.LocalPreferences @@ -69,7 +67,7 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalMaterialApi::class) @Composable fun DrawerContent( - navController: NavHostController, + nav: (String) -> Unit, scaffoldState: ScaffoldState, sheetState: ModalBottomSheetState, accountViewModel: AccountViewModel @@ -89,15 +87,15 @@ fun DrawerContent( .padding(horizontal = 25.dp) .padding(top = 100.dp), scaffoldState, - navController + nav ) Divider( thickness = 0.25.dp, modifier = Modifier.padding(top = 20.dp) ) ListContent( - account.userProfile(), - navController, + account.userProfile().pubkeyHex, + nav, scaffoldState, sheetState, modifier = Modifier @@ -106,7 +104,7 @@ fun DrawerContent( account ) - BottomContent(account.userProfile(), scaffoldState, navController) + BottomContent(account.userProfile(), scaffoldState, nav) } } } @@ -116,21 +114,30 @@ fun ProfileContent( baseAccountUser: User, modifier: Modifier = Modifier, scaffoldState: ScaffoldState, - navController: NavController + nav: (String) -> Unit ) { val coroutineScope = rememberCoroutineScope() val accountUserState by baseAccountUser.live().metadata.observeAsState() - val accountUser = accountUserState?.user ?: return + val accountUser = remember(accountUserState) { accountUserState?.user } ?: return + + val profilePubHex = remember(accountUserState) { accountUserState?.user?.pubkeyHex } ?: return + + val profileBanner = remember(accountUserState) { accountUserState?.user?.info?.banner?.ifBlank { null } } + val profilePicture = remember(accountUserState) { accountUserState?.user?.profilePicture()?.ifBlank { null }?.let { ResizeImage(it, 100.dp) } } + val bestUserName = remember(accountUserState) { accountUserState?.user?.bestUsername() } + val bestDisplayName = remember(accountUserState) { accountUserState?.user?.bestDisplayName() } + val tags = remember(accountUserState) { accountUserState?.user?.info?.latestMetadata?.tags } + val route = remember(accountUserState) { "User/${accountUserState?.user?.pubkeyHex}" } val accountUserFollowsState by baseAccountUser.live().follows.observeAsState() - val accountUserFollows = accountUserFollowsState?.user ?: return + val followingCount = remember(accountUserFollowsState) { accountUserFollowsState?.user?.cachedFollowCount()?.toString() ?: "--" } + val followerCount = remember(accountUserFollowsState) { accountUserFollowsState?.user?.cachedFollowerCount()?.toString() ?: "--" } Box { - val banner = accountUser.info?.banner - if (!banner.isNullOrBlank()) { + if (profileBanner != null) { AsyncImage( - model = banner, + model = profileBanner, contentDescription = stringResource(id = R.string.profile_image), contentScale = ContentScale.FillWidth, modifier = Modifier @@ -149,35 +156,34 @@ fun ProfileContent( } Column(modifier = modifier) { - RobohashAsyncImageProxy( - robot = accountUser.pubkeyHex, - model = ResizeImage(accountUser.profilePicture(), 100.dp), - contentDescription = stringResource(id = R.string.profile_image), - modifier = Modifier - .width(100.dp) - .height(100.dp) - .clip(shape = CircleShape) - .border(3.dp, MaterialTheme.colors.background, CircleShape) - .background(MaterialTheme.colors.background) - .clickable(onClick = { - accountUser.let { - navController.navigate("User/${it.pubkeyHex}") - } - coroutineScope.launch { - scaffoldState.drawerState.close() - } - }) - ) - if (accountUser.bestDisplayName() != null) { + profilePicture?.let { + RobohashAsyncImageProxy( + robot = profilePubHex, + model = profilePicture, + contentDescription = stringResource(id = R.string.profile_image), + modifier = Modifier + .width(100.dp) + .height(100.dp) + .clip(shape = CircleShape) + .border(3.dp, MaterialTheme.colors.background, CircleShape) + .background(MaterialTheme.colors.background) + .clickable(onClick = { + nav(route) + coroutineScope.launch { + scaffoldState.drawerState.close() + } + }) + ) + } + + if (bestDisplayName != null) { CreateTextWithEmoji( - text = accountUser.bestDisplayName() ?: "", - tags = accountUser.info?.latestMetadata?.tags, + text = bestDisplayName, + tags = tags, modifier = Modifier .padding(top = 7.dp) .clickable(onClick = { - accountUser.let { - navController.navigate("User/${it.pubkeyHex}") - } + nav(route) coroutineScope.launch { scaffoldState.drawerState.close() } @@ -186,18 +192,16 @@ fun ProfileContent( fontSize = 18.sp ) } - if (accountUser.bestUsername() != null) { + if (bestUserName != null) { CreateTextWithEmoji( - text = " @${accountUser.bestUsername()}", + text = " @$bestUserName", tags = accountUser.info?.latestMetadata?.tags, color = Color.LightGray, modifier = Modifier .padding(top = 15.dp) .clickable( onClick = { - accountUser.let { - navController.navigate("User/${it.pubkeyHex}") - } + nav(route) coroutineScope.launch { scaffoldState.drawerState.close() } @@ -209,9 +213,7 @@ fun ProfileContent( modifier = Modifier .padding(top = 15.dp) .clickable(onClick = { - accountUser.let { - navController.navigate("User/${it.pubkeyHex}") - } + nav(route) coroutineScope.launch { scaffoldState.drawerState.close() } @@ -219,14 +221,14 @@ fun ProfileContent( ) { Row() { Text( - "${accountUserFollows.cachedFollowCount() ?: "--"}", + text = followingCount, fontWeight = FontWeight.Bold ) Text(stringResource(R.string.following)) } Row(modifier = Modifier.padding(start = 10.dp)) { Text( - "${accountUserFollows.cachedFollowerCount() ?: "--"}", + text = followerCount, fontWeight = FontWeight.Bold ) Text(stringResource(R.string.followers)) @@ -239,8 +241,8 @@ fun ProfileContent( @OptIn(ExperimentalMaterialApi::class) @Composable fun ListContent( - accountUser: User?, - navController: NavHostController, + accountUserPubKey: String?, + nav: (String) -> Unit, scaffoldState: ScaffoldState, sheetState: ModalBottomSheetState, modifier: Modifier, @@ -254,21 +256,21 @@ fun ListContent( var proxyPort = remember { mutableStateOf(account.proxyPort.toString()) } Column(modifier = modifier.fillMaxHeight().verticalScroll(rememberScrollState())) { - if (accountUser != null) { + if (accountUserPubKey != null) { NavigationRow( title = stringResource(R.string.profile), icon = Route.Profile.icon, tint = MaterialTheme.colors.primary, - navController = navController, + nav = nav, scaffoldState = scaffoldState, - route = "User/${accountUser.pubkeyHex}" + route = "User/$accountUserPubKey" ) NavigationRow( title = stringResource(R.string.bookmarks), icon = Route.Bookmarks.icon, tint = MaterialTheme.colors.onBackground, - navController = navController, + nav = nav, scaffoldState = scaffoldState, route = Route.Bookmarks.route ) @@ -278,7 +280,7 @@ fun ListContent( title = stringResource(R.string.security_filters), icon = Route.BlockedUsers.icon, tint = MaterialTheme.colors.onBackground, - navController = navController, + nav = nav, scaffoldState = scaffoldState, route = Route.BlockedUsers.route ) @@ -398,16 +400,13 @@ fun NavigationRow( title: String, icon: Int, tint: Color, - navController: NavHostController, + nav: (String) -> Unit, scaffoldState: ScaffoldState, route: String ) { val coroutineScope = rememberCoroutineScope() - val currentRoute = currentRoute(navController) IconRow(title, icon, tint, onClick = { - if (currentRoute != route) { - navController.navigate(route) - } + nav(route) coroutineScope.launch { scaffoldState.drawerState.close() } @@ -447,7 +446,7 @@ fun IconRow(title: String, icon: Int, tint: Color, onClick: () -> Unit, onLongCl } @Composable -fun BottomContent(user: User, scaffoldState: ScaffoldState, navController: NavController) { +fun BottomContent(user: User, scaffoldState: ScaffoldState, nav: (String) -> Unit) { val coroutineScope = rememberCoroutineScope() // store the dialog open or close state @@ -514,7 +513,7 @@ fun BottomContent(user: User, scaffoldState: ScaffoldState, navController: NavCo coroutineScope.launch { scaffoldState.drawerState.close() } - navController.navigate(it) + nav(it) }, onClose = { dialogOpen = false } ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index a123dc0c4..ee631624c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -1,8 +1,10 @@ package com.vitorpamplona.amethyst.ui.navigation +import android.os.Bundle import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.navigation.NamedNavArgument +import androidx.navigation.NavDestination import androidx.navigation.NavHostController import androidx.navigation.NavType import androidx.navigation.compose.currentBackStackEntryAsState @@ -185,3 +187,48 @@ object MessagesLatestItem { return (note.createdAt() ?: 0) > lastTime } } + +fun getRouteWithArguments(navController: NavHostController): String? { + val currentEntry = navController.currentBackStackEntry ?: return null + return getRouteWithArguments(currentEntry.destination, currentEntry.arguments) +} + +private fun getRouteWithArguments( + destination: NavDestination, + arguments: Bundle? +): String? { + var route = destination.route ?: return null + arguments?.let { bundle -> + destination.arguments.keys.forEach { key -> + val value = destination.arguments[key]?.type?.get(bundle, key)?.toString() + if (value == null) { + val keyStart = route.indexOf("{$key}") + // if it is a parameter, removes the complete segment `var={key}` and adjust connectors `#`, `&` or `&` + if (keyStart > 0 && route[keyStart - 1] == '=') { + val end = keyStart + "{$key}".length + var start = keyStart + for (i in keyStart downTo 0) { + if (route[i] == '#' || route[i] == '?' || route[i] == '&') { + start = i + 1 + break + } + } + if (end < route.length && route[end] == '&') { + route = route.removeRange(start, end + 1) + } else if (end < route.length && route[end] == '#') { + route = route.removeRange(start - 1, end) + } else if (end == route.length) { + route = route.removeRange(start - 1, end) + } else { + route = route.removeRange(start, end) + } + } else { + route = route.replaceFirst("{$key}", "") + } + } else { + route = route.replaceFirst("{$key}", value) + } + } + } + return route +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/AddBountyAmountDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/AddBountyAmountDialog.kt index 4791d00b4..9d94699e6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/AddBountyAmountDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/AddBountyAmountDialog.kt @@ -55,10 +55,11 @@ class AddBountyAmountViewModel : ViewModel() { if (newValue != null) { account?.sendPost( - newValue.toString(), - listOfNotNull(bounty), - listOfNotNull(bounty?.author), - listOf("bounty-added-reward") + message = newValue.toString(), + replyTo = listOfNotNull(bounty), + mentions = listOfNotNull(bounty?.author), + tags = listOf("bounty-added-reward"), + wantsToMarkAsSensitive = false ) nextAmount = TextFieldValue("") diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt index 066777618..6623105b0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt @@ -32,7 +32,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.screen.BadgeCard @@ -42,7 +41,7 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @Composable -fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, navController: NavController) { +fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) { val noteState by likeSetCard.note.live().metadata.observeAsState() val note = noteState?.note @@ -79,7 +78,7 @@ fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForL routeFor( note, accountViewModel.userProfile() - )?.let { navController.navigate(it) } + )?.let { nav(it) } } }, onLongClick = { popupExpanded = true } @@ -149,7 +148,7 @@ fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForL isBoostedNote = true, parentBackgroundColor = backgroundColor, accountViewModel = accountViewModel, - navController = navController + nav = nav ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt index a333a2b6c..a9a95fe6b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt @@ -17,7 +17,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import com.google.accompanist.flowlayout.FlowRow import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note @@ -55,7 +54,7 @@ fun BlankNote(modifier: Modifier = Modifier, isQuote: Boolean = false, idHex: St } @Composable -fun HiddenNote(reports: Set, loggedIn: User, modifier: Modifier = Modifier, isQuote: Boolean = false, navController: NavController, onClick: () -> Unit) { +fun HiddenNote(reports: Set, loggedIn: User, modifier: Modifier = Modifier, isQuote: Boolean = false, nav: (String) -> Unit, onClick: () -> Unit) { Column(modifier = modifier) { Row(modifier = Modifier.padding(horizontal = if (!isQuote) 12.dp else 6.dp)) { Column(modifier = Modifier.padding(start = if (!isQuote) 10.dp else 5.dp)) { @@ -75,7 +74,7 @@ fun HiddenNote(reports: Set, loggedIn: User, modifier: Modifier = Modifier reports.forEach { NoteAuthorPicture( baseNote = it, - navController = navController, + nav = nav, userAccount = loggedIn, size = 35.dp ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BoostSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BoostSetCompose.kt index 10b61d0d6..4e437ea87 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BoostSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BoostSetCompose.kt @@ -25,7 +25,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import com.google.accompanist.flowlayout.FlowRow import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.R @@ -37,7 +36,7 @@ import kotlinx.coroutines.withContext @OptIn(ExperimentalFoundationApi::class) @Composable -fun BoostSetCompose(boostSetCard: BoostSetCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, navController: NavController) { +fun BoostSetCompose(boostSetCard: BoostSetCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) { val noteState by boostSetCard.note.live().metadata.observeAsState() val note = noteState?.note @@ -77,7 +76,7 @@ fun BoostSetCompose(boostSetCard: BoostSetCard, isInnerNote: Boolean = false, ro routeFor( note, account.userProfile() - )?.let { navController.navigate(it) } + )?.let { nav(it) } } }, onLongClick = { popupExpanded = true } @@ -114,7 +113,7 @@ fun BoostSetCompose(boostSetCard: BoostSetCard, isInnerNote: Boolean = false, ro boostSetCard.boostEvents.forEach { NoteAuthorPicture( baseNote = it, - navController = navController, + nav = nav, userAccount = account.userProfile(), size = 35.dp ) @@ -128,7 +127,7 @@ fun BoostSetCompose(boostSetCard: BoostSetCard, isInnerNote: Boolean = false, ro isBoostedNote = true, parentBackgroundColor = backgroundColor, accountViewModel = accountViewModel, - navController = navController + nav = nav ) NoteDropDownMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt index b4711e887..549617784 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt @@ -39,7 +39,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.navigation.NavController import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.LocalCache @@ -57,7 +56,7 @@ import kotlinx.coroutines.launch fun ChatroomCompose( baseNote: Note, accountViewModel: AccountViewModel, - navController: NavController + nav: (String) -> Unit ) { val noteState by baseNote.live().metadata.observeAsState() val note = noteState?.note @@ -128,7 +127,7 @@ fun ChatroomCompose( channelLastTime = note.createdAt(), channelLastContent = "${author?.toBestDisplayName()}: " + description, hasNewMessages = hasNewMessages, - onClick = { navController.navigate("Channel/${chan.idHex}") } + onClick = { nav("Channel/${chan.idHex}") } ) } } else { @@ -172,7 +171,7 @@ fun ChatroomCompose( channelLastTime = note.createdAt(), channelLastContent = accountViewModel.decrypt(note), hasNewMessages = hasNewMessages, - onClick = { navController.navigate("Room/${user.pubkeyHex}") } + onClick = { nav("Room/${user.pubkeyHex}") } ) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index 55fe08669..4452452ab 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -38,8 +37,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.ColorMatrix import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.layout.onSizeChanged @@ -51,7 +48,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.navigation.NavController import com.google.accompanist.flowlayout.FlowRow import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.R @@ -65,8 +61,10 @@ import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.ResizeImage import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage +import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.RelayIconFilter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -82,7 +80,7 @@ fun ChatroomMessageCompose( innerQuote: Boolean = false, parentBackgroundColor: Color? = null, accountViewModel: AccountViewModel, - navController: NavController, + nav: (String) -> Unit, onWantsToReply: (Note) -> Unit ) { val accountState by accountViewModel.accountLiveData.observeAsState() @@ -119,7 +117,7 @@ fun ChatroomMessageCompose( LaunchedEffect(key1 = noteReportsState, key2 = accountState) { withContext(Dispatchers.IO) { account.userProfile().let { loggedIn -> - val newCanPreview = note.author === loggedIn || + val newCanPreview = note.author?.pubkeyHex == loggedIn.pubkeyHex || (note.author?.let { loggedIn.isFollowingCached(it) } ?: true) || !(noteForReports.hasAnyReports()) @@ -138,7 +136,7 @@ fun ChatroomMessageCompose( account.userProfile(), Modifier, innerQuote, - navController, + nav, onClick = { showHiddenNote = true } ) } else { @@ -211,7 +209,7 @@ fun ChatroomMessageCompose( .combinedClickable( onClick = { if (noteEvent is ChannelCreateEvent) { - navController.navigate("Channel/${note.idHex}") + nav("Channel/${note.idHex}") } }, onLongClick = { popupExpanded = true } @@ -230,7 +228,7 @@ fun ChatroomMessageCompose( DrawAuthorInfo( baseNote, alignment, - navController + nav ) } else { Spacer(modifier = Modifier.height(5.dp)) @@ -246,7 +244,7 @@ fun ChatroomMessageCompose( innerQuote = true, parentBackgroundColor = backgroundBubbleColor, accountViewModel = accountViewModel, - navController = navController, + nav = nav, onWantsToReply = onWantsToReply ) } @@ -270,7 +268,7 @@ fun ChatroomMessageCompose( isAcceptableAndCanPreview.second, backgroundBubbleColor, accountViewModel, - navController + nav ) } } @@ -364,31 +362,37 @@ private fun RenderRegularTextNote( canPreview: Boolean, backgroundBubbleColor: Color, accountViewModel: AccountViewModel, - navController: NavController + nav: (String) -> Unit ) { val tags = remember { note.event?.tags() } val eventContent = remember { accountViewModel.decrypt(note) } val modifier = remember { Modifier.padding(top = 5.dp) } if (eventContent != null) { - TranslatableRichTextViewer( - eventContent, - canPreview, - modifier, - tags, - backgroundBubbleColor, - accountViewModel, - navController - ) + val hasSensitiveContent = remember(note.event) { note.event?.isSensitive() ?: false } + SensitivityWarning( + hasSensitiveContent = hasSensitiveContent, + accountViewModel = accountViewModel + ) { + TranslatableRichTextViewer( + content = eventContent, + canPreview = canPreview, + modifier = modifier, + tags = tags, + backgroundColor = backgroundBubbleColor, + accountViewModel = accountViewModel, + nav = nav + ) + } } else { TranslatableRichTextViewer( - stringResource(R.string.could_not_decrypt_the_message), - true, - modifier, - tags, - backgroundBubbleColor, - accountViewModel, - navController + content = stringResource(id = R.string.could_not_decrypt_the_message), + canPreview = true, + modifier = modifier, + tags = tags, + backgroundColor = backgroundBubbleColor, + accountViewModel = accountViewModel, + nav = nav ) } } @@ -445,7 +449,7 @@ private fun RenderCreateChannelNote(note: Note) { private fun DrawAuthorInfo( baseNote: Note, alignment: Arrangement.Horizontal, - navController: NavController + nav: (String) -> Unit ) { val userState by baseNote.author!!.live().metadata.observeAsState() @@ -469,7 +473,7 @@ private fun DrawAuthorInfo( .height(25.dp) .clip(shape = CircleShape) .clickable(onClick = { - navController.navigate(route) + nav(route) }) ) @@ -480,7 +484,7 @@ private fun DrawAuthorInfo( fontWeight = FontWeight.Bold, overrideColor = MaterialTheme.colors.onBackground, route = route, - navController = navController + nav = nav ) } } @@ -505,7 +509,7 @@ private fun RelayBadges(baseNote: Note) { var expanded by remember { mutableStateOf(false) } - val relaysToDisplay by remember { + val relaysToDisplay by remember(noteRelaysState) { derivedStateOf { if (expanded) state.noteRelays else state.noteRelaysSimple } @@ -533,31 +537,27 @@ private fun RelayBadges(baseNote: Note) { } @Composable -private fun RenderRelay(dirtyUrl: String) { +fun RenderRelay(dirtyUrl: String) { val uri = LocalUriHandler.current - val website = remember { - val cleanUrl = dirtyUrl.removePrefix("wss://").removePrefix("ws://") + val website = remember(dirtyUrl) { + val cleanUrl = dirtyUrl.trim().removePrefix("wss://").removePrefix("ws://").removeSuffix("/") "https://$cleanUrl" } - val iconUrl = remember { - val cleanUrl = dirtyUrl.removePrefix("wss://").removePrefix("ws://") + val iconUrl = remember(dirtyUrl) { + val cleanUrl = dirtyUrl.trim().removePrefix("wss://").removePrefix("ws://").removeSuffix("/") "https://$cleanUrl/favicon.ico" } - val clickableModifier = remember { + val clickableModifier = remember(dirtyUrl) { Modifier - .size(15.dp) .padding(1.dp) + .size(15.dp) .clickable(onClick = { uri.openUri(website) }) } - val colorFilter = remember { - ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0.5f) }) - } - - val iconModifier = remember { + val iconModifier = remember(dirtyUrl) { Modifier - .fillMaxSize(1f) + .size(13.dp) .clip(shape = CircleShape) } @@ -566,10 +566,10 @@ private fun RenderRelay(dirtyUrl: String) { ) { RobohashFallbackAsyncImage( robot = iconUrl, - robotSize = 15.dp, + robotSize = 13.dp, model = iconUrl, contentDescription = stringResource(id = R.string.relay_icon), - colorFilter = colorFilter, + colorFilter = RelayIconFilter, modifier = iconModifier.background(MaterialTheme.colors.background) ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/LikeSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/LikeSetCompose.kt index eb22f8631..ee5a90b93 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/LikeSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/LikeSetCompose.kt @@ -25,7 +25,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import com.google.accompanist.flowlayout.FlowRow import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.R @@ -37,7 +36,7 @@ import kotlinx.coroutines.withContext @OptIn(ExperimentalFoundationApi::class) @Composable -fun LikeSetCompose(likeSetCard: LikeSetCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, navController: NavController) { +fun LikeSetCompose(likeSetCard: LikeSetCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) { val noteState by likeSetCard.note.live().metadata.observeAsState() val note = noteState?.note @@ -76,7 +75,7 @@ fun LikeSetCompose(likeSetCard: LikeSetCard, isInnerNote: Boolean = false, route routeFor( note, account.userProfile() - )?.let { navController.navigate(it) } + )?.let { nav(it) } } }, onLongClick = { popupExpanded = true } @@ -113,7 +112,7 @@ fun LikeSetCompose(likeSetCard: LikeSetCard, isInnerNote: Boolean = false, route likeSetCard.likeEvents.forEach { NoteAuthorPicture( baseNote = it, - navController = navController, + nav = nav, userAccount = account.userProfile(), size = 35.dp ) @@ -127,7 +126,7 @@ fun LikeSetCompose(likeSetCard: LikeSetCard, isInnerNote: Boolean = false, route isBoostedNote = true, parentBackgroundColor = backgroundColor, accountViewModel = accountViewModel, - navController = navController + nav = nav ) NoteDropDownMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt index c893e0439..0cb22db4c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt @@ -25,7 +25,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.screen.MessageSetCard @@ -35,10 +34,15 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @Composable -fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String, accountViewModel: AccountViewModel, navController: NavController) { - val noteState by messageSetCard.note.live().metadata.observeAsState() +fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) { + val baseNote = remember { messageSetCard.note } + + val noteState by baseNote.live().metadata.observeAsState() val note = remember(noteState) { noteState?.note } + val accountState by accountViewModel.accountLiveData.observeAsState() + val loggedIn = remember(accountState) { accountState?.account?.userProfile() } ?: return + var popupExpanded by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() @@ -79,9 +83,9 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String, onClick = { scope.launch { routeFor( - note, - accountViewModel.userProfile() - )?.let { navController.navigate(it) } + baseNote, + loggedIn + )?.let { nav(it) } } }, onLongClick = { popupExpanded = true } @@ -91,24 +95,17 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String, Column(columnModifier) { Row(Modifier.fillMaxWidth()) { - Box(modifier = remember { Modifier.width(55.dp).padding(top = 5.dp, end = 5.dp) }) { - Icon( - painter = painterResource(R.drawable.ic_dm), - null, - modifier = remember { Modifier.size(16.dp).align(Alignment.TopEnd) }, - tint = MaterialTheme.colors.primary - ) - } + MessageIcon() Column(modifier = remember { Modifier.padding(start = 10.dp) }) { NoteCompose( - baseNote = messageSetCard.note, + baseNote = baseNote, routeForLastRead = null, isBoostedNote = true, addMarginTop = false, parentBackgroundColor = backgroundColor, accountViewModel = accountViewModel, - navController = navController + nav = nav ) NoteDropDownMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel) @@ -117,3 +114,25 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String, } } } + +@Composable +private fun MessageIcon() { + Box( + modifier = remember { + Modifier + .width(55.dp) + .padding(top = 5.dp, end = 5.dp) + } + ) { + Icon( + painter = painterResource(R.drawable.ic_dm), + null, + modifier = remember { + Modifier + .size(16.dp) + .align(Alignment.TopEnd) + }, + tint = MaterialTheme.colors.primary + ) + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt index 86dd8e055..f6fd7b1a1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt @@ -8,10 +8,12 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.Text @@ -19,7 +21,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Bolt import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf @@ -28,42 +30,48 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.navigation.NavController +import androidx.compose.ui.unit.sp import com.google.accompanist.flowlayout.FlowRow import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.UserState +import com.vitorpamplona.amethyst.service.model.LnZapEvent import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.screen.MultiSetCard import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.showAmountAxis import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableMap import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @Composable -fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accountViewModel: AccountViewModel, navController: NavController) { - val noteState by multiSetCard.note.live().metadata.observeAsState() - val note = remember(noteState) { noteState?.note } +fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) { + val baseNote = remember { multiSetCard.note } + + val noteState by baseNote.live().metadata.observeAsState() + val note = remember(noteState) { noteState?.note } ?: return val accountState by accountViewModel.accountLiveData.observeAsState() - val account = remember(accountState) { accountState?.account } + val loggedIn = remember(accountState) { accountState?.account?.userProfile() } ?: return var popupExpanded by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() - if (note == null || account == null) { + if (note.event == null) { BlankNote(Modifier, false) } else { var isNew by remember { mutableStateOf(false) } @@ -80,62 +88,64 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun } } + val primaryColor = MaterialTheme.colors.primary.copy(0.12f) + val defaultBackgroundColor = MaterialTheme.colors.background + val backgroundColor = if (isNew) { - MaterialTheme.colors.primary.copy(0.12f).compositeOver(MaterialTheme.colors.background) + primaryColor.compositeOver(defaultBackgroundColor) } else { - MaterialTheme.colors.background + defaultBackgroundColor } - val columnModifier = remember(isNew) { - Modifier - .background(backgroundColor) - .padding( - start = 12.dp, - end = 12.dp, - top = 10.dp - ) - .combinedClickable( - onClick = { - scope.launch { - routeFor( - note, - account.userProfile() - )?.let { navController.navigate(it) } - } - }, - onLongClick = { popupExpanded = true } - ) - .fillMaxWidth() - } + val columnModifier = Modifier + .background(backgroundColor) + .padding( + start = 12.dp, + end = 12.dp, + top = 10.dp + ) + .combinedClickable( + onClick = { + scope.launch { + routeFor(baseNote, loggedIn)?.let { nav(it) } + } + }, + onLongClick = { popupExpanded = true } + ) + .fillMaxWidth() - val zapEvents = remember { multiSetCard.zapEvents } - val boostEvents = remember { multiSetCard.boostEvents } - val likeEvents = remember { multiSetCard.likeEvents } + val zapEvents by remember { derivedStateOf { multiSetCard.zapEvents } } + val boostEvents by remember { derivedStateOf { multiSetCard.boostEvents } } + val likeEvents by remember { derivedStateOf { multiSetCard.likeEvents } } + + val hasZapEvents by remember { derivedStateOf { multiSetCard.zapEvents.isNotEmpty() } } + val hasBoostEvents by remember { derivedStateOf { multiSetCard.boostEvents.isNotEmpty() } } + val hasLikeEvents by remember { derivedStateOf { multiSetCard.likeEvents.isNotEmpty() } } Column(modifier = columnModifier) { - if (zapEvents.isNotEmpty()) { - RenderZapGallery(zapEvents, navController, account, accountViewModel) + if (hasZapEvents) { + RenderZapGallery(zapEvents, backgroundColor, nav, accountViewModel) } - if (boostEvents.isNotEmpty()) { - RenderBoostGallery(boostEvents, navController, account, accountViewModel) + if (hasBoostEvents) { + RenderBoostGallery(boostEvents, backgroundColor, nav, accountViewModel) } - if (likeEvents.isNotEmpty()) { - RenderLikeGallery(likeEvents, navController, account, accountViewModel) + if (hasLikeEvents) { + RenderLikeGallery(likeEvents, backgroundColor, nav, accountViewModel) } Row(Modifier.fillMaxWidth()) { Spacer(modifier = Modifier.width(65.dp)) NoteCompose( - baseNote = multiSetCard.note, + baseNote = baseNote, routeForLastRead = null, modifier = Modifier.padding(top = 5.dp), isBoostedNote = true, parentBackgroundColor = backgroundColor, accountViewModel = accountViewModel, - navController = navController + nav = nav ) NoteDropDownMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel) @@ -146,9 +156,9 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun @Composable private fun RenderLikeGallery( - likeEvents: List, - navController: NavController, - account: Account, + likeEvents: ImmutableList, + backgroundColor: Color, + nav: (String) -> Unit, accountViewModel: AccountViewModel ) { Row(Modifier.fillMaxWidth()) { @@ -171,15 +181,15 @@ private fun RenderLikeGallery( ) } - AuthorGallery(likeEvents, navController, account, accountViewModel) + AuthorGallery(likeEvents, backgroundColor, nav, accountViewModel) } } @Composable private fun RenderZapGallery( - zapEvents: Map, - navController: NavController, - account: Account, + zapEvents: ImmutableMap, + backgroundColor: Color, + nav: (String) -> Unit, accountViewModel: AccountViewModel ) { Row(Modifier.fillMaxWidth()) { @@ -202,18 +212,22 @@ private fun RenderZapGallery( ) } - AuthorGalleryZaps(zapEvents, navController, account, accountViewModel) + AuthorGalleryZaps(zapEvents, backgroundColor, nav, accountViewModel) } } @Composable private fun RenderBoostGallery( - boostEvents: List, - navController: NavController, - account: Account, + boostEvents: ImmutableList, + backgroundColor: Color, + nav: (String) -> Unit, accountViewModel: AccountViewModel ) { - Row(Modifier.fillMaxWidth()) { + Row( + modifier = remember { + Modifier.fillMaxWidth() + } + ) { Box( modifier = remember { Modifier @@ -226,34 +240,30 @@ private fun RenderBoostGallery( null, modifier = remember { Modifier - .size(18.dp) + .size(19.dp) .align(Alignment.TopEnd) }, tint = Color.Unspecified ) } - AuthorGallery(boostEvents, navController, account, accountViewModel) + AuthorGallery(boostEvents, backgroundColor, nav, accountViewModel) } } @Composable fun AuthorGalleryZaps( - authorNotes: Map, - navController: NavController, - account: Account, + authorNotes: ImmutableMap, + backgroundColor: Color, + nav: (String) -> Unit, accountViewModel: AccountViewModel ) { - val accountState = account.userProfile().live().follows.observeAsState() - - val listToRender = remember { - authorNotes.keys.take(50) - } - Column(modifier = Modifier.padding(start = 10.dp)) { FlowRow() { - listToRender.forEach { - AuthorPictureAndComment(it, navController, accountState, accountViewModel) + authorNotes.forEach { + Box() { + AuthorPictureAndComment(it.key, it.value, backgroundColor, nav, accountViewModel) + } } } } @@ -262,52 +272,72 @@ fun AuthorGalleryZaps( @Composable private fun AuthorPictureAndComment( zapRequest: Note, - navController: NavController, - accountUser: State, + zapEvent: Note?, + backgroundColor: Color, + nav: (String) -> Unit, accountViewModel: AccountViewModel ) { - val author = zapRequest.author ?: return - - var content by remember { mutableStateOf>(Pair(author, null)) } + var content by remember { mutableStateOf>(Triple(zapRequest.author, null, null)) } val scope = rememberCoroutineScope() - LaunchedEffect(key1 = zapRequest.idHex) { - scope.launch(Dispatchers.IO) { + LaunchedEffect(key1 = zapRequest.idHex, key2 = zapEvent?.idHex) { + scope.launch(Dispatchers.Default) { (zapRequest.event as? LnZapRequestEvent)?.let { val decryptedContent = accountViewModel.decryptZap(zapRequest) + val amount = (zapEvent?.event as? LnZapEvent)?.amount if (decryptedContent != null) { val newAuthor = LocalCache.getOrCreateUser(decryptedContent.pubKey) - content = Pair(newAuthor, decryptedContent.content) + content = Triple(newAuthor, decryptedContent.content.ifBlank { null }, showAmountAxis(amount)) } else { - if (!zapRequest.event?.content().isNullOrBlank()) { - content = Pair(author, zapRequest.event?.content()) + if (!zapRequest.event?.content().isNullOrBlank() || amount != null) { + content = Triple(zapRequest.author, zapRequest.event?.content()?.ifBlank { null }, showAmountAxis(amount)) } } } } } - AuthorPictureAndComment(content.first, content.second, navController, accountUser, accountViewModel) + content.first?.let { + val route by remember { + derivedStateOf { + "User/${it.pubkeyHex}" + } + } + + AuthorPictureAndComment( + author = it, + comment = content.second, + amount = content.third, + route = route, + backgroundColor = backgroundColor, + nav = nav, + accountViewModel = accountViewModel + ) + } } @Composable private fun AuthorPictureAndComment( author: User, comment: String?, - navController: NavController, - accountUser: State, + amount: String?, + route: String, + backgroundColor: Color, + nav: (String) -> Unit, accountViewModel: AccountViewModel ) { + val authorPictureModifier = remember { Modifier } + val modifier = remember(comment) { - if (!comment.isNullOrBlank()) { + if (comment != null) { Modifier .fillMaxWidth() .clickable { - navController.navigate("User/${author.pubkeyHex}") + nav(route) } } else { Modifier.clickable { - navController.navigate("User/${author.pubkeyHex}") + nav(route) } } } @@ -316,22 +346,44 @@ private fun AuthorPictureAndComment( modifier = modifier, verticalAlignment = Alignment.CenterVertically ) { - FastNoteAuthorPicture( - author = author, - userAccount = accountUser, - size = 35.dp - ) + Box(modifier = remember { Modifier.size(35.dp) }, contentAlignment = Alignment.BottomCenter) { + FastNoteAuthorPicture( + author = author, + size = 35.dp, + accountViewModel = accountViewModel, + pictureModifier = authorPictureModifier + ) - if (!comment.isNullOrBlank()) { + amount?.let { + Box(modifier = Modifier.fillMaxSize().clip(shape = CircleShape), contentAlignment = Alignment.BottomCenter) { + Box( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colors.background.copy(0.62f)), + contentAlignment = Alignment.BottomCenter + ) { + Text( + text = it, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colors.secondaryVariant, + fontSize = 12.sp, + modifier = Modifier.padding(bottom = 1.dp) + ) + } + } + } + } + + comment?.let { Spacer(modifier = Modifier.width(5.dp)) TranslatableRichTextViewer( - content = comment, + content = it, canPreview = true, tags = null, modifier = Modifier.weight(1f), - backgroundColor = MaterialTheme.colors.background, + backgroundColor = backgroundColor, accountViewModel = accountViewModel, - navController = navController + nav = nav ) } } @@ -339,38 +391,50 @@ private fun AuthorPictureAndComment( @Composable fun AuthorGallery( - authorNotes: Collection, - navController: NavController, - account: Account, + authorNotes: ImmutableList, + backgroundColor: Color, + nav: (String) -> Unit, accountViewModel: AccountViewModel ) { - val accountState = account.userProfile().live().follows.observeAsState() - val listToRender = remember { - Pair( - authorNotes.take(50).mapNotNull { it.author }, - authorNotes.size - ) - } - Column(modifier = Modifier.padding(start = 10.dp)) { FlowRow() { - listToRender.first.forEach { author -> - AuthorPictureAndComment(author, null, navController, accountState, accountViewModel) - } - - if (listToRender.second > 50) { - Text(" and ${listToRender.second - 50} others") + authorNotes.forEach { note -> + Box(Modifier.size(35.dp)) { + NotePictureAndComment(note, backgroundColor, nav, accountViewModel) + } } } } } +@Composable +private fun NotePictureAndComment( + baseNote: Note, + backgroundColor: Color, + nav: (String) -> Unit, + accountViewModel: AccountViewModel +) { + val author by remember(baseNote) { + derivedStateOf { + baseNote.author + } + } + + val route by remember(baseNote) { + derivedStateOf { + "User/${baseNote.author?.pubkeyHex}" + } + } + + author?.let { AuthorPictureAndComment(it, null, null, route, backgroundColor, nav, accountViewModel) } +} + @Composable fun FastNoteAuthorPicture( author: User, - userAccount: State, size: Dp, - pictureModifier: Modifier = Modifier + pictureModifier: Modifier = Modifier, + accountViewModel: AccountViewModel ) { val userState by author.live().metadata.observeAsState() val profilePicture = remember(userState) { @@ -381,8 +445,15 @@ fun FastNoteAuthorPicture( author.pubkeyHex } - val showFollowingMark = remember(userAccount.value) { - userAccount.value?.user?.isFollowingCached(author) == true || (author === userAccount.value?.user) + val accountState by accountViewModel.accountLiveData.observeAsState() + val loggedInLiveFollows = remember(accountState) { accountState?.account?.userProfile()?.live()?.follows } ?: return + + val accountFollowsState by loggedInLiveFollows.observeAsState() + + val showFollowingMark by remember(accountFollowsState) { + derivedStateOf { + accountFollowsState?.user?.isFollowingCached(author) == true || (author.pubkeyHex == accountFollowsState?.user?.pubkeyHex) + } } UserPicture( 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 31dc03ec1..4940819b7 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 @@ -19,6 +19,7 @@ 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.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -34,38 +35,55 @@ import com.vitorpamplona.amethyst.model.UserMetadata import com.vitorpamplona.amethyst.service.Nip05Verifier import com.vitorpamplona.amethyst.ui.theme.Nip05 import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext +import kotlinx.coroutines.launch import java.util.Date @Composable fun nip05VerificationAsAState(user: UserMetadata, pubkeyHex: String): State { - var nip05Verified = remember { mutableStateOf(null) } + val nip05Verified = remember(user) { + // starts with null if must verify or already filled in if verified in the last hour + val default = if ((user.nip05LastVerificationTime ?: 0) > (Date().time / 1000 - 60 * 60)) { // 1hour + user.nip05Verified + } else { + null + } + + mutableStateOf(default) + } + + val scope = rememberCoroutineScope() LaunchedEffect(key1 = user) { - withContext(Dispatchers.IO) { - user.nip05?.ifBlank { null }?.let { nip05 -> - val now = Date().time / 1000 - if ((user.nip05LastVerificationTime ?: 0) > (now - 60 * 60)) { // 1hour - nip05Verified.value = user.nip05Verified - } else { + if (nip05Verified.value == null) { + scope.launch(Dispatchers.IO) { + user.nip05?.ifBlank { null }?.let { nip05 -> Nip05Verifier().verifyNip05( nip05, onSuccess = { // Marks user as verified if (it == pubkeyHex) { user.nip05Verified = true - user.nip05LastVerificationTime = now - nip05Verified.value = true + user.nip05LastVerificationTime = Date().time / 1000 + + if (nip05Verified.value != true) { + nip05Verified.value = true + } } else { user.nip05Verified = false user.nip05LastVerificationTime = 0 - nip05Verified.value = false + + if (nip05Verified.value != false) { + nip05Verified.value = false + } } }, onError = { user.nip05LastVerificationTime = 0 user.nip05Verified = false - nip05Verified.value = false + + if (nip05Verified.value != false) { + nip05Verified.value = false + } } ) } @@ -90,16 +108,14 @@ fun ObserveDisplayNip05Status(baseNote: Note, columnModifier: Modifier = Modifie @Composable fun ObserveDisplayNip05Status(baseUser: User, columnModifier: Modifier = Modifier) { val userState by baseUser.live().metadata.observeAsState() - val user = userState?.user ?: return + val user = remember(userState) { userState?.user } ?: return + val parts = remember(userState) { userState?.user?.nip05()?.split("@") } ?: return - user.nip05()?.let { nip05 -> - val parts = nip05.split("@") - if (parts.size == 2) { - val nip05Verified by nip05VerificationAsAState(user.info!!, user.pubkeyHex) + if (parts.size == 2) { + val nip05Verified by nip05VerificationAsAState(user.info!!, user.pubkeyHex) - Column(modifier = columnModifier) { - DisplayNIP05(parts[0], parts[1], nip05Verified) - } + Column(modifier = columnModifier) { + DisplayNIP05(parts[0], parts[1], nip05Verified) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index b6c279f1f..86ce4ff2b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -57,14 +57,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.ColorMatrix import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.graphics.luminance import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString @@ -77,7 +74,6 @@ import androidx.compose.ui.unit.sp import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.get -import androidx.navigation.NavController import coil.compose.AsyncImage import coil.compose.AsyncImagePainter import com.google.accompanist.flowlayout.FlowRow @@ -116,7 +112,7 @@ import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status import com.vitorpamplona.amethyst.ui.components.ResizeImage import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy -import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage +import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.components.VideoView import com.vitorpamplona.amethyst.ui.components.ZoomableContent @@ -139,10 +135,8 @@ import java.io.File import java.math.BigDecimal import java.net.URL import java.util.Locale -import kotlin.time.ExperimentalTime -import kotlin.time.measureTimedValue -@OptIn(ExperimentalTime::class) +@OptIn(ExperimentalFoundationApi::class) @Composable fun NoteCompose( baseNote: Note, @@ -155,41 +149,7 @@ fun NoteCompose( addMarginTop: Boolean = true, parentBackgroundColor: Color? = null, accountViewModel: AccountViewModel, - navController: NavController -) { - val (value, elapsed) = measureTimedValue { - NoteComposeInner( - baseNote, - routeForLastRead, - modifier, - isBoostedNote, - isQuotedNote, - unPackReply, - makeItShort, - addMarginTop, - parentBackgroundColor, - accountViewModel, - navController - ) - } - - Log.d("Time", "Note Compose in $elapsed for ${baseNote.idHex} ${baseNote.event?.kind()} ${baseNote.event?.content()?.split("\n")?.get(0)?.take(100)}") -} - -@OptIn(ExperimentalFoundationApi::class, ExperimentalTime::class) -@Composable -fun NoteComposeInner( - baseNote: Note, - routeForLastRead: String? = null, - modifier: Modifier = Modifier, - isBoostedNote: Boolean = false, - isQuotedNote: Boolean = false, - unPackReply: Boolean = true, - makeItShort: Boolean = false, - addMarginTop: Boolean = true, - parentBackgroundColor: Color? = null, - accountViewModel: AccountViewModel, - navController: NavController + nav: (String) -> Unit ) { val accountState by accountViewModel.accountLiveData.observeAsState() val account = remember(accountState) { accountState?.account } ?: return @@ -203,6 +163,7 @@ fun NoteComposeInner( val noteEvent = remember(noteState) { note.event } val baseChannel = remember(noteState) { note.channel() } + val isSensitive = remember(noteState) { note.event?.isSensitive() ?: false } var popupExpanded by remember { mutableStateOf(false) } @@ -220,7 +181,7 @@ fun NoteComposeInner( note.let { NoteQuickActionMenu(it, popupExpanded, { popupExpanded = false }, accountViewModel) } - } else if (account.isHidden(noteForReports.author!!)) { + } else if (account.isHidden(noteForReports.author!!) || (isSensitive && account.showSensitiveContent == false)) { // Does nothing } else { var showHiddenNote by remember { mutableStateOf(false) } @@ -229,7 +190,7 @@ fun NoteComposeInner( LaunchedEffect(key1 = noteReportsState, key2 = accountState) { withContext(Dispatchers.IO) { account.userProfile().let { loggedIn -> - val newCanPreview = note.author === loggedIn || + val newCanPreview = note.author?.pubkeyHex == loggedIn.pubkeyHex || (note.author?.let { loggedIn.isFollowingCached(it) } ?: true) || !(noteForReports.hasAnyReports()) @@ -248,11 +209,11 @@ fun NoteComposeInner( account.userProfile(), modifier, isBoostedNote, - navController, + nav, onClick = { showHiddenNote = true } ) } else if ((noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) && baseChannel != null) { - ChannelHeader(baseChannel = baseChannel, account = account, navController = navController) + ChannelHeader(baseChannel = baseChannel, account = account, nav = nav) } else if (noteEvent is BadgeDefinitionEvent) { BadgeDisplay(baseNote = note) } else if (noteEvent is FileHeaderEvent) { @@ -282,15 +243,19 @@ fun NoteComposeInner( } } - val backgroundColor = if (isNew) { - val newColor = MaterialTheme.colors.primary.copy(0.12f) - if (parentBackgroundColor != null) { - newColor.compositeOver(parentBackgroundColor) + val primaryColor = MaterialTheme.colors.primary.copy(0.12f) + val defaultBackgroundColor = MaterialTheme.colors.background + + val backgroundColor = remember(isNew, parentBackgroundColor) { + if (isNew) { + if (parentBackgroundColor != null) { + primaryColor.compositeOver(parentBackgroundColor) + } else { + primaryColor.compositeOver(defaultBackgroundColor) + } } else { - newColor.compositeOver(MaterialTheme.colors.background) + parentBackgroundColor ?: defaultBackgroundColor } - } else { - parentBackgroundColor ?: MaterialTheme.colors.background } val columnModifier = remember(backgroundColor) { @@ -299,7 +264,7 @@ fun NoteComposeInner( onClick = { scope.launch { routeFor(note, loggedIn)?.let { - navController.navigate(it) + nav(it) } } }, @@ -320,7 +285,7 @@ fun NoteComposeInner( } ) { if (!isBoostedNote && !isQuotedNote) { - DrawAuthorImages(baseNote, loggedIn, navController) + DrawAuthorImages(baseNote, loggedIn, nav) } Column( @@ -334,14 +299,14 @@ fun NoteComposeInner( showAuthorPicture = isQuotedNote, account = account, accountViewModel = accountViewModel, - navController = navController + nav = nav ) if (noteEvent !is RepostEvent && !makeItShort && !isQuotedNote) { SecondUserInfoRow( note, account, - navController + nav ) } @@ -354,17 +319,17 @@ fun NoteComposeInner( backgroundColor, account, accountViewModel, - navController + nav ) } when (noteEvent) { is ReactionEvent -> { - RenderReaction(note, backgroundColor, accountViewModel, navController) + RenderReaction(note, backgroundColor, accountViewModel, nav) } is RepostEvent -> { - RenderRepost(note, backgroundColor, accountViewModel, navController) + RenderRepost(note, backgroundColor, accountViewModel, nav) } is ReportEvent -> { @@ -372,31 +337,31 @@ fun NoteComposeInner( } is LongTextNoteEvent -> { - RenderLongFormContent(note, loggedIn, accountViewModel, navController) + RenderLongFormContent(note, loggedIn, accountViewModel, nav) } is BadgeAwardEvent -> { - RenderBadgeAward(note, backgroundColor, accountViewModel, navController) + RenderBadgeAward(note, backgroundColor, accountViewModel, nav) } is PeopleListEvent -> { - RenderPeopleList(noteState, backgroundColor, accountViewModel, navController) + RenderPeopleList(noteState, backgroundColor, accountViewModel, nav) } is AudioTrackEvent -> { - RenderAudioTrack(note, loggedIn, accountViewModel, navController) + RenderAudioTrack(note, loggedIn, accountViewModel, nav) } is PinListEvent -> { - RenderPinListEvent(noteState, backgroundColor, accountViewModel, navController) + RenderPinListEvent(noteState, backgroundColor, accountViewModel, nav) } is PrivateDmEvent -> { - RenderPrivateMessage(note, makeItShort, isAcceptableAndCanPreview.second, backgroundColor, accountViewModel, navController) + RenderPrivateMessage(note, makeItShort, isAcceptableAndCanPreview.second, backgroundColor, accountViewModel, nav) } is HighlightEvent -> { - RenderHighlight(note, makeItShort, isAcceptableAndCanPreview.second, backgroundColor, accountViewModel, navController) + RenderHighlight(note, makeItShort, isAcceptableAndCanPreview.second, backgroundColor, accountViewModel, nav) } is PollNoteEvent -> { @@ -406,7 +371,7 @@ fun NoteComposeInner( isAcceptableAndCanPreview.second, backgroundColor, accountViewModel, - navController + nav ) } @@ -417,7 +382,7 @@ fun NoteComposeInner( isAcceptableAndCanPreview.second, backgroundColor, accountViewModel, - navController + nav ) } } @@ -466,15 +431,12 @@ private fun RenderTextEvent( canPreview: Boolean, backgroundColor: Color, accountViewModel: AccountViewModel, - navController: NavController + nav: (String) -> Unit ) { - val tags = remember { note.event?.tags() } - val hashtags = remember { note.event?.hashtags() ?: emptyList() } - val eventContent = remember { accountViewModel.decrypt(note) } - val modifier = remember { Modifier.fillMaxWidth() } - val isAuthorTheLoggedUser = remember { accountViewModel.isLoggedUser(note.author) } + val eventContent = remember(note.event) { accountViewModel.decrypt(note) } if (eventContent != null) { + val isAuthorTheLoggedUser = remember(note.event) { accountViewModel.isLoggedUser(note.author) } if (makeItShort && isAuthorTheLoggedUser) { Text( text = eventContent, @@ -483,22 +445,33 @@ private fun RenderTextEvent( overflow = TextOverflow.Ellipsis ) } else { - TranslatableRichTextViewer( - content = eventContent, - canPreview = canPreview && !makeItShort, - modifier = modifier, - tags = tags, - backgroundColor = backgroundColor, - accountViewModel = accountViewModel, - navController = navController - ) + val hasSensitiveContent = remember(note.event) { note.event?.isSensitive() ?: false } - DisplayUncitedHashtags(hashtags, eventContent, navController) + SensitivityWarning( + hasSensitiveContent = hasSensitiveContent, + accountViewModel = accountViewModel + ) { + val modifier = remember(note.event) { Modifier.fillMaxWidth() } + val tags = remember(note.event) { note.event?.tags() } + + TranslatableRichTextViewer( + content = eventContent, + canPreview = canPreview && !makeItShort, + modifier = modifier, + tags = tags, + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav + ) + } + + val hashtags = remember(note.event) { note.event?.hashtags() ?: emptyList() } + DisplayUncitedHashtags(hashtags, eventContent, nav) } } if (!makeItShort) { - ReactionsRow(note, accountViewModel, navController) + ReactionsRow(note, accountViewModel, nav) } Divider( @@ -514,7 +487,7 @@ private fun RenderPoll( canPreview: Boolean, backgroundColor: Color, accountViewModel: AccountViewModel, - navController: NavController + nav: (String) -> Unit ) { val noteEvent = note.event as? PollNoteEvent ?: return val eventContent = noteEvent.content() @@ -527,29 +500,35 @@ private fun RenderPoll( overflow = TextOverflow.Ellipsis ) } else { - TranslatableRichTextViewer( - eventContent, - canPreview = canPreview && !makeItShort, - Modifier.fillMaxWidth(), - noteEvent.tags(), - backgroundColor, - accountViewModel, - navController - ) + val hasSensitiveContent = remember(note.event) { note.event?.isSensitive() ?: false } + SensitivityWarning( + hasSensitiveContent = hasSensitiveContent, + accountViewModel = accountViewModel + ) { + TranslatableRichTextViewer( + eventContent, + canPreview = canPreview && !makeItShort, + Modifier.fillMaxWidth(), + noteEvent.tags(), + backgroundColor, + accountViewModel, + nav + ) - DisplayUncitedHashtags(noteEvent.hashtags(), eventContent, navController) + PollNote( + note, + canPreview = canPreview && !makeItShort, + backgroundColor, + accountViewModel, + nav + ) + } - PollNote( - note, - canPreview = canPreview && !makeItShort, - backgroundColor, - accountViewModel, - navController - ) + DisplayUncitedHashtags(noteEvent.hashtags(), eventContent, nav) } if (!makeItShort) { - ReactionsRow(note, accountViewModel, navController) + ReactionsRow(note, accountViewModel, nav) } Divider( @@ -565,7 +544,7 @@ private fun RenderHighlight( canPreview: Boolean, backgroundColor: Color, accountViewModel: AccountViewModel, - navController: NavController + nav: (String) -> Unit ) { val noteEvent = note.event as? HighlightEvent ?: return @@ -577,11 +556,11 @@ private fun RenderHighlight( canPreview, backgroundColor, accountViewModel, - navController + nav ) if (!makeItShort) { - ReactionsRow(note, accountViewModel, navController) + ReactionsRow(note, accountViewModel, nav) } Divider( @@ -597,16 +576,21 @@ private fun RenderPrivateMessage( canPreview: Boolean, backgroundColor: Color, accountViewModel: AccountViewModel, - navController: NavController + nav: (String) -> Unit ) { val noteEvent = note.event as? PrivateDmEvent ?: return + val withMe = remember { noteEvent.with(accountViewModel.userProfile().pubkeyHex) } + val tags = remember(note.event?.id()) { note.event?.tags() } + val hashtags = remember(note.event?.id()) { note.event?.hashtags() ?: emptyList() } + val modifier = remember(note.event?.id()) { Modifier.fillMaxWidth() } + val isAuthorTheLoggedUser = remember(note.event?.id()) { accountViewModel.isLoggedUser(note.author) } if (withMe) { val eventContent = remember { accountViewModel.decrypt(note) } if (eventContent != null) { - if (makeItShort && accountViewModel.isLoggedUser(note.author)) { + if (makeItShort && isAuthorTheLoggedUser) { Text( text = eventContent, color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), @@ -614,17 +598,23 @@ private fun RenderPrivateMessage( overflow = TextOverflow.Ellipsis ) } else { - TranslatableRichTextViewer( - content = eventContent, - canPreview = canPreview && !makeItShort, - modifier = Modifier.fillMaxWidth(), - tags = noteEvent.tags(), - backgroundColor = backgroundColor, - accountViewModel = accountViewModel, - navController = navController - ) + val hasSensitiveContent = remember(note.event) { note.event?.isSensitive() ?: false } + SensitivityWarning( + hasSensitiveContent = hasSensitiveContent, + accountViewModel = accountViewModel + ) { + TranslatableRichTextViewer( + content = eventContent, + canPreview = canPreview && !makeItShort, + modifier = modifier, + tags = tags, + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav + ) + } - DisplayUncitedHashtags(noteEvent.hashtags(), eventContent, navController) + DisplayUncitedHashtags(hashtags, eventContent, nav) } } } else { @@ -641,12 +631,12 @@ private fun RenderPrivateMessage( noteEvent.tags(), backgroundColor, accountViewModel, - navController + nav ) } if (!makeItShort) { - ReactionsRow(note, accountViewModel, navController) + ReactionsRow(note, accountViewModel, nav) } Divider( @@ -660,12 +650,12 @@ fun RenderPeopleList( noteState: NoteState?, backgroundColor: Color, accountViewModel: AccountViewModel, - navController: NavController + nav: (String) -> Unit ) { - DisplayPeopleList(noteState, backgroundColor, accountViewModel, navController) + DisplayPeopleList(noteState, backgroundColor, accountViewModel, nav) noteState?.note?.let { - ReactionsRow(it, accountViewModel, navController) + ReactionsRow(it, accountViewModel, nav) } Divider( @@ -679,7 +669,7 @@ fun DisplayPeopleList( noteState: NoteState?, backgroundColor: Color, accountViewModel: AccountViewModel, - navController: NavController + nav: (String) -> Unit ) { val note = remember(noteState) { noteState?.note } ?: return val noteEvent = note.event as? PeopleListEvent ?: return @@ -724,7 +714,7 @@ fun DisplayPeopleList( user, overallModifier = Modifier, accountViewModel = accountViewModel, - navController = navController + nav = nav ) } } @@ -768,7 +758,7 @@ private fun RenderBadgeAward( note: Note, backgroundColor: Color, accountViewModel: AccountViewModel, - navController: NavController + nav: (String) -> Unit ) { if (note.replyTo.isNullOrEmpty()) return @@ -793,7 +783,7 @@ private fun RenderBadgeAward( modifier = Modifier .size(size = 35.dp) .clickable { - navController.navigate("User/${user.pubkeyHex}") + nav("User/${user.pubkeyHex}") }, verticalAlignment = Alignment.CenterVertically ) { @@ -819,11 +809,11 @@ private fun RenderBadgeAward( unPackReply = false, parentBackgroundColor = backgroundColor, accountViewModel = accountViewModel, - navController = navController + nav = nav ) } - ReactionsRow(note, accountViewModel, navController) + ReactionsRow(note, accountViewModel, nav) Divider( modifier = Modifier.padding(top = 10.dp), @@ -836,7 +826,7 @@ private fun RenderReaction( note: Note, backgroundColor: Color, accountViewModel: AccountViewModel, - navController: NavController + nav: (String) -> Unit ) { note.replyTo?.lastOrNull()?.let { NoteCompose( @@ -846,7 +836,7 @@ private fun RenderReaction( unPackReply = false, parentBackgroundColor = backgroundColor, accountViewModel = accountViewModel, - navController = navController + nav = nav ) } @@ -864,7 +854,7 @@ private fun RenderRepost( note: Note, backgroundColor: Color, accountViewModel: AccountViewModel, - navController: NavController + nav: (String) -> Unit ) { val boostedNote = remember { note.replyTo?.lastOrNull() @@ -878,7 +868,7 @@ private fun RenderRepost( unPackReply = false, parentBackgroundColor = backgroundColor, accountViewModel = accountViewModel, - navController = navController + nav = nav ) } } @@ -888,13 +878,13 @@ private fun RenderPinListEvent( noteState: NoteState?, backgroundColor: Color, accountViewModel: AccountViewModel, - navController: NavController + nav: (String) -> Unit ) { val noteEvent = noteState?.note?.event as? PinListEvent ?: return - PinListHeader(noteState, backgroundColor, accountViewModel, navController) + PinListHeader(noteState, backgroundColor, accountViewModel, nav) - ReactionsRow(noteState?.note, accountViewModel, navController) + ReactionsRow(noteState?.note, accountViewModel, nav) Divider( modifier = Modifier.padding(top = 10.dp), @@ -907,7 +897,7 @@ fun PinListHeader( noteState: NoteState?, backgroundColor: Color, accountViewModel: AccountViewModel, - navController: NavController + nav: (String) -> Unit ) { val note = remember(noteState) { noteState?.note } ?: return val noteEvent = note.event as? PinListEvent ?: return @@ -954,7 +944,7 @@ fun PinListHeader( tags = emptyList(), backgroundColor = backgroundColor, accountViewModel = accountViewModel, - navController = navController + nav = nav ) } } @@ -998,13 +988,13 @@ private fun RenderAudioTrack( note: Note, loggedIn: User, accountViewModel: AccountViewModel, - navController: NavController + nav: (String) -> Unit ) { val noteEvent = note.event as? AudioTrackEvent ?: return - AudioTrackHeader(noteEvent, note, loggedIn, navController) + AudioTrackHeader(noteEvent, note, loggedIn, nav) - ReactionsRow(note, accountViewModel, navController) + ReactionsRow(note, accountViewModel, nav) Divider( modifier = Modifier.padding(top = 10.dp), @@ -1017,13 +1007,13 @@ private fun RenderLongFormContent( note: Note, loggedIn: User, accountViewModel: AccountViewModel, - navController: NavController + nav: (String) -> Unit ) { val noteEvent = note.event as? LongTextNoteEvent ?: return LongFormHeader(noteEvent, note, loggedIn) - ReactionsRow(note, accountViewModel, navController) + ReactionsRow(note, accountViewModel, nav) Divider( modifier = Modifier.padding(top = 10.dp), @@ -1070,7 +1060,7 @@ private fun ReplyRow( backgroundColor: Color, account: Account, accountViewModel: AccountViewModel, - navController: NavController + nav: (String) -> Unit ) { val noteEvent = note.event @@ -1096,16 +1086,16 @@ private fun ReplyRow( parentBackgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.05f) .compositeOver(backgroundColor), accountViewModel = accountViewModel, - navController = navController + nav = nav ) } else { - ReplyInformation(note.replyTo, noteEvent.mentions(), account, navController) + ReplyInformation(note.replyTo, noteEvent.mentions(), account, nav) } Spacer(modifier = Modifier.height(5.dp)) } else if (noteEvent is ChannelMessageEvent && (note.replyTo != null || noteEvent.hasAnyTaggedUser())) { note.channel()?.let { - ReplyInformationChannel(note.replyTo, noteEvent.mentions(), it, account, navController) + ReplyInformationChannel(note.replyTo, noteEvent.mentions(), it, account, nav) } Spacer(modifier = Modifier.height(5.dp)) @@ -1116,7 +1106,7 @@ private fun ReplyRow( private fun SecondUserInfoRow( note: Note, account: Account, - navController: NavController + nav: (String) -> Unit ) { val noteEvent = remember { note.event } ?: return val noteAuthor = remember { note.author } ?: return @@ -1126,7 +1116,7 @@ private fun SecondUserInfoRow( val baseReward = remember { noteEvent.getReward() } if (baseReward != null) { - DisplayReward(baseReward, note, account, navController) + DisplayReward(baseReward, note, account, nav) } val pow = remember { noteEvent.getPoWRank() } @@ -1142,7 +1132,7 @@ private fun FirstUserInfoRow( showAuthorPicture: Boolean, account: Account, accountViewModel: AccountViewModel, - navController: NavController + nav: (String) -> Unit ) { var moreActionsExpanded by remember { mutableStateOf(false) } val eventNote = remember { baseNote.event } ?: return @@ -1154,7 +1144,7 @@ private fun FirstUserInfoRow( Row(verticalAlignment = Alignment.CenterVertically) { if (showAuthorPicture) { - NoteAuthorPicture(baseNote, navController, loggedIn, 25.dp) + NoteAuthorPicture(baseNote, nav, loggedIn, 25.dp) Spacer(padding) NoteUsernameDisplay(baseNote, Modifier.weight(1f)) } else { @@ -1168,7 +1158,7 @@ private fun FirstUserInfoRow( color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ) } else { - DisplayFollowingHashtagsInPost(eventNote, account, navController) + DisplayFollowingHashtagsInPost(eventNote, account, nav) } TimeAgo(time) @@ -1197,12 +1187,14 @@ private fun FirstUserInfoRow( @Composable fun TimeAgo(time: Long) { val context = LocalContext.current - var timeStr by remember { mutableStateOf("") } LaunchedEffect(key1 = time) { withContext(Dispatchers.IO) { - timeStr = timeAgo(time, context = context) + val newTimeStr = timeAgo(time, context = context) + if (newTimeStr != timeStr) { + timeStr = newTimeStr + } } } @@ -1214,17 +1206,17 @@ fun TimeAgo(time: Long) { } @Composable -private fun DrawAuthorImages(baseNote: Note, loggedIn: User, navController: NavController) { +private fun DrawAuthorImages(baseNote: Note, loggedIn: User, nav: (String) -> Unit) { val baseChannel = remember { baseNote.channel() } val modifier = remember { Modifier.width(55.dp) } Column(modifier) { // Draws the boosted picture outside the boosted card. Box(modifier = modifier, contentAlignment = Alignment.BottomEnd) { - NoteAuthorPicture(baseNote, navController, loggedIn, 55.dp) + NoteAuthorPicture(baseNote, nav, loggedIn, 55.dp) if (baseNote.event is RepostEvent) { - RepostNoteAuthorPicture(baseNote, navController, loggedIn) + RepostNoteAuthorPicture(baseNote, nav, loggedIn) } if (baseNote.event is ChannelMessageEvent && baseChannel != null) { @@ -1288,7 +1280,7 @@ private fun ChannelNotePicture(baseChannel: Channel) { @Composable private fun RepostNoteAuthorPicture( baseNote: Note, - navController: NavController, + nav: (String) -> Unit, loggedIn: User ) { val baseRepost = remember { baseNote.replyTo?.lastOrNull() } @@ -1303,7 +1295,7 @@ private fun RepostNoteAuthorPicture( Box(modifier) { NoteAuthorPicture( it, - navController, + nav, loggedIn, 35.dp, pictureModifier = Modifier.border( @@ -1325,7 +1317,7 @@ fun DisplayHighlight( canPreview: Boolean, backgroundColor: Color, accountViewModel: AccountViewModel, - navController: NavController + nav: (String) -> Unit ) { val quote = remember { @@ -1342,7 +1334,7 @@ fun DisplayHighlight( emptyList(), backgroundColor, accountViewModel, - navController + nav ) var userBase by remember { mutableStateOf(null) } @@ -1366,10 +1358,10 @@ fun DisplayHighlight( if (userDisplayName != null) { CreateClickableTextWithEmoji( clickablePart = userDisplayName, - suffix = "", + suffix = " ", tags = userTags, route = route, - navController = navController + nav = nav ) } } @@ -1397,7 +1389,7 @@ fun DisplayHighlight( fun DisplayFollowingHashtagsInPost( noteEvent: EventInterface, account: Account, - navController: NavController + nav: (String) -> Unit ) { var firstTag by remember { mutableStateOf(null) } @@ -1412,7 +1404,7 @@ fun DisplayFollowingHashtagsInPost( firstTag?.let { ClickableText( text = AnnotatedString(" #$firstTag"), - onClick = { navController.navigate("Hashtag/$firstTag") }, + onClick = { nav("Hashtag/$firstTag") }, style = LocalTextStyle.current.copy( color = MaterialTheme.colors.primary.copy( alpha = 0.52f @@ -1428,7 +1420,7 @@ fun DisplayFollowingHashtagsInPost( fun DisplayUncitedHashtags( hashtags: List, eventContent: String, - navController: NavController + nav: (String) -> Unit ) { if (hashtags.isNotEmpty()) { FlowRow( @@ -1438,7 +1430,7 @@ fun DisplayUncitedHashtags( if (!eventContent.contains(hashtag, true)) { ClickableText( text = AnnotatedString("#$hashtag "), - onClick = { navController.navigate("Hashtag/$hashtag") }, + onClick = { nav("Hashtag/$hashtag") }, style = LocalTextStyle.current.copy( color = MaterialTheme.colors.primary.copy( alpha = 0.52f @@ -1470,7 +1462,7 @@ fun DisplayReward( baseReward: BigDecimal, baseNote: Note, account: Account, - navController: NavController + nav: (String) -> Unit ) { var popupExpanded by remember { mutableStateOf(false) } @@ -1481,7 +1473,7 @@ fun DisplayReward( ) { ClickableText( text = AnnotatedString("#bounty"), - onClick = { navController.navigate("Hashtag/bounty") }, + onClick = { nav("Hashtag/bounty") }, style = LocalTextStyle.current.copy( color = MaterialTheme.colors.primary.copy( alpha = 0.52f @@ -1702,7 +1694,7 @@ fun FileStorageHeaderDisplay(baseNote: Note) { } @Composable -fun AudioTrackHeader(noteEvent: AudioTrackEvent, note: Note, loggedIn: User, navController: NavController) { +fun AudioTrackHeader(noteEvent: AudioTrackEvent, note: Note, loggedIn: User, nav: (String) -> Unit) { val media = remember { noteEvent.media() } val cover = remember { noteEvent.cover() } val subject = remember { noteEvent.subject() } @@ -1739,7 +1731,7 @@ fun AudioTrackHeader(noteEvent: AudioTrackEvent, note: Note, loggedIn: User, nav modifier = Modifier .padding(top = 5.dp, start = 10.dp, end = 10.dp) .clickable { - navController.navigate("User/${it.second.pubkeyHex}") + nav("User/${it.second.pubkeyHex}") } ) { UserPicture(it.second, loggedIn, 25.dp) @@ -1870,7 +1862,7 @@ private fun LongFormHeader(noteEvent: LongTextNoteEvent, note: Note, loggedIn: U @Composable private fun RelayBadges(baseNote: Note) { val noteRelaysState by baseNote.live().relays.observeAsState() - val noteRelays = noteRelaysState?.note ?: return + val noteRelays = remember(noteRelaysState) { noteRelaysState?.note } ?: return var expanded by remember { mutableStateOf(false) } var showShowMore by remember { mutableStateOf(false) } @@ -1916,46 +1908,11 @@ private fun VerticalRelayPanelWithFlow( // FlowRow Seems to be a lot faster than LazyVerticalGrid FlowRow() { relays.forEach { url -> - RelayIconCompose(url) + RenderRelay(url) } } } -@Composable -@Stable -private fun RelayIconCompose(url: String) { - val uri = LocalUriHandler.current - - val model = remember(url) { "https://$url/favicon.ico" } - - val boxModifier = remember { - Modifier - .padding(1.dp) - .size(15.dp) - } - val iconModifier = remember(url) { - Modifier - .width(13.dp) - .height(13.dp) - .clip(shape = CircleShape) - .clickable(onClick = { uri.openUri("https://$url") }) - } - val colorFilter = remember { - ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0.5f) }) - } - - Box(boxModifier) { - RobohashFallbackAsyncImage( - robot = model, - robotSize = 15.dp, - model = model, - contentDescription = stringResource(R.string.relay_icon), - colorFilter = colorFilter, - modifier = iconModifier.background(MaterialTheme.colors.background) - ) - } -} - @Composable private fun ShowMoreRelaysButton(onClick: () -> Unit) { val boxModifier = remember { @@ -1988,13 +1945,13 @@ private fun ShowMoreRelaysButton(onClick: () -> Unit) { @Composable fun NoteAuthorPicture( baseNote: Note, - navController: NavController, + nav: (String) -> Unit, userAccount: User, size: Dp, pictureModifier: Modifier = Modifier ) { NoteAuthorPicture(baseNote, userAccount, size, pictureModifier) { - navController.navigate("User/${it.pubkeyHex}") + nav("User/${it.pubkeyHex}") } } @@ -2041,13 +1998,13 @@ fun NoteAuthorPicture( @Composable fun UserPicture( user: User, - navController: NavController, + nav: (String) -> Unit, userAccount: User, size: Dp, pictureModifier: Modifier = Modifier ) { UserPicture(user, userAccount, size, pictureModifier) { - navController.navigate("User/${it.pubkeyHex}") + nav("User/${it.pubkeyHex}") } } @@ -2057,7 +2014,7 @@ fun UserPicture( baseUser: User, baseUserAccount: User, size: Dp, - modifier: Modifier = Modifier, + modifier: Modifier = remember { Modifier }, onClick: ((User) -> Unit)? = null, onLongClick: ((User) -> Unit)? = null ) { @@ -2073,7 +2030,7 @@ fun UserPicture( } val showFollowingMark = remember(accountState) { - accountState?.user?.isFollowingCached(baseUser) == true || baseUser === accountState?.user + accountState?.user?.isFollowingCached(baseUser) == true || baseUser.pubkeyHex == accountState?.user?.pubkeyHex } // BaseUser is the same reference as accountState.user @@ -2150,7 +2107,9 @@ fun UserPicture( } val myIconModifier = remember { - Modifier.fillMaxSize() + Modifier + .width(size.div(3.5f)) + .height(size.div(3.5f)) } Box(myIconBoxModifier, contentAlignment = Alignment.Center) { @@ -2173,7 +2132,9 @@ data class DropDownParams( val isFollowingAuthor: Boolean, val isPrivateBookmarkNote: Boolean, val isPublicBookmarkNote: Boolean, - val isLoggedUser: Boolean + val isLoggedUser: Boolean, + val isSensitive: Boolean, + val showSensitiveContent: Boolean? ) @Composable @@ -2184,20 +2145,23 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit, var reportDialogShowing by remember { mutableStateOf(false) } val bookmarkState by accountViewModel.userProfile().live().bookmarks.observeAsState() + val accountState by accountViewModel.accountLiveData.observeAsState() var state by remember { mutableStateOf( - DropDownParams(false, false, false, false) + DropDownParams(false, false, false, false, false, null) ) } - LaunchedEffect(key1 = note, key2 = bookmarkState) { + LaunchedEffect(key1 = note, key2 = bookmarkState, key3 = accountState) { withContext(Dispatchers.IO) { state = DropDownParams( - accountViewModel.isFollowing(note.author), - accountViewModel.isInPrivateBookmarks(note), - accountViewModel.isInPublicBookmarks(note), - accountViewModel.isLoggedUser(note.author) + isFollowingAuthor = accountViewModel.isFollowing(note.author), + isPrivateBookmarkNote = accountViewModel.isInPrivateBookmarks(note), + isPublicBookmarkNote = accountViewModel.isInPublicBookmarks(note), + isLoggedUser = accountViewModel.isLoggedUser(note.author), + isSensitive = note.event?.isSensitive() ?: false, + showSensitiveContent = accountState?.account?.showSensitiveContent ) } } @@ -2275,6 +2239,24 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit, Text("Block / Report") } } + // if (state.isSensitive) { + Divider() + if (state.showSensitiveContent == null || state.showSensitiveContent == true) { + DropdownMenuItem(onClick = { accountViewModel.hideSensitiveContent(); onDismiss() }) { + Text(stringResource(R.string.content_warning_hide_all_sensitive_content)) + } + } + if (state.showSensitiveContent == null || state.showSensitiveContent == false) { + DropdownMenuItem(onClick = { accountViewModel.disableContentWarnings(); onDismiss() }) { + Text(stringResource(R.string.content_warning_show_all_sensitive_content)) + } + } + if (state.showSensitiveContent != null) { + DropdownMenuItem(onClick = { accountViewModel.seeContentWarnings(); onDismiss() }) { + Text(stringResource(R.string.content_warning_see_warnings)) + } + } + // } } if (reportDialogShowing) { 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 73c75812c..2c9ab94bb 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 @@ -116,10 +116,10 @@ fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Uni val cardShape = RoundedCornerShape(5.dp) val clipboardManager = LocalClipboardManager.current val scope = rememberCoroutineScope() - var showSelectTextDialog by remember { mutableStateOf(false) } - var showDeleteAlertDialog by remember { mutableStateOf(false) } - var showBlockAlertDialog by remember { mutableStateOf(false) } - var showReportDialog by remember { mutableStateOf(false) } + var showSelectTextDialog by remember(note) { mutableStateOf(false) } + var showDeleteAlertDialog by remember(note) { mutableStateOf(false) } + var showBlockAlertDialog by remember(note) { mutableStateOf(false) } + var showReportDialog by remember(note) { mutableStateOf(false) } val backgroundColor = if (MaterialTheme.colors.isLight) { MaterialTheme.colors.primary diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt index 12342c292..ca545bb3f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt @@ -29,7 +29,6 @@ import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.Popup import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavController import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer @@ -47,12 +46,14 @@ fun PollNote( canPreview: Boolean, backgroundColor: Color, accountViewModel: AccountViewModel, - navController: NavController + nav: (String) -> Unit ) { val accountState by accountViewModel.accountLiveData.observeAsState() val account = remember(accountState) { accountState?.account } ?: return - val pollViewModel: PollNoteViewModel = viewModel() + val pollViewModel: PollNoteViewModel = viewModel( + key = baseNote.idHex + ) LaunchedEffect(key1 = baseNote) { pollViewModel.load(account, baseNote) @@ -64,7 +65,7 @@ fun PollNote( canPreview = canPreview, backgroundColor = backgroundColor, accountViewModel = accountViewModel, - navController = navController + nav = nav ) } @@ -75,7 +76,7 @@ fun PollNote( canPreview: Boolean, backgroundColor: Color, accountViewModel: AccountViewModel, - navController: NavController + nav: (String) -> Unit ) { val zapsState by baseNote.live().zaps.observeAsState() @@ -93,7 +94,7 @@ fun PollNote( accountViewModel, canPreview, backgroundColor, - navController + nav ) } } @@ -106,7 +107,7 @@ private fun OptionNote( accountViewModel: AccountViewModel, canPreview: Boolean, backgroundColor: Color, - navController: NavController + nav: (String) -> Unit ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -168,7 +169,7 @@ private fun OptionNote( pollViewModel.pollEvent?.tags(), backgroundColor, accountViewModel, - navController + nav ) } } @@ -202,7 +203,7 @@ private fun OptionNote( pollViewModel.pollEvent?.tags(), backgroundColor, accountViewModel, - navController + nav ) } } 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 36891d3b5..8d671a9f2 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 @@ -28,6 +28,7 @@ import androidx.compose.material.icons.outlined.Bolt import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf @@ -50,7 +51,6 @@ import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Popup -import androidx.navigation.NavController import coil.compose.AsyncImage import coil.request.CachePolicy import coil.request.ImageRequest @@ -63,10 +63,11 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.math.BigDecimal import java.math.RoundingMode +import kotlin.math.abs import kotlin.math.roundToInt @Composable -fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel, navController: NavController) { +fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) { val accountState by accountViewModel.accountLiveData.observeAsState() val account = remember(accountState) { accountState?.account } ?: return @@ -81,11 +82,11 @@ fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel, navControll } if (wantsToReplyTo != null) { - NewPostView({ wantsToReplyTo = null }, wantsToReplyTo, null, account, accountViewModel, navController) + NewPostView({ wantsToReplyTo = null }, wantsToReplyTo, null, account, accountViewModel, nav) } if (wantsToQuote != null) { - NewPostView({ wantsToQuote = null }, null, wantsToQuote, account, accountViewModel, navController) + NewPostView({ wantsToQuote = null }, null, wantsToQuote, account, accountViewModel, nav) } Spacer(modifier = Modifier.height(8.dp)) @@ -125,6 +126,14 @@ fun ReplyReaction( val repliesState by baseNote.live().replies.observeAsState() val replies = remember(repliesState) { repliesState?.note?.replies } ?: emptySet() + val isWriteable = remember { accountViewModel.isWriteable() } + + val replyCount by remember(repliesState) { + derivedStateOf { + showCount(replies.size) + } + } + val context = LocalContext.current val scope = rememberCoroutineScope() @@ -139,7 +148,7 @@ fun ReplyReaction( IconButton( modifier = iconButtonModifier, onClick = { - if (accountViewModel.isWriteable()) { + if (isWriteable) { onPress() } else { scope.launch { @@ -162,7 +171,7 @@ fun ReplyReaction( if (showCounter) { Text( - " ${showCount(replies.size)}", + " $replyCount", fontSize = 14.sp, color = grayTint ) @@ -180,11 +189,25 @@ fun BoostReaction( val boostsState by baseNote.live().boosts.observeAsState() val boostedNote = remember(boostsState) { boostsState?.note } ?: return - val hasBoosted = remember(boostsState) { accountViewModel.hasBoosted(baseNote) } - val wasBoostedByLoggedIn = remember(boostsState) { boostedNote.isBoostedBy(accountViewModel.userProfile()) } + val hasBoosted by remember(boostsState) { + derivedStateOf { + accountViewModel.hasBoosted(baseNote) + } + } + + val wasBoostedByLoggedIn by remember(boostsState) { + derivedStateOf { + boostedNote.isBoostedBy(accountViewModel.userProfile()) + } + } + val isWriteable = remember { accountViewModel.isWriteable() } - val boostCount = remember(boostsState) { showCount(boostedNote.boosts.size) } + val boostCount by remember(boostsState) { + derivedStateOf { + showCount(boostedNote.boosts.size) + } + } val context = LocalContext.current val scope = rememberCoroutineScope() @@ -259,11 +282,25 @@ fun LikeReaction( val reactionsState by baseNote.live().reactions.observeAsState() val reactedNote = remember(reactionsState) { reactionsState?.note } ?: return - val hasReacted = remember(reactionsState) { accountViewModel.hasReactedTo(baseNote) } - val wasReactedByLoggedIn = remember(reactionsState) { reactedNote.isReactedBy(accountViewModel.userProfile()) } + val hasReacted by remember(reactionsState) { + derivedStateOf { + accountViewModel.hasReactedTo(baseNote) + } + } + + val wasReactedByLoggedIn by remember(reactionsState) { + derivedStateOf { + reactedNote.isReactedBy(accountViewModel.userProfile()) + } + } + val isWriteable = remember { accountViewModel.isWriteable() } - val reactionCount = remember(reactionsState) { showCount(reactedNote.reactions.size) } + val reactionCount by remember(reactionsState) { + derivedStateOf { + showCount(reactedNote.reactions.size) + } + } val context = LocalContext.current val scope = rememberCoroutineScope() @@ -331,15 +368,15 @@ fun ZapReaction( animationSize: Dp = 14.dp ) { val accountState by accountViewModel.accountLiveData.observeAsState() - val account = accountState?.account ?: return + val account = remember(accountState) { accountState?.account } ?: return val zapsState by baseNote.live().zaps.observeAsState() - val zappedNote = zapsState?.note ?: return - val zapMessage = "" + val zappedNote = remember(zapsState) { zapsState?.note } ?: return var wantsToZap by remember { mutableStateOf(false) } var wantsToChangeZapAmount by remember { mutableStateOf(false) } var wantsToSetCustomZap by remember { mutableStateOf(false) } + val context = LocalContext.current val scope = rememberCoroutineScope() @@ -351,10 +388,23 @@ fun ZapReaction( LaunchedEffect(key1 = zapsState) { scope.launch(Dispatchers.IO) { if (!wasZappedByLoggedInUser) { - wasZappedByLoggedInUser = accountViewModel.calculateIfNoteWasZappedByAccount(zappedNote) + val newWasZapped = accountViewModel.calculateIfNoteWasZappedByAccount(zappedNote) + + if (wasZappedByLoggedInUser != newWasZapped) { + wasZappedByLoggedInUser = newWasZapped + } } - zapAmountTxt = showAmount(account.calculateZappedAmount(zappedNote)) + val newZapAmount = showAmount(account.calculateZappedAmount(zappedNote)) + if (newZapAmount != zapAmountTxt) { + zapAmountTxt = newZapAmount + } + + if (wasZappedByLoggedInUser) { + if (abs(zappingProgress - 1) < 0.001) { + zappingProgress = 1f + } + } } } @@ -393,7 +443,7 @@ fun ZapReaction( baseNote, account.zapAmountChoices.first() * 1000, null, - zapMessage, + "", context, onError = { scope.launch { @@ -457,7 +507,6 @@ fun ZapReaction( } if (wasZappedByLoggedInUser) { - zappingProgress = 1f Icon( imageVector = Icons.Default.Bolt, contentDescription = stringResource(R.string.zaps), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt index 2d7ab31a4..19512a828 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt @@ -14,7 +14,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.sp -import androidx.navigation.NavController import com.google.accompanist.flowlayout.FlowRow import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.* @@ -23,7 +22,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @Composable -fun ReplyInformation(replyTo: List?, mentions: List, account: Account, navController: NavController) { +fun ReplyInformation(replyTo: List?, mentions: List, account: Account, nav: (String) -> Unit) { var dupMentions by remember { mutableStateOf?>(null) } LaunchedEffect(Unit) { @@ -34,7 +33,7 @@ fun ReplyInformation(replyTo: List?, mentions: List, account: Acco if (dupMentions != null) { ReplyInformation(replyTo, dupMentions, account) { - navController.navigate("User/${it.pubkeyHex}") + nav("User/${it.pubkeyHex}") } } } @@ -116,7 +115,7 @@ fun ReplyInformation(replyTo: List?, dupMentions: List?, account: Ac } @Composable -fun ReplyInformationChannel(replyTo: List?, mentions: List, channel: Channel, account: Account, navController: NavController) { +fun ReplyInformationChannel(replyTo: List?, mentions: List, channel: Channel, account: Account, nav: (String) -> Unit) { var sortedMentions by remember { mutableStateOf?>(null) } LaunchedEffect(Unit) { @@ -134,26 +133,26 @@ fun ReplyInformationChannel(replyTo: List?, mentions: List, channe sortedMentions, channel, onUserTagClick = { - navController.navigate("User/${it.pubkeyHex}") + nav("User/${it.pubkeyHex}") }, onChannelTagClick = { - navController.navigate("Channel/${it.idHex}") + nav("Channel/${it.idHex}") } ) } } @Composable -fun ReplyInformationChannel(replyTo: List?, mentions: List?, channel: Channel, navController: NavController) { +fun ReplyInformationChannel(replyTo: List?, mentions: List?, channel: Channel, nav: (String) -> Unit) { ReplyInformationChannel( replyTo, mentions, channel, onUserTagClick = { - navController.navigate("User/${it.pubkeyHex}") + nav("User/${it.pubkeyHex}") }, onChannelTagClick = { - navController.navigate("Channel/${it.idHex}") + nav("Channel/${it.idHex}") } ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt index 1164d9067..5c366380f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt @@ -15,7 +15,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.FollowButton @@ -34,7 +33,7 @@ fun UserCompose( top = 10.dp ), accountViewModel: AccountViewModel, - navController: NavController + nav: (String) -> Unit ) { val accountState by accountViewModel.accountLiveData.observeAsState() val account = accountState?.account ?: return @@ -47,14 +46,14 @@ fun UserCompose( Column( modifier = Modifier.clickable( - onClick = { navController.navigate("User/${baseUser.pubkeyHex}") } + onClick = { nav("User/${baseUser.pubkeyHex}") } ) ) { Row( modifier = overallModifier, verticalAlignment = Alignment.CenterVertically ) { - UserPicture(baseUser, navController, account.userProfile(), 55.dp) + UserPicture(baseUser, nav, account.userProfile(), 55.dp) Column(modifier = Modifier.padding(start = 10.dp).weight(1f)) { Row(verticalAlignment = Alignment.CenterVertically) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserReactionsRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserReactionsRow.kt index 484fbdde2..26cebaa48 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserReactionsRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserReactionsRow.kt @@ -13,8 +13,10 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Bolt import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier @@ -26,7 +28,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.navigation.NavController import com.patrykandpatrick.vico.core.chart.composed.ComposedChartEntryModel import com.patrykandpatrick.vico.core.entry.ChartEntryModel import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer @@ -41,8 +42,9 @@ import com.vitorpamplona.amethyst.service.model.LnZapEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent -import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.showAmountAxis import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange +import com.vitorpamplona.amethyst.ui.theme.RoyalBlue import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch @@ -53,7 +55,10 @@ import java.time.ZoneId import java.time.format.DateTimeFormatter @Composable -fun UserReactionsRow(model: UserReactionsViewModel, accountViewModel: AccountViewModel, navController: NavController, onClick: () -> Unit) { +fun UserReactionsRow( + model: UserReactionsViewModel, + onClick: () -> Unit +) { Row( verticalAlignment = CenterVertically, modifier = Modifier @@ -75,23 +80,24 @@ fun UserReactionsRow(model: UserReactionsViewModel, accountViewModel: AccountVie } Row(verticalAlignment = CenterVertically, modifier = Modifier.weight(1f)) { - UserReplyReaction(model.replies[model.today]) + UserReplyReaction(model.replies[model.today()]) } Row(verticalAlignment = CenterVertically, modifier = Modifier.weight(1f)) { - UserBoostReaction(model.boosts[model.today]) + UserBoostReaction(model.boosts[model.today()]) } Row(verticalAlignment = CenterVertically, modifier = Modifier.weight(1f)) { - UserLikeReaction(model.reactions[model.today]) + UserLikeReaction(model.reactions[model.today()]) } Row(verticalAlignment = CenterVertically, modifier = Modifier.weight(1f)) { - UserZapReaction(model.zaps[model.today]) + UserZapReaction(model.zaps[model.today()]) } } } +@Stable class UserReactionsViewModel : ViewModel() { var user: User? = null @@ -100,14 +106,12 @@ class UserReactionsViewModel : ViewModel() { var zaps by mutableStateOf>(emptyMap()) var replies by mutableStateOf>(emptyMap()) - var takenIntoAccount = setOf() - - val sdf = DateTimeFormatter.ofPattern("yyyy-MM-dd") // SimpleDateFormat() - val today = sdf.format(LocalDateTime.now()) - var chartModel by mutableStateOf?>(null) var axisLabels by mutableStateOf>(emptyList()) + private var takenIntoAccount = setOf() + private val sdf = DateTimeFormatter.ofPattern("yyyy-MM-dd") // SimpleDateFormat() + fun load(baseUser: User) { user = baseUser reactions = emptyMap() @@ -125,6 +129,8 @@ class UserReactionsViewModel : ViewModel() { ) } + fun today() = sdf.format(LocalDateTime.now()) + fun initializeSuspend() { val currentUser = user?.pubkeyHex ?: return @@ -274,17 +280,19 @@ class UserReactionsViewModel : ViewModel() { fun UserReplyReaction( replyCount: Int? ) { + val showCounts = remember(replyCount) { showCount(replyCount) } + Icon( painter = painterResource(R.drawable.ic_comment), null, modifier = Modifier.size(20.dp), - tint = Color.Cyan + tint = RoyalBlue ) Spacer(modifier = Modifier.width(10.dp)) Text( - showCount(replyCount), + showCounts, fontWeight = FontWeight.Bold, fontSize = 18.sp ) @@ -294,6 +302,8 @@ fun UserReplyReaction( fun UserBoostReaction( boostCount: Int? ) { + val showCounts = remember(boostCount) { showCount(boostCount) } + Icon( painter = painterResource(R.drawable.ic_retweeted), null, @@ -304,7 +314,7 @@ fun UserBoostReaction( Spacer(modifier = Modifier.width(10.dp)) Text( - showCount(boostCount), + showCounts, fontWeight = FontWeight.Bold, fontSize = 18.sp ) @@ -314,6 +324,8 @@ fun UserBoostReaction( fun UserLikeReaction( likeCount: Int? ) { + val showCounts = remember(likeCount) { showCount(likeCount) } + Icon( painter = painterResource(R.drawable.ic_liked), null, @@ -324,7 +336,7 @@ fun UserLikeReaction( Spacer(modifier = Modifier.width(10.dp)) Text( - showCount(likeCount), + showCounts, fontWeight = FontWeight.Bold, fontSize = 18.sp ) @@ -334,6 +346,8 @@ fun UserLikeReaction( fun UserZapReaction( amount: BigDecimal? ) { + val showAmounts = remember(amount) { showAmountAxis(amount) } + Icon( imageVector = Icons.Default.Bolt, contentDescription = stringResource(R.string.zaps), @@ -344,7 +358,7 @@ fun UserZapReaction( Spacer(modifier = Modifier.width(8.dp)) Text( - showAmount(amount), + showAmounts, fontWeight = FontWeight.Bold, fontSize = 18.sp ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt index aa881b1f0..de185a436 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt @@ -23,7 +23,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.navigation.NavController import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.LnZapEvent @@ -37,7 +36,7 @@ import kotlinx.coroutines.launch import java.math.BigDecimal @Composable -fun ZapNoteCompose(baseNote: Pair, accountViewModel: AccountViewModel, navController: NavController) { +fun ZapNoteCompose(baseNote: Pair, accountViewModel: AccountViewModel, nav: (String) -> Unit) { val accountState by accountViewModel.accountLiveData.observeAsState() val account = accountState?.account ?: return @@ -60,7 +59,7 @@ fun ZapNoteCompose(baseNote: Pair, accountViewModel: AccountViewMode Column( modifier = Modifier.clickable( - onClick = { navController.navigate("User/${baseAuthor.pubkeyHex}") } + onClick = { nav("User/${baseAuthor.pubkeyHex}") } ), verticalArrangement = Arrangement.Center ) { @@ -73,7 +72,7 @@ fun ZapNoteCompose(baseNote: Pair, accountViewModel: AccountViewMode ), verticalAlignment = Alignment.CenterVertically ) { - UserPicture(baseAuthor, navController, account.userProfile(), 55.dp) + UserPicture(baseAuthor, nav, account.userProfile(), 55.dp) Column(modifier = Modifier.padding(start = 10.dp).weight(1f)) { Row(verticalAlignment = Alignment.CenterVertically) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapSetCompose.kt index b99f3007c..a67a18f5a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapSetCompose.kt @@ -26,7 +26,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import com.google.accompanist.flowlayout.FlowRow import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.R @@ -39,7 +38,7 @@ import kotlinx.coroutines.withContext @OptIn(ExperimentalFoundationApi::class) @Composable -fun ZapSetCompose(zapSetCard: ZapSetCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, navController: NavController) { +fun ZapSetCompose(zapSetCard: ZapSetCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) { val noteState by zapSetCard.note.live().metadata.observeAsState() val note = noteState?.note @@ -78,7 +77,7 @@ fun ZapSetCompose(zapSetCard: ZapSetCard, isInnerNote: Boolean = false, routeFor routeFor( note, account.userProfile() - )?.let { navController.navigate(it) } + )?.let { nav(it) } } }, onLongClick = { popupExpanded = true } @@ -115,7 +114,7 @@ fun ZapSetCompose(zapSetCard: ZapSetCard, isInnerNote: Boolean = false, routeFor zapSetCard.zapEvents.forEach { NoteAuthorPicture( baseNote = it.key, - navController = navController, + nav = nav, userAccount = account.userProfile(), size = 35.dp ) @@ -129,7 +128,7 @@ fun ZapSetCompose(zapSetCard: ZapSetCard, isInnerNote: Boolean = false, routeFor isBoostedNote = true, parentBackgroundColor = backgroundColor, accountViewModel = accountViewModel, - navController = navController + nav = nav ) NoteDropDownMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapUserSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapUserSetCompose.kt index 70aa994f7..5958fdb91 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapUserSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapUserSetCompose.kt @@ -25,7 +25,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import com.google.accompanist.flowlayout.FlowRow import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.R @@ -37,7 +36,7 @@ import kotlinx.coroutines.withContext @OptIn(ExperimentalFoundationApi::class) @Composable -fun ZapUserSetCompose(zapSetCard: ZapUserSetCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, navController: NavController) { +fun ZapUserSetCompose(zapSetCard: ZapUserSetCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) { val accountState by accountViewModel.accountLiveData.observeAsState() val account = accountState?.account ?: return @@ -61,7 +60,7 @@ fun ZapUserSetCompose(zapSetCard: ZapUserSetCard, isInnerNote: Boolean = false, modifier = Modifier .background(backgroundColor) .clickable { - navController.navigate("User/${zapSetCard.user.pubkeyHex}") + nav("User/${zapSetCard.user.pubkeyHex}") } ) { Row( @@ -95,14 +94,14 @@ fun ZapUserSetCompose(zapSetCard: ZapUserSetCard, isInnerNote: Boolean = false, zapSetCard.zapEvents.forEach { NoteAuthorPicture( baseNote = it.key, - navController = navController, + nav = nav, userAccount = account.userProfile(), size = 35.dp ) } } - UserCompose(baseUser = zapSetCard.user, accountViewModel = accountViewModel, navController = navController) + UserCompose(baseUser = zapSetCard.user, accountViewModel = accountViewModel, nav = nav) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt index f96914f78..f3c923a68 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.MainScreen import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginPage @@ -21,10 +22,20 @@ fun AccountScreen(accountStateViewModel: AccountStateViewModel, startingPage: St LoginPage(accountStateViewModel, isFirstLogin = true) } is AccountState.LoggedIn -> { - MainScreen(AccountViewModel(state.account), accountStateViewModel, startingPage) + val accountViewModel: AccountViewModel = viewModel( + key = state.account.userProfile().pubkeyHex, + factory = AccountViewModel.Factory(state.account) + ) + + MainScreen(accountViewModel, accountStateViewModel, startingPage) } is AccountState.LoggedInViewOnly -> { - MainScreen(AccountViewModel(state.account), accountStateViewModel, startingPage) + val accountViewModel: AccountViewModel = viewModel( + key = state.account.userProfile().pubkeyHex, + factory = AccountViewModel.Factory(state.account) + ) + + MainScreen(accountViewModel, accountStateViewModel, startingPage) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedState.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedState.kt index 3f374730c..2cd9757a4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedState.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedState.kt @@ -1,14 +1,20 @@ package com.vitorpamplona.amethyst.ui.screen +import androidx.compose.runtime.Immutable import androidx.compose.runtime.MutableState +import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableMap +@Immutable abstract class Card() { abstract fun createdAt(): Long abstract fun id(): String } +@Immutable class BadgeCard(val note: Note) : Card() { override fun createdAt(): Long { return note.createdAt() ?: 0 @@ -17,6 +23,7 @@ class BadgeCard(val note: Note) : Card() { override fun id() = note.idHex } +@Immutable class NoteCard(val note: Note) : Card() { override fun createdAt(): Long { return note.createdAt() ?: 0 @@ -25,6 +32,7 @@ class NoteCard(val note: Note) : Card() { override fun id() = note.idHex } +@Immutable class LikeSetCard(val note: Note, val likeEvents: List) : Card() { val createdAt = likeEvents.maxOf { it.createdAt() ?: 0 } override fun createdAt(): Long { @@ -33,6 +41,7 @@ class LikeSetCard(val note: Note, val likeEvents: List) : Card() { override fun id() = note.idHex + "L" + createdAt } +@Immutable class ZapSetCard(val note: Note, val zapEvents: Map) : Card() { val createdAt = zapEvents.maxOf { it.value.createdAt() ?: 0 } override fun createdAt(): Long { @@ -41,7 +50,8 @@ class ZapSetCard(val note: Note, val zapEvents: Map) : Card() { override fun id() = note.idHex + "Z" + createdAt } -class ZapUserSetCard(val user: User, val zapEvents: Map) : Card() { +@Immutable +class ZapUserSetCard(val user: User, val zapEvents: ImmutableMap) : Card() { val createdAt = zapEvents.maxOf { it.value.createdAt() ?: 0 } override fun createdAt(): Long { return createdAt @@ -49,7 +59,8 @@ class ZapUserSetCard(val user: User, val zapEvents: Map) : Card() { override fun id() = user.pubkeyHex + "U" + createdAt } -class MultiSetCard(val note: Note, val boostEvents: List, val likeEvents: List, val zapEvents: Map) : Card() { +@Immutable +class MultiSetCard(val note: Note, val boostEvents: ImmutableList, val likeEvents: ImmutableList, val zapEvents: ImmutableMap) : Card() { val createdAt = maxOf( zapEvents.maxOfOrNull { it.value.createdAt() ?: 0 } ?: 0, likeEvents.maxOfOrNull { it.createdAt() ?: 0 } ?: 0, @@ -62,6 +73,7 @@ class MultiSetCard(val note: Note, val boostEvents: List, val likeEvents: override fun id() = note.idHex + "X" + createdAt } +@Immutable class BoostSetCard(val note: Note, val boostEvents: List) : Card() { val createdAt = boostEvents.maxOf { it.createdAt() ?: 0 } @@ -72,6 +84,7 @@ class BoostSetCard(val note: Note, val boostEvents: List) : Card() { override fun id() = note.idHex + "B" + createdAt } +@Immutable class MessageSetCard(val note: Note) : Card() { override fun createdAt(): Long { return note.createdAt() ?: 0 @@ -80,9 +93,17 @@ class MessageSetCard(val note: Note) : Card() { override fun id() = note.idHex } +@Immutable sealed class CardFeedState { + @Immutable object Loading : CardFeedState() - class Loaded(val feed: MutableState>) : CardFeedState() + + @Stable + class Loaded(val feed: MutableState>) : CardFeedState() + + @Immutable object Empty : CardFeedState() + + @Immutable class FeedError(val errorMessage: String) : CardFeedState() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt index b7d1344bb..7bd6b749f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt @@ -1,12 +1,16 @@ package com.vitorpamplona.amethyst.ui.screen -import android.util.Log import androidx.compose.animation.Crossfade import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.ExperimentalMaterialApi @@ -23,7 +27,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import com.vitorpamplona.amethyst.ui.note.BadgeCompose import com.vitorpamplona.amethyst.ui.note.BoostSetCompose import com.vitorpamplona.amethyst.ui.note.LikeSetCompose @@ -33,15 +36,13 @@ import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.ZapSetCompose import com.vitorpamplona.amethyst.ui.note.ZapUserSetCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import kotlin.time.ExperimentalTime -import kotlin.time.measureTimedValue @OptIn(ExperimentalMaterialApi::class) @Composable fun CardFeedView( viewModel: CardFeedViewModel, accountViewModel: AccountViewModel, - navController: NavController, + nav: (String) -> Unit, routeForLastRead: String, scrollStateKey: String? = null, scrollToTop: Boolean = false @@ -52,9 +53,14 @@ fun CardFeedView( val refresh = { refreshing = true; viewModel.invalidateData(); refreshing = false } val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh) - Box(Modifier.pullRefresh(pullRefreshState)) { - Column() { - Crossfade(targetState = feedState, animationSpec = tween(durationMillis = 100)) { state -> + Box(Modifier.fillMaxSize().pullRefresh(pullRefreshState)) { + Column(modifier = Modifier.fillMaxSize()) { + Crossfade( + modifier = Modifier.fillMaxSize(), + targetState = feedState, + animationSpec = tween(durationMillis = 100) + ) { state -> + when (state) { is CardFeedState.Empty -> { FeedEmpty { @@ -67,11 +73,14 @@ fun CardFeedView( } } is CardFeedState.Loaded -> { - refreshing = false + if (refreshing) { + refreshing = false + } + FeedLoaded( state = state, accountViewModel = accountViewModel, - navController = navController, + nav = nav, routeForLastRead = routeForLastRead, scrollStateKey = scrollStateKey, scrollToTop = scrollToTop @@ -88,12 +97,11 @@ fun CardFeedView( } } -@OptIn(ExperimentalTime::class) @Composable private fun FeedLoaded( state: CardFeedState.Loaded, accountViewModel: AccountViewModel, - navController: NavController, + nav: (String) -> Unit, routeForLastRead: String, scrollStateKey: String?, scrollToTop: Boolean = false @@ -106,76 +114,98 @@ private fun FeedLoaded( if (scrollToTop) { LaunchedEffect(Unit) { - listState.scrollToItem(index = 0) + if (listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0) { + listState.scrollToItem(index = 0) + } } } + LazyNotificationList(listState, state, routeForLastRead, accountViewModel, nav) +} + +@Composable +private fun LazyNotificationList( + listState: LazyListState, + state: CardFeedState.Loaded, + routeForLastRead: String, + accountViewModel: AccountViewModel, + nav: (String) -> Unit +) { LazyColumn( - contentPadding = PaddingValues( - top = 10.dp, - bottom = 10.dp - ), + modifier = remember { Modifier.fillMaxSize() }, + contentPadding = remember { + PaddingValues( + top = 10.dp, + bottom = 10.dp + ) + }, state = listState ) { itemsIndexed(state.feed.value, key = { _, item -> item.id() }) { _, item -> - val (value, elapsed) = measureTimedValue { + Row(Modifier.fillMaxWidth().defaultMinSize(minHeight = 100.dp)) { when (item) { is NoteCard -> NoteCompose( item.note, isBoostedNote = false, accountViewModel = accountViewModel, - navController = navController, + nav = nav, routeForLastRead = routeForLastRead ) + is ZapSetCard -> ZapSetCompose( item, isInnerNote = false, accountViewModel = accountViewModel, - navController = navController, + nav = nav, routeForLastRead = routeForLastRead ) + is ZapUserSetCard -> ZapUserSetCompose( item, isInnerNote = false, accountViewModel = accountViewModel, - navController = navController, + nav = nav, routeForLastRead = routeForLastRead ) + is LikeSetCard -> LikeSetCompose( item, isInnerNote = false, accountViewModel = accountViewModel, - navController = navController, + nav = nav, routeForLastRead = routeForLastRead ) + is BoostSetCard -> BoostSetCompose( item, isInnerNote = false, accountViewModel = accountViewModel, - navController = navController, + nav = nav, routeForLastRead = routeForLastRead ) + is MultiSetCard -> MultiSetCompose( item, accountViewModel = accountViewModel, - navController = navController, + nav = nav, routeForLastRead = routeForLastRead ) + is BadgeCard -> BadgeCompose( item, accountViewModel = accountViewModel, - navController = navController, + nav = nav, routeForLastRead = routeForLastRead ) + is MessageSetCard -> MessageSetCompose( messageSetCard = item, routeForLastRead = routeForLastRead, accountViewModel = accountViewModel, - navController = navController + nav = nav ) } } - Log.d("Time", "${item.javaClass.simpleName} Feed in $elapsed ${item.id()}") } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt index 075419deb..bae39a5fe 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt @@ -1,6 +1,7 @@ package com.vitorpamplona.amethyst.ui.screen import android.util.Log +import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -20,6 +21,9 @@ import com.vitorpamplona.amethyst.ui.components.BundledUpdate import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.amethyst.ui.dal.NotificationFeedFilter +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toImmutableMap import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -30,6 +34,7 @@ import kotlinx.coroutines.launch import kotlin.time.ExperimentalTime import kotlin.time.measureTimedValue +@Stable class NotificationViewModel : CardFeedViewModel(NotificationFeedFilter) open class CardFeedViewModel(val localFilter: FeedFilter) : ViewModel() { @@ -37,7 +42,7 @@ open class CardFeedViewModel(val localFilter: FeedFilter) : ViewModel() { val feedContent = _feedContent.asStateFlow() private var lastAccount: Account? = null - private var lastNotes: List? = null + private var lastNotes: Set? = null fun refresh() { val scope = CoroutineScope(Job() + Dispatchers.Default) @@ -48,7 +53,7 @@ open class CardFeedViewModel(val localFilter: FeedFilter) : ViewModel() { @Synchronized private fun refreshSuspended() { - val notes = localFilter.loadTop() + val notes = localFilter.feed() val thisAccount = (localFilter as? NotificationFeedFilter)?.account val lastNotesCopy = if (thisAccount == lastAccount) lastNotes else null @@ -57,14 +62,30 @@ open class CardFeedViewModel(val localFilter: FeedFilter) : ViewModel() { if (lastNotesCopy != null && oldNotesState is CardFeedState.Loaded) { val newCards = convertToCard(notes.minus(lastNotesCopy)) if (newCards.isNotEmpty()) { - lastNotes = notes + lastNotes = notes.toSet() lastAccount = (localFilter as? NotificationFeedFilter)?.account - updateFeed((oldNotesState.feed.value + newCards).distinctBy { it.id() }.sortedWith(compareBy({ it.createdAt() }, { it.id() })).reversed()) + + val updatedCards = (oldNotesState.feed.value + newCards) + .distinctBy { it.id() } + .sortedWith(compareBy({ it.createdAt() }, { it.id() })) + .reversed() + .take(1000) + .toImmutableList() + + if (!equalImmutableLists(oldNotesState.feed.value, updatedCards)) { + updateFeed(updatedCards) + } } } else { - val cards = convertToCard(notes) - lastNotes = notes + lastNotes = notes.toSet() lastAccount = (localFilter as? NotificationFeedFilter)?.account + + val cards = convertToCard(notes) + .sortedWith(compareBy({ it.createdAt() }, { it.id() })) + .reversed() + .take(1000) + .toImmutableList() + updateFeed(cards) } } @@ -133,9 +154,9 @@ open class CardFeedViewModel(val localFilter: FeedFilter) : ViewModel() { singleList.chunked(50).map { chunk -> MultiSetCard( baseNote, - boostsInCard.filter { it in chunk }, - reactionsInCard.filter { it in chunk }, - zapsInCard.filter { it.value in chunk } + boostsInCard.filter { it in chunk }.toImmutableList(), + reactionsInCard.filter { it in chunk }.toImmutableList(), + zapsInCard.filter { it.value in chunk }.toImmutableMap() ) } }.flatten() @@ -143,7 +164,7 @@ open class CardFeedViewModel(val localFilter: FeedFilter) : ViewModel() { val userZaps = zapsPerUser.map { ZapUserSetCard( it.key, - it.value + it.value.toImmutableMap() ) } @@ -160,7 +181,7 @@ open class CardFeedViewModel(val localFilter: FeedFilter) : ViewModel() { return (multiCards + textNoteCards + userZaps).sortedWith(compareBy({ it.createdAt() }, { it.id() })).reversed() } - private fun updateFeed(notes: List) { + private fun updateFeed(notes: ImmutableList) { val scope = CoroutineScope(Job() + Dispatchers.Main) scope.launch { val currentState = _feedContent.value @@ -168,7 +189,6 @@ open class CardFeedViewModel(val localFilter: FeedFilter) : ViewModel() { if (notes.isEmpty()) { _feedContent.update { CardFeedState.Empty } } else if (currentState is CardFeedState.Loaded) { - // updates the current list currentState.feed.value = notes } else { _feedContent.update { CardFeedState.Loaded(mutableStateOf(notes)) } @@ -184,13 +204,29 @@ open class CardFeedViewModel(val localFilter: FeedFilter) : ViewModel() { if (lastNotesCopy != null && localFilter is AdditiveFeedFilter && oldNotesState is CardFeedState.Loaded) { val filteredNewList = localFilter.applyFilter(newItems) + + if (filteredNewList.isEmpty()) return + val actuallyNew = filteredNewList.minus(lastNotesCopy) + if (actuallyNew.isEmpty()) return + val newCards = convertToCard(actuallyNew) + if (newCards.isNotEmpty()) { - lastNotes = lastNotesCopy + newItems + lastNotes = lastNotesCopy + actuallyNew lastAccount = (localFilter as? NotificationFeedFilter)?.account - updateFeed((oldNotesState.feed.value + newCards).distinctBy { it.id() }.sortedWith(compareBy({ it.createdAt() }, { it.id() })).reversed()) + + val updatedCards = (oldNotesState.feed.value + newCards) + .distinctBy { it.id() } + .sortedWith(compareBy({ it.createdAt() }, { it.id() })) + .reversed() + .take(1000) + .toImmutableList() + + if (!equalImmutableLists(oldNotesState.feed.value, updatedCards)) { + updateFeed(updatedCards) + } } } else { // Refresh Everything @@ -199,7 +235,7 @@ open class CardFeedViewModel(val localFilter: FeedFilter) : ViewModel() { } @OptIn(ExperimentalTime::class) - private val bundler = BundledUpdate(250, Dispatchers.IO) { + private val bundler = BundledUpdate(1000, Dispatchers.IO) { // adds the time to perform the refresh into this delay // holding off new updates in case of heavy refresh routines. val (value, elapsed) = measureTimedValue { @@ -207,19 +243,22 @@ open class CardFeedViewModel(val localFilter: FeedFilter) : ViewModel() { } Log.d("Time", "${this.javaClass.simpleName} Card update $elapsed") } - private val bundlerInsert = BundledInsert>(250, Dispatchers.IO) + private val bundlerInsert = BundledInsert>(1000, Dispatchers.IO) - fun invalidateData() { - bundler.invalidate() + fun invalidateData(ignoreIfDoing: Boolean = false) { + bundler.invalidate(ignoreIfDoing) } @OptIn(ExperimentalTime::class) fun invalidateInsertData(newItems: Set) { bundlerInsert.invalidateList(newItems) { + val newObjects = it.flatten().toSet() val (value, elapsed) = measureTimedValue { - refreshFromOldState(it.flatten().toSet()) + if (newObjects.isNotEmpty()) { + refreshFromOldState(newObjects) + } } - Log.d("Time", "${this.javaClass.simpleName} Card additive update $elapsed") + Log.d("Time", "${this.javaClass.simpleName} Card additive update $elapsed. ${newObjects.size}") } } @@ -249,3 +288,13 @@ open class CardFeedViewModel(val localFilter: FeedFilter) : ViewModel() { super.onCleared() } } + +fun equalImmutableLists(list1: ImmutableList, list2: ImmutableList): Boolean { + if (list1.size != list2.size) return false + for (i in 0 until list1.size) { + if (list1[i] !== list2[i]) { + return false + } + } + return true +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt index 1f67a61f3..ff929707e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt @@ -14,13 +14,12 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable -fun ChatroomFeedView(viewModel: FeedViewModel, accountViewModel: AccountViewModel, navController: NavController, routeForLastRead: String, onWantsToReply: (Note) -> Unit) { +fun ChatroomFeedView(viewModel: FeedViewModel, accountViewModel: AccountViewModel, nav: (String) -> Unit, routeForLastRead: String, onWantsToReply: (Note) -> Unit) { val feedState by viewModel.feedContent.collectAsState() var isRefreshing by remember { mutableStateOf(false) } @@ -63,7 +62,7 @@ fun ChatroomFeedView(viewModel: FeedViewModel, accountViewModel: AccountViewMode state = listState ) { itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item -> - ChatroomMessageCompose(item, routeForLastRead, accountViewModel = accountViewModel, navController = navController, onWantsToReply = onWantsToReply) + ChatroomMessageCompose(item, routeForLastRead, accountViewModel = accountViewModel, nav = nav, onWantsToReply = onWantsToReply) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt index 8238ad33e..736976132 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt @@ -24,7 +24,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.model.PrivateDmEvent @@ -36,7 +35,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel fun ChatroomListFeedView( viewModel: FeedViewModel, accountViewModel: AccountViewModel, - navController: NavController, + nav: (String) -> Unit, markAsRead: MutableState ) { val feedState by viewModel.feedContent.collectAsStateWithLifecycle() @@ -65,8 +64,10 @@ fun ChatroomListFeedView( } is FeedState.Loaded -> { - refreshing = false - FeedLoaded(state, accountViewModel, navController, markAsRead) + if (refreshing) { + refreshing = false + } + FeedLoaded(state, accountViewModel, nav, markAsRead) } FeedState.Loading -> { @@ -84,7 +85,7 @@ fun ChatroomListFeedView( private fun FeedLoaded( state: FeedState.Loaded, accountViewModel: AccountViewModel, - navController: NavController, + nav: (String) -> Unit, markAsRead: MutableState ) { val listState = rememberLazyListState() @@ -137,7 +138,7 @@ private fun FeedLoaded( ChatroomCompose( item, accountViewModel = accountViewModel, - navController = navController + nav = nav ) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt index 3db7f3803..08d0fe524 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt @@ -29,7 +29,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -39,7 +38,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel fun FeedView( viewModel: FeedViewModel, accountViewModel: AccountViewModel, - navController: NavController, + nav: (String) -> Unit, routeForLastRead: String?, scrollStateKey: String? = null, scrollToTop: Boolean = false, @@ -77,12 +76,14 @@ fun FeedView( } is FeedState.Loaded -> { - refreshing = false + if (refreshing) { + refreshing = false + } FeedLoaded( state, routeForLastRead, accountViewModel, - navController, + nav, scrollStateKey, scrollToTop ) @@ -106,7 +107,7 @@ private fun FeedLoaded( state: FeedState.Loaded, routeForLastRead: String?, accountViewModel: AccountViewModel, - navController: NavController, + nav: (String) -> Unit, scrollStateKey: String?, scrollToTop: Boolean = false ) { @@ -118,10 +119,16 @@ private fun FeedLoaded( if (scrollToTop) { LaunchedEffect(Unit) { - listState.scrollToItem(index = 0) + if (listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0) { + listState.scrollToItem(index = 0) + } } } + val baseModifier = remember { + Modifier + } + LazyColumn( contentPadding = PaddingValues( top = 10.dp, @@ -132,10 +139,11 @@ private fun FeedLoaded( itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item -> NoteCompose( item, - isBoostedNote = false, routeForLastRead = routeForLastRead, + modifier = baseModifier, + isBoostedNote = false, accountViewModel = accountViewModel, - navController = navController + nav = nav ) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index a3f3da1a5..1686410bf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -115,8 +115,8 @@ abstract class FeedViewModel(val localFilter: FeedFilter) : ViewModel() { } private val bundlerInsert = BundledInsert>(250, Dispatchers.IO) - fun invalidateData() { - bundler.invalidate() + fun invalidateData(ignoreIfDoing: Boolean = false) { + bundler.invalidate(ignoreIfDoing) } fun invalidateInsertData(newItems: Set) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedView.kt index ab8e591a2..e32473ad1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedView.kt @@ -21,7 +21,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import com.vitorpamplona.amethyst.ui.note.ZapNoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -30,7 +29,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel fun LnZapFeedView( viewModel: LnZapFeedViewModel, accountViewModel: AccountViewModel, - navController: NavController, + nav: (String) -> Unit, enablePullRefresh: Boolean = true ) { val feedState by viewModel.feedContent.collectAsState() @@ -60,8 +59,11 @@ fun LnZapFeedView( } } is LnZapFeedState.Loaded -> { - refreshing = false - LnZapFeedLoaded(state, accountViewModel, navController) + if (refreshing) { + refreshing = false + } + + LnZapFeedLoaded(state, accountViewModel, nav) } is LnZapFeedState.Loading -> { LoadingFeed() @@ -80,7 +82,7 @@ fun LnZapFeedView( private fun LnZapFeedLoaded( state: LnZapFeedState.Loaded, accountViewModel: AccountViewModel, - navController: NavController + nav: (String) -> Unit ) { val listState = rememberLazyListState() @@ -92,7 +94,7 @@ private fun LnZapFeedLoaded( state = listState ) { itemsIndexed(state.feed.value, key = { _, item -> item.second.idHex }) { _, item -> - ZapNoteCompose(item, accountViewModel = accountViewModel, navController = navController) + ZapNoteCompose(item, accountViewModel = accountViewModel, nav = nav) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index cc1d47a35..25840d8ac 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -49,7 +49,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.navigation.NavController import coil.compose.AsyncImage import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note @@ -61,6 +60,7 @@ import com.vitorpamplona.amethyst.service.model.PeopleListEvent import com.vitorpamplona.amethyst.service.model.PinListEvent import com.vitorpamplona.amethyst.service.model.PollNoteEvent import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status +import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.note.* import com.vitorpamplona.amethyst.ui.note.BadgeDisplay @@ -82,7 +82,7 @@ import kotlinx.coroutines.delay @OptIn(ExperimentalMaterialApi::class) @Composable -fun ThreadFeedView(noteId: String, viewModel: FeedViewModel, accountViewModel: AccountViewModel, navController: NavController) { +fun ThreadFeedView(noteId: String, viewModel: FeedViewModel, accountViewModel: AccountViewModel, nav: (String) -> Unit) { val feedState by viewModel.feedContent.collectAsState() val listState = rememberLazyListState() @@ -139,7 +139,7 @@ fun ThreadFeedView(noteId: String, viewModel: FeedViewModel, accountViewModel: A if (item.idHex == noteId) MaterialTheme.colors.primary.copy(alpha = 0.52f) else MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ), accountViewModel = accountViewModel, - navController = navController + nav = nav ) } else { Column() { @@ -155,7 +155,7 @@ fun ThreadFeedView(noteId: String, viewModel: FeedViewModel, accountViewModel: A isBoostedNote = false, unPackReply = false, accountViewModel = accountViewModel, - navController = navController + nav = nav ) } } @@ -204,7 +204,7 @@ fun NoteMaster( baseNote: Note, modifier: Modifier = Modifier, accountViewModel: AccountViewModel, - navController: NavController + nav: (String) -> Unit ) { val noteState by baseNote.live().metadata.observeAsState() val note = noteState?.note @@ -233,7 +233,7 @@ fun NoteMaster( account.userProfile(), Modifier, false, - navController, + nav, onClick = { showHiddenNote = true } ) } else { @@ -247,13 +247,13 @@ fun NoteMaster( .padding(start = 12.dp, end = 12.dp) .clickable(onClick = { note.author?.let { - navController.navigate("User/${it.pubkeyHex}") + nav("User/${it.pubkeyHex}") } }) ) { NoteAuthorPicture( baseNote = baseNote, - navController = navController, + nav = nav, userAccount = account.userProfile(), size = 55.dp ) @@ -262,7 +262,7 @@ fun NoteMaster( Row(verticalAlignment = Alignment.CenterVertically) { NoteUsernameDisplay(baseNote, Modifier.weight(1f)) - DisplayFollowingHashtagsInPost(noteEvent, account, navController) + DisplayFollowingHashtagsInPost(noteEvent, account, nav) Text( timeAgo(note.createdAt(), context = context), @@ -290,7 +290,7 @@ fun NoteMaster( val baseReward = noteEvent.getReward() if (baseReward != null) { - DisplayReward(baseReward, baseNote, account, navController) + DisplayReward(baseReward, baseNote, account, nav) } val pow = noteEvent.getPoWRank() @@ -353,11 +353,11 @@ fun NoteMaster( ) { Column() { if (noteEvent is PeopleListEvent) { - DisplayPeopleList(noteState, MaterialTheme.colors.background, accountViewModel, navController) + DisplayPeopleList(noteState, MaterialTheme.colors.background, accountViewModel, nav) } else if (noteEvent is AudioTrackEvent) { - AudioTrackHeader(noteEvent, note, account.userProfile(), navController) + AudioTrackHeader(noteEvent, note, account.userProfile(), nav) } else if (noteEvent is PinListEvent) { - PinListHeader(noteState, MaterialTheme.colors.background, accountViewModel, navController) + PinListHeader(noteState, MaterialTheme.colors.background, accountViewModel, nav) } else if (noteEvent is HighlightEvent) { DisplayHighlight( noteEvent.quote(), @@ -367,7 +367,7 @@ fun NoteMaster( true, backgroundColor, accountViewModel, - navController + nav ) } else { val eventContent = note.event?.content() @@ -377,17 +377,24 @@ fun NoteMaster( !noteForReports.hasAnyReports() if (eventContent != null) { - TranslatableRichTextViewer( - eventContent, - canPreview, - Modifier.fillMaxWidth(), - note.event?.tags(), - MaterialTheme.colors.background, - accountViewModel, - navController - ) + val hasSensitiveContent = remember(note.event) { note.event?.isSensitive() ?: false } - DisplayUncitedHashtags(noteEvent.hashtags(), eventContent, navController) + SensitivityWarning( + hasSensitiveContent = hasSensitiveContent, + accountViewModel = accountViewModel + ) { + TranslatableRichTextViewer( + eventContent, + canPreview, + Modifier.fillMaxWidth(), + note.event?.tags(), + MaterialTheme.colors.background, + accountViewModel, + nav + ) + } + + DisplayUncitedHashtags(noteEvent.hashtags(), eventContent, nav) if (noteEvent is PollNoteEvent) { PollNote( @@ -395,13 +402,13 @@ fun NoteMaster( canPreview, backgroundColor, accountViewModel, - navController + nav ) } } } - ReactionsRow(note, accountViewModel, navController) + ReactionsRow(note, accountViewModel, nav) Divider( modifier = Modifier.padding(top = 10.dp), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt index d2f8078cd..4c3946e6d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt @@ -21,7 +21,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import com.vitorpamplona.amethyst.ui.note.UserCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -30,7 +29,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel fun UserFeedView( viewModel: UserFeedViewModel, accountViewModel: AccountViewModel, - navController: NavController, + nav: (String) -> Unit, enablePullRefresh: Boolean = true ) { val feedState by viewModel.feedContent.collectAsState() @@ -61,7 +60,7 @@ fun UserFeedView( } is UserFeedState.Loaded -> { refreshing = false - FeedLoaded(state, accountViewModel, navController) + FeedLoaded(state, accountViewModel, nav) } is UserFeedState.Loading -> { LoadingFeed() @@ -80,7 +79,7 @@ fun UserFeedView( private fun FeedLoaded( state: UserFeedState.Loaded, accountViewModel: AccountViewModel, - navController: NavController + nav: (String) -> Unit ) { val listState = rememberLazyListState() @@ -92,7 +91,7 @@ private fun FeedLoaded( state = listState ) { itemsIndexed(state.feed.value, key = { _, item -> item.pubkeyHex }) { _, item -> - UserCompose(item, accountViewModel = accountViewModel, navController = navController) + UserCompose(item, accountViewModel = accountViewModel, nav = nav) } } } 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 05e5c7fa6..bbad681ff 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 @@ -3,9 +3,11 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn import android.content.Context import android.content.Intent import android.net.Uri +import androidx.compose.runtime.Immutable import androidx.core.content.ContextCompat import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.map import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.R @@ -23,6 +25,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.util.Locale +@Immutable class AccountViewModel(private val account: Account) : ViewModel() { val accountLiveData: LiveData = account.live.map { it } val accountLanguagesLiveData: LiveData = account.liveLanguages.map { it } @@ -207,7 +210,7 @@ class AccountViewModel(private val account: Account) : ViewModel() { } fun isLoggedUser(user: User?): Boolean { - return account.userProfile() === user + return account.userProfile().pubkeyHex == user?.pubkeyHex } fun isFollowing(user: User?): Boolean { @@ -228,4 +231,22 @@ class AccountViewModel(private val account: Account) : ViewModel() { fun dontShowBlockAlertDialog() { account.setHideBlockAlertDialog() } + + fun hideSensitiveContent() { + account.updateShowSensitiveContent(false) + } + + fun disableContentWarnings() { + account.updateShowSensitiveContent(true) + } + + fun seeContentWarnings() { + account.updateShowSensitiveContent(null) + } + + class Factory(val account: Account) : ViewModelProvider.Factory { + override fun create(modelClass: Class): AccountViewModel { + return AccountViewModel(account) as AccountViewModel + } + } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BookmarkListScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BookmarkListScreen.kt index b9e79bef6..c81b6ea74 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BookmarkListScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BookmarkListScreen.kt @@ -19,7 +19,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavController import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.dal.BookmarkPrivateFeedFilter import com.vitorpamplona.amethyst.ui.dal.BookmarkPublicFeedFilter @@ -30,7 +29,7 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @Composable -fun BookmarkListScreen(accountViewModel: AccountViewModel, navController: NavController) { +fun BookmarkListScreen(accountViewModel: AccountViewModel, nav: (String) -> Unit) { val accountState by accountViewModel.accountLiveData.observeAsState() val account = accountState?.account @@ -74,8 +73,8 @@ fun BookmarkListScreen(accountViewModel: AccountViewModel, navController: NavCon } HorizontalPager(pageCount = 2, state = pagerState) { page -> when (page) { - 0 -> FeedView(privateFeedViewModel, accountViewModel, navController, null) - 1 -> FeedView(publicFeedViewModel, accountViewModel, navController, null) + 0 -> FeedView(privateFeedViewModel, accountViewModel, nav, null) + 1 -> FeedView(publicFeedViewModel, accountViewModel, nav, null) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt index c6bcc52ab..52f082482 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt @@ -60,7 +60,6 @@ import androidx.compose.ui.unit.sp import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavController import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Channel @@ -83,7 +82,7 @@ import com.vitorpamplona.amethyst.ui.screen.NostrChannelFeedViewModel fun ChannelScreen( channelId: String?, accountViewModel: AccountViewModel, - navController: NavController + nav: (String) -> Unit ) { val accountState by accountViewModel.accountLiveData.observeAsState() val account = accountState?.account @@ -143,7 +142,7 @@ fun ChannelScreen( ChannelHeader( channel, account, - navController = navController + nav = nav ) Column( @@ -152,7 +151,7 @@ fun ChannelScreen( .padding(vertical = 0.dp) .weight(1f, true) ) { - ChatroomFeedView(feedViewModel, accountViewModel, navController, "Channel/$channelId") { + ChatroomFeedView(feedViewModel, accountViewModel, nav, "Channel/$channelId") { replyTo.value = it } } @@ -168,7 +167,7 @@ fun ChannelScreen( null, innerQuote = true, accountViewModel = accountViewModel, - navController = navController, + nav = nav, onWantsToReply = { replyTo.value = it } @@ -219,7 +218,7 @@ fun ChannelScreen( onPost = { val tagger = NewMessageTagger(channel, listOfNotNull(replyTo.value?.author), listOfNotNull(replyTo.value), channelScreenModel.message.text) tagger.run() - account.sendChannelMessage(tagger.message, channel.idHex, tagger.replyTos, tagger.mentions) + account.sendChannelMessage(tagger.message, channel.idHex, tagger.replyTos, tagger.mentions, wantsToMarkAsSensitive = false) channelScreenModel.message = TextFieldValue("") replyTo.value = null feedViewModel.invalidateData() // Don't wait a full second before updating @@ -248,7 +247,7 @@ fun ChannelScreen( } @Composable -fun ChannelHeader(baseChannel: Channel, account: Account, navController: NavController) { +fun ChannelHeader(baseChannel: Channel, account: Account, nav: (String) -> Unit) { val channelState by baseChannel.live.observeAsState() val channel = channelState?.channel ?: return @@ -256,7 +255,7 @@ fun ChannelHeader(baseChannel: Channel, account: Account, navController: NavCont Column( Modifier.clickable { - navController.navigate("Channel/${baseChannel.idHex}") + nav("Channel/${baseChannel.idHex}") } ) { Column(modifier = Modifier.padding(12.dp)) { @@ -306,9 +305,9 @@ fun ChannelHeader(baseChannel: Channel, account: Account, navController: NavCont } if (account.followingChannels.contains(channel.idHex)) { - LeaveButton(account, channel, navController) + LeaveButton(account, channel, nav) } else { - JoinButton(account, channel, navController) + JoinButton(account, channel, nav) } } } @@ -349,7 +348,7 @@ private fun NoteCopyButton( expanded = popupExpanded, onDismissRequest = { popupExpanded = false } ) { - DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(note.idNote())); popupExpanded = false }) { + DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString("nostr:" + note.idNote())); popupExpanded = false }) { Text(stringResource(R.string.copy_channel_id_note_to_the_clipboard)) } } @@ -386,7 +385,7 @@ private fun EditButton(account: Account, channel: Channel) { } @Composable -private fun JoinButton(account: Account, channel: Channel, navController: NavController) { +private fun JoinButton(account: Account, channel: Channel, nav: (String) -> Unit) { Button( modifier = Modifier.padding(horizontal = 3.dp), onClick = { @@ -404,12 +403,12 @@ private fun JoinButton(account: Account, channel: Channel, navController: NavCon } @Composable -private fun LeaveButton(account: Account, channel: Channel, navController: NavController) { +private fun LeaveButton(account: Account, channel: Channel, nav: (String) -> Unit) { Button( modifier = Modifier.padding(horizontal = 3.dp), onClick = { account.leaveChannel(channel.idHex) - navController.navigate(Route.Message.route) + nav(Route.Message.route) }, shape = RoundedCornerShape(20.dp), colors = ButtonDefaults diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt index 8c7c26e02..880006ecd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt @@ -39,7 +39,6 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavController import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource import com.vitorpamplona.amethyst.ui.dal.ChatroomListKnownFeedFilter @@ -52,7 +51,7 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @Composable -fun ChatroomListScreen(accountViewModel: AccountViewModel, navController: NavController) { +fun ChatroomListScreen(accountViewModel: AccountViewModel, nav: (String) -> Unit) { val pagerState = rememberPagerState() val coroutineScope = rememberCoroutineScope() @@ -151,7 +150,7 @@ fun ChatroomListScreen(accountViewModel: AccountViewModel, navController: NavCon ChatroomListFeedView( viewModel = tabs[page].viewModel, accountViewModel = accountViewModel, - navController = navController, + nav = nav, markAsRead = tabs[page].markAsRead ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt index cf75e066a..a64bf071d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt @@ -44,7 +44,6 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavController import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User @@ -61,7 +60,7 @@ import com.vitorpamplona.amethyst.ui.screen.ChatroomFeedView import com.vitorpamplona.amethyst.ui.screen.NostrChatRoomFeedViewModel @Composable -fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navController: NavController) { +fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, nav: (String) -> Unit) { val accountState by accountViewModel.accountLiveData.observeAsState() val account = accountState?.account val context = LocalContext.current @@ -106,7 +105,7 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr Column(Modifier.fillMaxHeight()) { NostrChatroomDataSource.withUser?.let { - ChatroomHeader(it, account.userProfile(), navController = navController) + ChatroomHeader(it, account.userProfile(), nav = nav) } Column( @@ -115,7 +114,7 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr .padding(vertical = 0.dp) .weight(1f, true) ) { - ChatroomFeedView(feedViewModel, accountViewModel, navController, "Room/$userId") { + ChatroomFeedView(feedViewModel, accountViewModel, nav, "Room/$userId") { replyTo.value = it } } @@ -131,7 +130,7 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr null, innerQuote = true, accountViewModel = accountViewModel, - navController = navController, + nav = nav, onWantsToReply = { replyTo.value = it } @@ -179,7 +178,7 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr trailingIcon = { PostButton( onPost = { - account.sendPrivateMessage(chatRoomScreenModel.message.text, userId, replyTo.value, null) + account.sendPrivateMessage(chatRoomScreenModel.message.text, userId, replyTo.value, null, wantsToMarkAsSensitive = false) chatRoomScreenModel.message = TextFieldValue("") replyTo.value = null feedViewModel.invalidateData() // Don't wait a full second before updating @@ -208,10 +207,10 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr } @Composable -fun ChatroomHeader(baseUser: User, accountUser: User, navController: NavController) { +fun ChatroomHeader(baseUser: User, accountUser: User, nav: (String) -> Unit) { Column( modifier = Modifier.clickable( - onClick = { navController.navigate("User/${baseUser.pubkeyHex}") } + onClick = { nav("User/${baseUser.pubkeyHex}") } ) ) { Column(modifier = Modifier.padding(12.dp)) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HashtagScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HashtagScreen.kt index b2cd429e8..4d8489073 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HashtagScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HashtagScreen.kt @@ -22,7 +22,6 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavController import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.NostrHashtagDataSource import com.vitorpamplona.amethyst.ui.dal.HashtagFeedFilter @@ -32,7 +31,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @Composable -fun HashtagScreen(tag: String?, accountViewModel: AccountViewModel, navController: NavController) { +fun HashtagScreen(tag: String?, accountViewModel: AccountViewModel, nav: (String) -> Unit) { val accountState by accountViewModel.accountLiveData.observeAsState() val account = accountState?.account ?: return @@ -76,7 +75,7 @@ fun HashtagScreen(tag: String?, accountViewModel: AccountViewModel, navControlle modifier = Modifier.padding(vertical = 0.dp) ) { HashtagHeader(tag, account) - FeedView(feedViewModel, accountViewModel, navController, null) + FeedView(feedViewModel, accountViewModel, nav, null) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt index 243e57289..fa8a42c76 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt @@ -18,7 +18,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavController import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.dal.HiddenAccountsFeedFilter import com.vitorpamplona.amethyst.ui.screen.NostrHiddenAccountsFeedViewModel @@ -27,7 +26,7 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @Composable -fun HiddenUsersScreen(accountViewModel: AccountViewModel, navController: NavController) { +fun HiddenUsersScreen(accountViewModel: AccountViewModel, nav: (String) -> Unit) { val accountState by accountViewModel.accountLiveData.observeAsState() val account = accountState?.account @@ -55,7 +54,7 @@ fun HiddenUsersScreen(accountViewModel: AccountViewModel, navController: NavCont } HorizontalPager(pageCount = 1, state = pagerState) { page -> when (page) { - 0 -> UserFeedView(feedViewModel, accountViewModel, navController) + 0 -> UserFeedView(feedViewModel, accountViewModel, nav) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt index 713cafd32..10130e4c6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt @@ -25,7 +25,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver -import androidx.navigation.NavController import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.NostrHomeDataSource import com.vitorpamplona.amethyst.ui.dal.HomeConversationsFeedFilter @@ -37,6 +36,7 @@ import com.vitorpamplona.amethyst.ui.screen.FeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @@ -45,7 +45,7 @@ fun HomeScreen( homeFeedViewModel: NostrHomeFeedViewModel, repliesFeedViewModel: NostrHomeRepliesFeedViewModel, accountViewModel: AccountViewModel, - navController: NavController, + nav: (String) -> Unit, pagerState: PagerState, scrollToTop: Boolean = false, nip47: String? = null @@ -59,9 +59,9 @@ fun HomeScreen( LaunchedEffect(accountViewModel, account.defaultHomeFollowList) { HomeNewThreadFeedFilter.account = account HomeConversationsFeedFilter.account = account - NostrHomeDataSource.resetFilters() - homeFeedViewModel.invalidateData() - repliesFeedViewModel.invalidateData() + NostrHomeDataSource.invalidateFilters() + homeFeedViewModel.invalidateData(true) + repliesFeedViewModel.invalidateData(true) } if (wantsToAddNip47 != null) { @@ -74,9 +74,7 @@ fun HomeScreen( if (event == Lifecycle.Event.ON_RESUME) { HomeNewThreadFeedFilter.account = account HomeConversationsFeedFilter.account = account - NostrHomeDataSource.resetFilters() - homeFeedViewModel.invalidateData() - repliesFeedViewModel.invalidateData() + NostrHomeDataSource.invalidateFilters() } } @@ -86,6 +84,17 @@ fun HomeScreen( } } + if (scrollToTop) { + val scope = rememberCoroutineScope() + LaunchedEffect(key1 = Unit) { + scope.launch(Dispatchers.IO) { + NostrHomeDataSource.invalidateFilters() + homeFeedViewModel.invalidateData(true) + repliesFeedViewModel.invalidateData(true) + } + } + } + val tabs by remember(homeFeedViewModel, repliesFeedViewModel) { mutableStateOf( listOf( @@ -120,7 +129,7 @@ fun HomeScreen( FeedView( viewModel = tabs[page].viewModel, accountViewModel = accountViewModel, - navController = navController, + nav = nav, routeForLastRead = tabs[page].routeForLastRead, scrollStateKey = tabs[page].scrollStateKey, scrollToTop = scrollToTop diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt index e230e9d42..035034165 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt @@ -53,6 +53,14 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun skipHalfExpanded = true ) + val nav = remember { + { route: String -> + if (getRouteWithArguments(navController) != route) { + navController.navigate(route) + } + } + } + val accountState by accountViewModel.accountLiveData.observeAsState() val account = remember(accountState) { accountState?.account } @@ -76,7 +84,7 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun AppTopBar(followLists, navController, scaffoldState, accountViewModel) }, drawerContent = { - DrawerContent(navController, scaffoldState, sheetState, accountViewModel) + DrawerContent(nav, scaffoldState, sheetState, accountViewModel) BackHandler(enabled = scaffoldState.drawerState.isOpen) { scope.launch { scaffoldState.drawerState.close() } } @@ -97,6 +105,14 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun fun FloatingButtons(navController: NavHostController, accountViewModel: AccountViewModel, accountStateViewModel: AccountStateViewModel) { val accountState by accountStateViewModel.accountContent.collectAsState() + val nav = remember { + { route: String -> + if (getRouteWithArguments(navController) != route) { + navController.navigate(route) + } + } + } + Crossfade(targetState = accountState, animationSpec = tween(durationMillis = 100)) { state -> when (state) { is AccountState.LoggedInViewOnly -> { @@ -107,13 +123,13 @@ fun FloatingButtons(navController: NavHostController, accountViewModel: AccountV } is AccountState.LoggedIn -> { if (currentRoute(navController)?.substringBefore("?") == Route.Home.base) { - NewNoteButton(state.account, accountViewModel, navController) + NewNoteButton(state.account, accountViewModel, nav) } if (currentRoute(navController) == Route.Message.base) { - ChannelFabColumn(state.account, navController) + ChannelFabColumn(state.account, nav) } if (currentRoute(navController)?.substringBefore("?") == Route.Video.base) { - NewImageButton(accountViewModel, navController) + NewImageButton(accountViewModel, nav) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt index 298955136..b3090afec 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt @@ -13,6 +13,7 @@ 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.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush @@ -23,7 +24,6 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavController import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis import com.patrykandpatrick.vico.compose.axis.vertical.endAxis import com.patrykandpatrick.vico.compose.axis.vertical.startAxis @@ -52,6 +52,9 @@ import com.vitorpamplona.amethyst.ui.screen.CardFeedView import com.vitorpamplona.amethyst.ui.screen.NotificationViewModel import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange +import com.vitorpamplona.amethyst.ui.theme.RoyalBlue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import java.math.BigDecimal import java.math.RoundingMode import kotlin.math.roundToInt @@ -61,29 +64,24 @@ fun NotificationScreen( notifFeedViewModel: NotificationViewModel, userReactionsStatsModel: UserReactionsViewModel, accountViewModel: AccountViewModel, - navController: NavController, + nav: (String) -> Unit, scrollToTop: Boolean = false ) { val accountState by accountViewModel.accountLiveData.observeAsState() - val account = accountState?.account ?: return + val account = remember(accountState) { accountState?.account } ?: return - if (scrollToTop) { - notifFeedViewModel.clear() - } - - LaunchedEffect(account.userProfile().pubkeyHex, account.defaultNotificationFollowList) { - NostrAccountDataSource.resetFilters() + LaunchedEffect(accountViewModel, account.defaultNotificationFollowList) { + NostrAccountDataSource.invalidateFilters() NotificationFeedFilter.account = account - notifFeedViewModel.clear() - notifFeedViewModel.refresh() + notifFeedViewModel.invalidateData(true) } val lifeCycleOwner = LocalLifecycleOwner.current DisposableEffect(accountViewModel) { val observer = LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_RESUME) { + NostrAccountDataSource.invalidateFilters() NotificationFeedFilter.account = account - notifFeedViewModel.invalidateData() } } @@ -93,15 +91,27 @@ fun NotificationScreen( } } + if (scrollToTop) { + val scope = rememberCoroutineScope() + LaunchedEffect(key1 = Unit) { + scope.launch(Dispatchers.IO) { + notifFeedViewModel.clear() + notifFeedViewModel.invalidateData(true) + } + } + } + Column(Modifier.fillMaxHeight()) { Column( modifier = Modifier.padding(vertical = 0.dp) ) { - SummaryBar(userReactionsStatsModel, accountViewModel, navController) + SummaryBar( + model = userReactionsStatsModel + ) CardFeedView( viewModel = notifFeedViewModel, accountViewModel = accountViewModel, - navController = navController, + nav = nav, routeForLastRead = Route.Notification.base, scrollStateKey = ScrollStateKeys.NOTIFICATION_SCREEN, scrollToTop = scrollToTop @@ -111,18 +121,18 @@ fun NotificationScreen( } @Composable -fun SummaryBar(model: UserReactionsViewModel, accountViewModel: AccountViewModel, navController: NavController) { +fun SummaryBar(model: UserReactionsViewModel) { var showChart by remember { mutableStateOf(false) } - UserReactionsRow(model, accountViewModel, navController) { + UserReactionsRow(model) { showChart = !showChart } val lineChartCount = lineChart( - lines = listOf(Color.Cyan, Color.Green, Color.Red).map { lineChartColor -> + lines = listOf(RoyalBlue, Color.Green, Color.Red).map { lineChartColor -> LineChart.LineSpec( lineColor = lineChartColor.toArgb(), lineBackgroundShader = DynamicShaders.fromBrush( @@ -156,8 +166,8 @@ fun SummaryBar(model: UserReactionsViewModel, accountViewModel: AccountViewModel targetVerticalAxisPosition = AxisPosition.Vertical.End ) - model.chartModel?.let { - if (showChart) { + if (showChart) { + model.chartModel?.let { Row( modifier = Modifier .padding(vertical = 10.dp, horizontal = 20.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 485892a66..1fb9482eb 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 @@ -49,7 +49,6 @@ import androidx.core.content.ContextCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavController import coil.compose.AsyncImage import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account @@ -103,7 +102,7 @@ import kotlinx.coroutines.withContext import java.math.BigDecimal @Composable -fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navController: NavController) { +fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, nav: (String) -> Unit) { if (userId == null) return var userBase by remember { mutableStateOf(null) } @@ -118,14 +117,14 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro ProfileScreen( user = it, accountViewModel = accountViewModel, - navController = navController + nav = nav ) } } @OptIn(ExperimentalFoundationApi::class) @Composable -fun ProfileScreen(user: User, accountViewModel: AccountViewModel, navController: NavController) { +fun ProfileScreen(user: User, accountViewModel: AccountViewModel, nav: (String) -> Unit) { val accountState by accountViewModel.accountLiveData.observeAsState() val account = remember(accountState) { accountState?.account } ?: return @@ -210,7 +209,7 @@ fun ProfileScreen(user: User, accountViewModel: AccountViewModel, navController: .fillMaxHeight() ) { Column(modifier = Modifier.padding()) { - ProfileHeader(baseUser, navController, account, accountViewModel) + ProfileHeader(baseUser, nav, account, accountViewModel) ScrollableTabRow( backgroundColor = MaterialTheme.colors.background, selectedTabIndex = pagerState.currentPage, @@ -246,13 +245,13 @@ fun ProfileScreen(user: User, accountViewModel: AccountViewModel, navController: } ) { page -> when (page) { - 0 -> TabNotesNewThreads(accountViewModel, navController) - 1 -> TabNotesConversations(accountViewModel, navController) - 2 -> TabFollows(baseUser, accountViewModel, navController) - 3 -> TabFollowers(baseUser, accountViewModel, navController) - 4 -> TabReceivedZaps(baseUser, accountViewModel, navController) - 5 -> TabBookmarks(baseUser, accountViewModel, navController) - 6 -> TabReports(baseUser, accountViewModel, navController) + 0 -> TabNotesNewThreads(accountViewModel, nav) + 1 -> TabNotesConversations(accountViewModel, nav) + 2 -> TabFollows(baseUser, accountViewModel, nav) + 3 -> TabFollowers(baseUser, accountViewModel, nav) + 4 -> TabReceivedZaps(baseUser, accountViewModel, nav) + 5 -> TabBookmarks(baseUser, accountViewModel, nav) + 6 -> TabReports(baseUser, accountViewModel, nav) 7 -> TabRelays(baseUser, accountViewModel) } } @@ -331,7 +330,7 @@ private fun FollowTabHeader(baseUser: User) { @Composable private fun ProfileHeader( baseUser: User, - navController: NavController, + nav: (String) -> Unit, account: Account, accountViewModel: AccountViewModel ) { @@ -412,7 +411,7 @@ private fun ProfileHeader( .height(35.dp) .padding(bottom = 3.dp) ) { - MessageButton(baseUser, navController) + MessageButton(baseUser, nav) // No need for this button anymore // NPubCopyButton(baseUser) @@ -421,7 +420,7 @@ private fun ProfileHeader( } } - DrawAdditionalInfo(baseUser, account, accountViewModel, navController) + DrawAdditionalInfo(baseUser, account, accountViewModel, nav) Divider(modifier = Modifier.padding(top = 6.dp)) } @@ -468,7 +467,7 @@ private fun ProfileActions( } @Composable -private fun DrawAdditionalInfo(baseUser: User, account: Account, accountViewModel: AccountViewModel, navController: NavController) { +private fun DrawAdditionalInfo(baseUser: User, account: Account, accountViewModel: AccountViewModel, nav: (String) -> Unit) { val userState by baseUser.live().metadata.observeAsState() val user = remember(userState) { userState?.user } ?: return val tags = remember(userState) { userState?.user?.info?.latestMetadata?.tags } @@ -534,7 +533,7 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account, accountViewMode user, onScan = { dialogOpen = false - navController.navigate(it) + nav(it) }, onClose = { dialogOpen = false } ) @@ -553,7 +552,7 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account, accountViewMode } } - DisplayBadges(baseUser, navController) + DisplayBadges(baseUser, nav) DisplayNip05ProfileStatus(user) @@ -613,7 +612,7 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account, accountViewMode tags = null, backgroundColor = MaterialTheme.colors.background, accountViewModel = accountViewModel, - navController = navController + nav = nav ) } } @@ -701,7 +700,7 @@ private fun DisplayLNAddress( @OptIn(ExperimentalLayoutApi::class) private fun DisplayBadges( baseUser: User, - navController: NavController + nav: (String) -> Unit ) { val userBadgeState by baseUser.live().badges.observeAsState() val userBadge = remember(userBadgeState) { userBadgeState?.user } ?: return @@ -716,7 +715,7 @@ private fun DisplayBadges( val baseBadgeDefinition = badgeAwardState?.note?.replyTo?.firstOrNull() if (baseBadgeDefinition != null) { - BadgeThumb(baseBadgeDefinition, navController, 35.dp) + BadgeThumb(baseBadgeDefinition, nav, 35.dp) } } } @@ -728,12 +727,12 @@ private fun DisplayBadges( @Composable fun BadgeThumb( note: Note, - navController: NavController, + nav: (String) -> Unit, size: Dp, pictureModifier: Modifier = Modifier ) { BadgeThumb(note, size, pictureModifier) { - navController.navigate("Note/${it.idHex}") + nav("Note/${it.idHex}") } } @@ -831,7 +830,7 @@ private fun DrawBanner(baseUser: User) { } @Composable -fun TabNotesNewThreads(accountViewModel: AccountViewModel, navController: NavController) { +fun TabNotesNewThreads(accountViewModel: AccountViewModel, nav: (String) -> Unit) { val feedViewModel: NostrUserProfileNewThreadsFeedViewModel = viewModel() LaunchedEffect(Unit) { @@ -842,13 +841,13 @@ fun TabNotesNewThreads(accountViewModel: AccountViewModel, navController: NavCon Column( modifier = Modifier.padding(vertical = 0.dp) ) { - FeedView(feedViewModel, accountViewModel, navController, null, enablePullRefresh = false) + FeedView(feedViewModel, accountViewModel, nav, null, enablePullRefresh = false) } } } @Composable -fun TabNotesConversations(accountViewModel: AccountViewModel, navController: NavController) { +fun TabNotesConversations(accountViewModel: AccountViewModel, nav: (String) -> Unit) { val feedViewModel: NostrUserProfileConversationsFeedViewModel = viewModel() LaunchedEffect(Unit) { @@ -859,13 +858,13 @@ fun TabNotesConversations(accountViewModel: AccountViewModel, navController: Nav Column( modifier = Modifier.padding(vertical = 0.dp) ) { - FeedView(feedViewModel, accountViewModel, navController, null, enablePullRefresh = false) + FeedView(feedViewModel, accountViewModel, nav, null, enablePullRefresh = false) } } } @Composable -fun TabBookmarks(baseUser: User, accountViewModel: AccountViewModel, navController: NavController) { +fun TabBookmarks(baseUser: User, accountViewModel: AccountViewModel, nav: (String) -> Unit) { val feedViewModel: NostrUserProfileBookmarksFeedViewModel = viewModel() LaunchedEffect(Unit) { @@ -876,13 +875,13 @@ fun TabBookmarks(baseUser: User, accountViewModel: AccountViewModel, navControll Column( modifier = Modifier.padding(vertical = 0.dp) ) { - FeedView(feedViewModel, accountViewModel, navController, null, enablePullRefresh = false) + FeedView(feedViewModel, accountViewModel, nav, null, enablePullRefresh = false) } } } @Composable -fun TabFollows(baseUser: User, accountViewModel: AccountViewModel, navController: NavController) { +fun TabFollows(baseUser: User, accountViewModel: AccountViewModel, nav: (String) -> Unit) { val feedViewModel: NostrUserProfileFollowsUserFeedViewModel = viewModel() val userState by baseUser.live().follows.observeAsState() @@ -895,13 +894,13 @@ fun TabFollows(baseUser: User, accountViewModel: AccountViewModel, navController Column( modifier = Modifier.padding(vertical = 0.dp) ) { - UserFeedView(feedViewModel, accountViewModel, navController, enablePullRefresh = false) + UserFeedView(feedViewModel, accountViewModel, nav, enablePullRefresh = false) } } } @Composable -fun TabFollowers(baseUser: User, accountViewModel: AccountViewModel, navController: NavController) { +fun TabFollowers(baseUser: User, accountViewModel: AccountViewModel, nav: (String) -> Unit) { val feedViewModel: NostrUserProfileFollowersUserFeedViewModel = viewModel() val userState by baseUser.live().follows.observeAsState() @@ -914,13 +913,13 @@ fun TabFollowers(baseUser: User, accountViewModel: AccountViewModel, navControll Column( modifier = Modifier.padding(vertical = 0.dp) ) { - UserFeedView(feedViewModel, accountViewModel, navController, enablePullRefresh = false) + UserFeedView(feedViewModel, accountViewModel, nav, enablePullRefresh = false) } } } @Composable -fun TabReceivedZaps(baseUser: User, accountViewModel: AccountViewModel, navController: NavController) { +fun TabReceivedZaps(baseUser: User, accountViewModel: AccountViewModel, nav: (String) -> Unit) { val feedViewModel: NostrUserProfileZapsFeedViewModel = viewModel() val userState by baseUser.live().zaps.observeAsState() @@ -933,13 +932,13 @@ fun TabReceivedZaps(baseUser: User, accountViewModel: AccountViewModel, navContr Column( modifier = Modifier.padding(vertical = 0.dp) ) { - LnZapFeedView(feedViewModel, accountViewModel, navController, enablePullRefresh = false) + LnZapFeedView(feedViewModel, accountViewModel, nav, enablePullRefresh = false) } } } @Composable -fun TabReports(baseUser: User, accountViewModel: AccountViewModel, navController: NavController) { +fun TabReports(baseUser: User, accountViewModel: AccountViewModel, nav: (String) -> Unit) { val feedViewModel: NostrUserProfileReportFeedViewModel = viewModel() val userState by baseUser.live().reports.observeAsState() @@ -952,7 +951,7 @@ fun TabReports(baseUser: User, accountViewModel: AccountViewModel, navController Column( modifier = Modifier.padding(vertical = 0.dp) ) { - FeedView(feedViewModel, accountViewModel, navController, null, enablePullRefresh = false) + FeedView(feedViewModel, accountViewModel, nav, null, enablePullRefresh = false) } } } @@ -993,12 +992,12 @@ fun TabRelays(user: User, accountViewModel: AccountViewModel) { } @Composable -private fun MessageButton(user: User, navController: NavController) { +private fun MessageButton(user: User, nav: (String) -> Unit) { Button( modifier = Modifier .padding(horizontal = 3.dp) .width(50.dp), - onClick = { navController.navigate("Room/${user.pubkeyHex}") }, + onClick = { nav("Room/${user.pubkeyHex}") }, shape = RoundedCornerShape(20.dp), colors = ButtonDefaults .buttonColors( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt index 320e913c0..2bdce2350 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt @@ -48,7 +48,6 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.ViewModel import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavController import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Channel @@ -83,7 +82,7 @@ import kotlinx.coroutines.channels.Channel as CoroutineChannel fun SearchScreen( searchFeedViewModel: NostrGlobalFeedViewModel, accountViewModel: AccountViewModel, - navController: NavController, + nav: (String) -> Unit, scrollToTop: Boolean = false ) { val lifeCycleOwner = LocalLifecycleOwner.current @@ -124,8 +123,8 @@ fun SearchScreen( Column( modifier = Modifier.padding(vertical = 0.dp) ) { - SearchBar(accountViewModel, navController) - FeedView(searchFeedViewModel, accountViewModel, navController, null, ScrollStateKeys.GLOBAL_SCREEN, scrollToTop) + SearchBar(accountViewModel, nav) + FeedView(searchFeedViewModel, accountViewModel, nav, null, ScrollStateKeys.GLOBAL_SCREEN, scrollToTop) } } } @@ -185,7 +184,7 @@ class SearchBarViewModel : ViewModel() { @OptIn(FlowPreview::class) @Composable -private fun SearchBar(accountViewModel: AccountViewModel, navController: NavController) { +private fun SearchBar(accountViewModel: AccountViewModel, nav: (String) -> Unit) { val searchBarViewModel: SearchBarViewModel = viewModel() searchBarViewModel.account = accountViewModel.accountLiveData.value?.account @@ -306,12 +305,12 @@ private fun SearchBar(accountViewModel: AccountViewModel, navController: NavCont ) { itemsIndexed(searchBarViewModel.hashtagResults.value, key = { _, item -> "#" + item }) { _, item -> HashtagLine(item) { - navController.navigate("Hashtag/$item") + nav("Hashtag/$item") } } itemsIndexed(searchBarViewModel.searchResults.value, key = { _, item -> "u" + item.pubkeyHex }) { _, item -> - UserCompose(item, accountViewModel = accountViewModel, navController = navController) + UserCompose(item, accountViewModel = accountViewModel, nav = nav) } itemsIndexed(searchBarViewModel.searchResultsChannels.value, key = { _, item -> "c" + item.idHex }) { _, item -> @@ -327,12 +326,12 @@ private fun SearchBar(accountViewModel: AccountViewModel, navController: NavCont channelLastTime = null, channelLastContent = item.info.about, false, - onClick = { navController.navigate("Channel/${item.idHex}") } + onClick = { nav("Channel/${item.idHex}") } ) } itemsIndexed(searchBarViewModel.searchResultsNotes.value, key = { _, item -> "n" + item.idHex }) { _, item -> - NoteCompose(item, accountViewModel = accountViewModel, navController = navController) + NoteCompose(item, accountViewModel = accountViewModel, nav = nav) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ThreadScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ThreadScreen.kt index 2d5d6aa53..524711e09 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ThreadScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ThreadScreen.kt @@ -14,14 +14,13 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavController import com.vitorpamplona.amethyst.service.NostrThreadDataSource import com.vitorpamplona.amethyst.ui.dal.ThreadFeedFilter import com.vitorpamplona.amethyst.ui.screen.NostrThreadFeedViewModel import com.vitorpamplona.amethyst.ui.screen.ThreadFeedView @Composable -fun ThreadScreen(noteId: String?, accountViewModel: AccountViewModel, navController: NavController) { +fun ThreadScreen(noteId: String?, accountViewModel: AccountViewModel, nav: (String) -> Unit) { val account by accountViewModel.accountLiveData.observeAsState() val lifeCycleOwner = LocalLifecycleOwner.current @@ -63,7 +62,7 @@ fun ThreadScreen(noteId: String?, accountViewModel: AccountViewModel, navControl Column( modifier = Modifier.padding(vertical = 0.dp) ) { - ThreadFeedView(noteId, feedViewModel, accountViewModel, navController) + ThreadFeedView(noteId, feedViewModel, accountViewModel, nav) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt index 7dc3d86dc..7dd638332 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt @@ -7,7 +7,6 @@ import androidx.compose.animation.Crossfade import androidx.compose.animation.core.tween import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -47,19 +46,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.ColorMatrix import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavController import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted @@ -74,7 +68,6 @@ import com.vitorpamplona.amethyst.ui.actions.NewMediaModel import com.vitorpamplona.amethyst.ui.actions.NewMediaView import com.vitorpamplona.amethyst.ui.actions.NewPostView import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status -import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.dal.VideoFeedFilter import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.note.FileHeaderDisplay @@ -83,6 +76,7 @@ import com.vitorpamplona.amethyst.ui.note.LikeReaction import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture import com.vitorpamplona.amethyst.ui.note.NoteDropDownMenu import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay +import com.vitorpamplona.amethyst.ui.note.RenderRelay import com.vitorpamplona.amethyst.ui.note.ViewCountReaction import com.vitorpamplona.amethyst.ui.note.ZapReaction import com.vitorpamplona.amethyst.ui.screen.FeedEmpty @@ -92,6 +86,7 @@ import com.vitorpamplona.amethyst.ui.screen.LoadingFeed import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys import com.vitorpamplona.amethyst.ui.screen.rememberForeverPagerState +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -99,7 +94,7 @@ import kotlinx.coroutines.launch fun VideoScreen( videoFeedView: NostrVideoFeedViewModel, accountViewModel: AccountViewModel, - navController: NavController, + nav: (String) -> Unit, scrollToTop: Boolean = false ) { val lifeCycleOwner = LocalLifecycleOwner.current @@ -124,7 +119,6 @@ fun VideoScreen( VideoFeedFilter.account = account NostrVideoDataSource.account = account NostrVideoDataSource.start() - videoFeedView.invalidateData() } if (event == Lifecycle.Event.ON_PAUSE) { println("Video Stop") @@ -138,11 +132,21 @@ fun VideoScreen( } } + if (scrollToTop) { + val scope = rememberCoroutineScope() + LaunchedEffect(key1 = Unit) { + scope.launch(Dispatchers.IO) { + NostrVideoDataSource.resetFilters() + videoFeedView.invalidateData() + } + } + } + Column(Modifier.fillMaxHeight()) { Column( modifier = Modifier.padding(vertical = 0.dp) ) { - FeedView(videoFeedView, accountViewModel, navController, ScrollStateKeys.VIDEO_SCREEN, scrollToTop) + FeedView(videoFeedView, accountViewModel, nav, ScrollStateKeys.VIDEO_SCREEN, scrollToTop) } } } @@ -151,7 +155,7 @@ fun VideoScreen( fun FeedView( videoFeedView: NostrVideoFeedViewModel, accountViewModel: AccountViewModel, - navController: NavController, + nav: (String) -> Unit, scrollStateKey: String? = null, scrollToTop: Boolean = false ) { @@ -176,7 +180,7 @@ fun FeedView( SlidingCarousel( state.feed, accountViewModel, - navController, + nav, scrollStateKey, scrollToTop ) @@ -196,7 +200,7 @@ fun FeedView( fun SlidingCarousel( feed: MutableState>, accountViewModel: AccountViewModel, - navController: NavController, + nav: (String) -> Unit, scrollStateKey: String? = null, scrollToTop: Boolean = false ) { @@ -222,7 +226,7 @@ fun SlidingCarousel( } ) { index -> feed.value.getOrNull(index)?.let { note -> - RenderVideoOrPictureNote(note, accountViewModel, navController) + RenderVideoOrPictureNote(note, accountViewModel, nav) } } } @@ -231,7 +235,7 @@ fun SlidingCarousel( private fun RenderVideoOrPictureNote( note: Note, accountViewModel: AccountViewModel, - navController: NavController + nav: (String) -> Unit ) { val noteEvent = note.event @@ -255,7 +259,7 @@ private fun RenderVideoOrPictureNote( Column(Modifier.weight(1f)) { Row(Modifier.padding(10.dp), verticalAlignment = Alignment.Bottom) { Column(Modifier.size(55.dp), verticalArrangement = Arrangement.Center) { - NoteAuthorPicture(note, navController, loggedIn, 55.dp) + NoteAuthorPicture(note, nav, loggedIn, 55.dp) } Column( @@ -299,7 +303,7 @@ private fun RenderVideoOrPictureNote( verticalArrangement = Arrangement.Center ) { Row(horizontalArrangement = Arrangement.Center) { - ReactionsColumn(note, accountViewModel, navController) + ReactionsColumn(note, accountViewModel, nav) } } } @@ -308,41 +312,19 @@ private fun RenderVideoOrPictureNote( @Composable private fun RelayBadges(baseNote: Note) { val noteRelaysState by baseNote.live().relays.observeAsState() - val noteRelays = noteRelaysState?.note?.relays ?: emptySet() - - var expanded by remember { mutableStateOf(false) } - - val relaysToDisplay = noteRelays - - val uri = LocalUriHandler.current + val noteRelays = remember(noteRelaysState) { + noteRelaysState?.note?.relays ?: emptySet() + } FlowRow() { - relaysToDisplay.forEach { - val url = it.removePrefix("wss://").removePrefix("ws://") - Box( - Modifier - .size(15.dp) - .padding(1.dp) - ) { - RobohashFallbackAsyncImage( - robot = "https://$url/favicon.ico", - robotSize = 15.dp, - model = "https://$url/favicon.ico", - contentDescription = stringResource(id = R.string.relay_icon), - colorFilter = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0f) }), - modifier = Modifier - .fillMaxSize(1f) - .clip(shape = CircleShape) - .background(MaterialTheme.colors.background) - .clickable(onClick = { uri.openUri("https://$url") }) - ) - } + noteRelays.forEach { dirtyUrl -> + RenderRelay(dirtyUrl) } } } @Composable -fun ReactionsColumn(baseNote: Note, accountViewModel: AccountViewModel, navController: NavController) { +fun ReactionsColumn(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) { val accountState by accountViewModel.accountLiveData.observeAsState() val account = accountState?.account ?: return @@ -355,11 +337,11 @@ fun ReactionsColumn(baseNote: Note, accountViewModel: AccountViewModel, navContr } if (wantsToReplyTo != null) { - NewPostView({ wantsToReplyTo = null }, wantsToReplyTo, null, account, accountViewModel, navController) + NewPostView({ wantsToReplyTo = null }, wantsToReplyTo, null, account, accountViewModel, nav) } if (wantsToQuote != null) { - NewPostView({ wantsToQuote = null }, null, wantsToQuote, account, accountViewModel, navController) + NewPostView({ wantsToQuote = null }, null, wantsToQuote, account, accountViewModel, nav) } Spacer(modifier = Modifier.height(8.dp)) @@ -380,7 +362,7 @@ fun ReactionsColumn(baseNote: Note, accountViewModel: AccountViewModel, navContr @OptIn(ExperimentalPermissionsApi::class) @Composable -fun NewImageButton(accountViewModel: AccountViewModel, navController: NavController) { +fun NewImageButton(accountViewModel: AccountViewModel, nav: (String) -> Unit) { var wantsToPost by remember { mutableStateOf(false) } @@ -397,15 +379,7 @@ fun NewImageButton(accountViewModel: AccountViewModel, navController: NavControl // awaits an refresh on the list delay(250) val route = Route.Video.route.replace("{scrollToTop}", "true") - navController.navigate(route) { - navController.graph.startDestinationRoute?.let { start -> - popUpTo(start) { inclusive = false } - restoreState = true - } - - launchSingleTop = true - restoreState = true - } + nav(route) } } @@ -445,7 +419,7 @@ fun NewImageButton(accountViewModel: AccountViewModel, navController: NavControl onClose = { pickedURI = null }, postViewModel = postViewModel, accountViewModel = accountViewModel, - navController = navController + nav = nav ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt index f2609a161..c97fb88c6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt @@ -1,12 +1,15 @@ package com.vitorpamplona.amethyst.ui.theme import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.ColorMatrix val Purple200 = Color(0xFFBB86FC) val Purple500 = Color(0xFF6200EE) val Purple700 = Color(0xFF3700B3) val Teal200 = Color(0xFF03DAC5) val BitcoinOrange = Color(0xFFF7931A) +val RoyalBlue = Color(0xFF4169E1) val Following = Color(0xFF03DAC5) val Nip05 = Color(0xFF01BAFF) @@ -14,3 +17,5 @@ val FollowsFollow = Color.Yellow val NIP05Verified = Color.Blue val WarningColor = Color(0xFFC62828) + +val RelayIconFilter = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0.5f) }) 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 ed7b30ae4..7fcedea44 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 @@ -7,19 +7,22 @@ import androidx.compose.material.darkColors import androidx.compose.material.lightColors import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalView private val DarkColorPalette = darkColors( primary = Purple200, primaryVariant = Purple700, - secondary = Teal200 + secondary = Teal200, + secondaryVariant = Color(0xFFF7931A) ) private val LightColorPalette = lightColors( primary = Purple500, primaryVariant = Purple700, - secondary = Teal200 + secondary = Teal200, + secondaryVariant = Color(0xFFB66605) /* Other default colors to override background = Color.White, diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 86d2c42c4..649cf45ae 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -385,4 +385,10 @@ Csatlakozás Ma + + Tartalomra vonatkozó figyelmeztetés + Ez a bejegyzés érzékeny tartalmat tartalmaz, amelyet egyesek sértőnek vagy zavarónak találhatnak + Az érzékeny tartalmat mindig rejtse el + Az érzékeny tartalmat mindig jelenítse meg + A tartalomra vonatkozó figyelmeztetéseket mindig jelenítse meg diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 2db0e85dc..d662b8a8a 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -149,7 +149,7 @@ descrição para e foto para Sair - Remover seguidor + Desseguir Canal criado Informação do canal mudou para Chat público diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b10e09631..5a2b1bef5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -404,4 +404,10 @@ Join Today + + Content warning + This post contains sensitive content which some people may find offensive or disturbing + Always hide sensitive content + Always show sensitive content + Always show content warnings diff --git a/app/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt b/app/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt index 8e55bfcca..4825ac45b 100644 --- a/app/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt +++ b/app/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt @@ -30,7 +30,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.core.os.ConfigurationCompat -import androidx.navigation.NavController import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService import com.vitorpamplona.amethyst.service.lang.ResultOrError @@ -47,7 +46,7 @@ fun TranslatableRichTextViewer( tags: List>?, backgroundColor: Color, accountViewModel: AccountViewModel, - navController: NavController + nav: (String) -> Unit ) { var translatedTextState by remember { mutableStateOf(ResultOrError(content, null, null, null)) @@ -93,7 +92,7 @@ fun TranslatableRichTextViewer( tags, backgroundColor, accountViewModel, - navController + nav ) val target = translatedTextState.targetLang diff --git a/build.gradle b/build.gradle index 4d3a662e0..e8f7522c3 100644 --- a/build.gradle +++ b/build.gradle @@ -6,18 +6,18 @@ buildscript { nav_version = "2.5.3" room_version = "2.4.3" accompanist_version = '0.30.0' - coil_version = '2.3.0' - vico_version = '1.6.5' + coil_version = '2.4.0' + vico_version = '1.6.6' } dependencies { classpath 'com.google.gms:google-services:4.3.15' } }// Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '8.0.1' apply false - id 'com.android.library' version '8.0.1' apply false - id 'org.jetbrains.kotlin.android' version '1.8.10' apply false - id 'org.jetbrains.kotlin.jvm' version '1.8.10' apply false + id 'com.android.application' version '8.0.2' apply false + id 'com.android.library' version '8.0.2' apply false + id 'org.jetbrains.kotlin.android' version '1.8.21' apply false + id 'org.jetbrains.kotlin.jvm' version '1.8.21' apply false } task installGitHook(type: Copy) {