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