diff --git a/app/build.gradle b/app/build.gradle
index 2da3a6079..b229f89ee 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -12,8 +12,8 @@ android {
applicationId "com.vitorpamplona.amethyst"
minSdk 26
targetSdk 33
- versionCode 108
- versionName "0.30.1"
+ versionCode 109
+ versionName "0.30.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -114,7 +114,7 @@ dependencies {
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
// Bitcoin secp256k1 bindings to Android
- implementation 'fr.acinq.secp256k1:secp256k1-kmp-jni-android:0.7.1'
+ implementation 'fr.acinq.secp256k1:secp256k1-kmp-jni-android:0.8.0'
// Nostr Base Protocol
implementation('com.github.vitorpamplona.NostrPostr:nostrpostrlib:master-SNAPSHOT') {
@@ -137,14 +137,14 @@ dependencies {
implementation 'androidx.security:security-crypto-ktx:1.1.0-alpha05'
// view videos
- implementation 'com.google.android.exoplayer:exoplayer:2.18.4'
+ implementation 'com.google.android.exoplayer:exoplayer:2.18.5'
// Load images from the web.
- implementation "io.coil-kt:coil-compose:2.2.2"
+ implementation "io.coil-kt:coil-compose:$coil_version"
// view gifs
- implementation "io.coil-kt:coil-gif:2.2.2"
+ implementation "io.coil-kt:coil-gif:$coil_version"
// view svgs
- implementation("io.coil-kt:coil-svg:2.2.2")
+ implementation "io.coil-kt:coil-svg:$coil_version"
// Rendering clickable text
implementation "com.google.accompanist:accompanist-flowlayout:$accompanist_version"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c22a472ac..518e6a539 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -43,6 +43,13 @@
+
+
+
+
+
+
+
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 dea1ea08d..ce1c4d922 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt
@@ -156,7 +156,7 @@ class Account(
}
}
- fun createZapRequestFor(note: Note, pollOption: Int?): LnZapRequestEvent? {
+ fun createZapRequestFor(note: Note, pollOption: Int?, message: String = ""): LnZapRequestEvent? {
if (!isWriteable()) return null
note.event?.let { event ->
@@ -167,7 +167,10 @@ class Account(
loggedIn.privKey!!,
pollOption
)
- }
+ /*note.event?.let {
+ return LnZapRequestEvent.create(it, userProfile().latestContactList?.relays()?.keys?.ifEmpty { null } ?: localRelays.map { it.url }.toSet(), loggedIn.privKey!!, message)
+ }*/
+
return null
}
@@ -189,10 +192,10 @@ class Account(
return createZapRequestFor(user.pubkeyHex)
}
- fun createZapRequestFor(userPubKeyHex: String): LnZapRequestEvent? {
+ fun createZapRequestFor(userPubKeyHex: String, message: String = ""): LnZapRequestEvent? {
if (!isWriteable()) return null
- return LnZapRequestEvent.create(userPubKeyHex, userProfile().latestContactList?.relays()?.keys?.ifEmpty { null } ?: localRelays.map { it.url }.toSet(), loggedIn.privKey!!)
+ return LnZapRequestEvent.create(userPubKeyHex, userProfile().latestContactList?.relays()?.keys?.ifEmpty { null } ?: localRelays.map { it.url }.toSet(), loggedIn.privKey!!, message)
}
fun report(note: Note, type: ReportEvent.ReportType, content: String = "") {
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip47.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip47.kt
index 8b38638b3..4fb1bdcde 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip47.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip47.kt
@@ -1,10 +1,11 @@
package com.vitorpamplona.amethyst.ui.note
import android.net.Uri
+import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.decodePublicKey
import com.vitorpamplona.amethyst.model.toHexKey
-data class Nip47URI(val pubKeyHex: String, val relayUri: String?, val secret: String?)
+data class Nip47URI(val pubKeyHex: HexKey, val relayUri: String?, val secret: HexKey?)
// Rename to the corect nip number when ready.
object Nip47 {
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt
index b59120a58..fda096ec5 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt
@@ -30,25 +30,25 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SingleEventFeed") {
null
}
- if (hexToWatch == null) {
- return null
- }
-
// downloads all the reactions to a given event.
- return listOf(
- TypedFilter(
- types = FeedType.values().toSet(),
- filter = JsonFilter(
- ids = listOfNotNull(hexToWatch)
+ return listOfNotNull(
+ hexToWatch?.let {
+ TypedFilter(
+ types = FeedType.values().toSet(),
+ filter = JsonFilter(
+ ids = listOfNotNull(hexToWatch)
+ )
)
- ),
- TypedFilter(
- types = FeedType.values().toSet(),
- filter = JsonFilter(
- kinds = listOf(MetadataEvent.kind),
- authors = listOfNotNull(hexToWatch)
+ },
+ hexToWatch?.let {
+ TypedFilter(
+ types = FeedType.values().toSet(),
+ filter = JsonFilter(
+ kinds = listOf(MetadataEvent.kind),
+ authors = listOfNotNull(hexToWatch)
+ )
)
- ),
+ },
TypedFilter(
types = FeedType.values().toSet(),
filter = JsonFilter(
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt
index 272a93b39..ca34b801b 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt
@@ -71,8 +71,6 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
return null
}
- val now = Date().time / 1000
-
return reactionsToWatch.map {
TypedFilter(
types = FeedType.values().toSet(),
@@ -142,7 +140,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
val addresses = createAddressFilter()
val addressReactions = createTagToAddressFilter()
- singleEventChannel.typedFilters = listOfNotNull(reactions, missing, addresses, addressReactions).flatten().ifEmpty { null }
+ singleEventChannel.typedFilters = listOfNotNull(missing, addresses, reactions, addressReactions).flatten().ifEmpty { null }
}
fun add(eventId: Note) {
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 360793456..fae2d053b 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
@@ -24,9 +24,10 @@ class LnZapRequestEvent(
relays: Set,
privateKey: ByteArray,
pollOption: Int?,
+ message: String,
createdAt: Long = Date().time / 1000
): LnZapRequestEvent {
- val content = ""
+ val content = message
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
var tags = listOf(
listOf("e", originalNote.id()),
@@ -49,14 +50,19 @@ class LnZapRequestEvent(
userHex: String,
relays: Set,
privateKey: ByteArray,
+ pollOption: Int?,
+ message: String,
createdAt: Long = Date().time / 1000
): LnZapRequestEvent {
- val content = ""
+ val content = message
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
- val tags = listOf(
+ var tags = listOf(
listOf("p", userHex),
listOf("relays") + relays
)
+ if (pollOption != null && pollOption >= 0) {
+ tags = tags + listOf(listOf(POLL_OPTION, pollOption.toString()))
+ }
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Constants.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Constants.kt
index 5632e9da5..a167a2d99 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Constants.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Constants.kt
@@ -4,6 +4,7 @@ import com.vitorpamplona.amethyst.model.RelaySetupInfo
object Constants {
val activeTypes = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS)
+ val activeTypesChats = setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.PRIVATE_DMS)
val activeTypesGlobalChats = setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.PRIVATE_DMS, FeedType.GLOBAL)
val activeTypesSearch = setOf(FeedType.SEARCH)
@@ -14,16 +15,18 @@ object Constants {
}
val defaultRelays = arrayOf(
- // Free relays
- RelaySetupInfo("wss://nostr.bitcoiner.social", read = true, write = true, feedTypes = activeTypes),
- RelaySetupInfo("wss://relay.nostr.bg", read = true, write = true, feedTypes = activeTypes),
+ // Free relays for DMs and Follows
+ RelaySetupInfo("wss://no.str.cr", read = true, write = true, feedTypes = activeTypes),
RelaySetupInfo("wss://relay.snort.social", read = true, write = true, feedTypes = activeTypes),
RelaySetupInfo("wss://relay.damus.io", read = true, write = true, feedTypes = activeTypes),
- RelaySetupInfo("wss://nostr.oxtr.dev", read = true, write = true, feedTypes = activeTypes),
- RelaySetupInfo("wss://nostr-pub.wellorder.net", read = true, write = true, feedTypes = activeTypes),
- RelaySetupInfo("wss://nostr.mom", read = true, write = true, feedTypes = activeTypes),
- RelaySetupInfo("wss://no.str.cr", read = true, write = true, feedTypes = activeTypes),
- RelaySetupInfo("wss://nos.lol", read = true, write = true, feedTypes = activeTypes),
+
+ // Chats
+ RelaySetupInfo("wss://nostr.bitcoiner.social", read = true, write = true, feedTypes = activeTypesChats),
+ RelaySetupInfo("wss://relay.nostr.bg", read = true, write = true, feedTypes = activeTypesChats),
+ RelaySetupInfo("wss://nostr.oxtr.dev", read = true, write = true, feedTypes = activeTypesChats),
+ RelaySetupInfo("wss://nostr-pub.wellorder.net", read = true, write = true, feedTypes = activeTypesChats),
+ RelaySetupInfo("wss://nostr.mom", read = true, write = true, feedTypes = activeTypesChats),
+ RelaySetupInfo("wss://nos.lol", read = true, write = true, feedTypes = activeTypesChats),
// Less Reliable
// NewRelayListViewModel.Relay("wss://nostr.orangepill.dev", read = true, write = true, feedTypes = activeTypes),
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 c4f819831..8ab0b4d97 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
@@ -182,7 +182,7 @@ class Relay(
val filters = Client.getSubscriptionFilters(requestId).filter { activeTypes.intersect(it.types).isNotEmpty() }
if (filters.isNotEmpty()) {
val request =
- """["REQ","$requestId",${filters.take(10).joinToString(",") { it.filter.toJson(url) }}]"""
+ """["REQ","$requestId",${filters.take(40).joinToString(",") { it.filter.toJson(url) }}]"""
// println("FILTERSSENT $url $request")
socket?.send(request)
eventUploadCounterInBytes += request.bytesUsedInMemory()
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 3afecdcb7..49823b3e4 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt
@@ -21,6 +21,8 @@ import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.ServiceManager
import com.vitorpamplona.amethyst.service.nip19.Nip19
import com.vitorpamplona.amethyst.service.relays.Client
+import com.vitorpamplona.amethyst.ui.navigation.Route
+import com.vitorpamplona.amethyst.ui.note.Nip47
import com.vitorpamplona.amethyst.ui.screen.AccountScreen
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
@@ -28,6 +30,8 @@ import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
+import java.net.URLEncoder
+import java.nio.charset.StandardCharsets
class MainActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
@@ -40,6 +44,14 @@ class MainActivity : FragmentActivity() {
Nip19.Type.EVENT -> "Event/${nip19.hex}"
Nip19.Type.ADDRESS -> "Note/${nip19.hex}"
else -> null
+ } ?: try {
+ intent?.data?.toString()?.let {
+ Nip47.parse(it)
+ val encodedUri = URLEncoder.encode(it, StandardCharsets.UTF_8.toString())
+ Route.Home.base + "?nip47=" + encodedUri
+ }
+ } catch (e: Exception) {
+ null
}
Coil.setImageLoader {
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 0b83d9faa..25635f39b 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
@@ -49,6 +49,9 @@ open class NewPostViewModel : ViewModel() {
this.mentions = currentMentions.plus(replyUser)
}
}
+ } ?: {
+ replyTos = null
+ mentions = null
}
quote?.let {
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataView.kt
index 3a683ec89..130c20f83 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataView.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataView.kt
@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.actions
+import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -18,6 +19,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
@@ -30,9 +32,14 @@ import com.vitorpamplona.amethyst.model.Account
@Composable
fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
val postViewModel: NewUserMetadataViewModel = viewModel()
+ val context = LocalContext.current
LaunchedEffect(Unit) {
postViewModel.load(account)
+
+ postViewModel.imageUploadingError.collect { error ->
+ Toast.makeText(context, error, Toast.LENGTH_SHORT).show()
+ }
}
Dialog(
@@ -141,6 +148,15 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
},
+ leadingIcon = {
+ UploadFromGallery(
+ isUploading = postViewModel.isUploadingImageForPicture,
+ tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
+ modifier = Modifier.padding(start = 5.dp)
+ ) {
+ postViewModel.uploadForPicture(it, context)
+ }
+ },
singleLine = true
)
@@ -157,6 +173,15 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
},
+ leadingIcon = {
+ UploadFromGallery(
+ isUploading = postViewModel.isUploadingImageForBanner,
+ tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
+ modifier = Modifier.padding(start = 5.dp)
+ ) {
+ postViewModel.uploadForBanner(it, context)
+ }
+ },
singleLine = true
)
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt
index 0b8e272ff..e6a9bf2e0 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt
@@ -1,13 +1,20 @@
package com.vitorpamplona.amethyst.ui.actions
+import android.content.Context
+import android.net.Uri
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.ObjectNode
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.model.GitHubIdentity
import com.vitorpamplona.amethyst.service.model.MastodonIdentity
import com.vitorpamplona.amethyst.service.model.TwitterIdentity
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.launch
import java.io.ByteArrayInputStream
import java.io.StringWriter
@@ -30,6 +37,10 @@ class NewUserMetadataViewModel : ViewModel() {
val github = mutableStateOf("")
val mastodon = mutableStateOf("")
+ var isUploadingImageForPicture by mutableStateOf(false)
+ var isUploadingImageForBanner by mutableStateOf(false)
+ val imageUploadingError = MutableSharedFlow()
+
fun load(account: Account) {
this.account = account
@@ -127,4 +138,49 @@ class NewUserMetadataViewModel : ViewModel() {
github.value = ""
mastodon.value = ""
}
+
+ fun uploadForPicture(uri: Uri, context: Context) {
+ upload(
+ uri,
+ context,
+ onUploading = {
+ isUploadingImageForPicture = it
+ },
+ onUploaded = {
+ picture.value = it
+ }
+ )
+ }
+
+ fun uploadForBanner(uri: Uri, context: Context) {
+ upload(
+ uri,
+ context,
+ onUploading = {
+ isUploadingImageForBanner = it
+ },
+ onUploaded = {
+ banner.value = it
+ }
+ )
+ }
+
+ fun upload(it: Uri, context: Context, onUploading: (Boolean) -> Unit, onUploaded: (String) -> Unit) {
+ onUploading(true)
+
+ ImageUploader.uploadImage(
+ uri = it,
+ contentResolver = context.contentResolver,
+ onSuccess = { imageUrl ->
+ onUploading(false)
+ onUploaded(imageUrl)
+ },
+ onError = {
+ onUploading(false)
+ viewModelScope.launch {
+ imageUploadingError.emit("Failed to upload the image / video")
+ }
+ }
+ )
+ }
}
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt
index 8cf0b80c4..2fff33f83 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt
@@ -130,7 +130,7 @@ fun InvoiceRequest(lud16: String, toUserPubKeyHex: String, account: Account, onC
Button(
modifier = Modifier.fillMaxWidth().padding(vertical = 10.dp),
onClick = {
- val zapRequest = account.createZapRequestFor(toUserPubKeyHex)
+ val zapRequest = account.createZapRequestFor(toUserPubKeyHex, message)
LightningAddressResolver().lnAddressInvoice(
lud16,
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableImageView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableImageView.kt
index 84486200d..729ecadfc 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableImageView.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableImageView.kt
@@ -10,8 +10,12 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.InlineTextContent
+import androidx.compose.foundation.text.appendInlineContent
+import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
+import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -24,14 +28,21 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.Placeholder
+import androidx.compose.ui.text.PlaceholderVerticalAlign
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import coil.compose.AsyncImage
+import coil.compose.AsyncImagePainter
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.PagerState
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.CloseButton
+import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation
import com.vitorpamplona.amethyst.ui.actions.SaveToGallery
import net.engawapg.lib.zoomable.rememberZoomState
import net.engawapg.lib.zoomable.zoomable
@@ -46,6 +57,11 @@ fun ZoomableImageView(word: String, images: List = listOf(word)) {
mutableStateOf(false)
}
+ // store the dialog open or close state
+ var imageState by remember {
+ mutableStateOf(null)
+ }
+
if (imageExtension.matcher(word).matches()) {
AsyncImage(
model = word,
@@ -63,8 +79,48 @@ fun ZoomableImageView(word: String, images: List = listOf(word)) {
.combinedClickable(
onClick = { dialogOpen = true },
onLongClick = { clipboardManager.setText(AnnotatedString(word)) }
- )
+ ),
+ onLoading = {
+ imageState = it
+ },
+ onSuccess = {
+ imageState = it
+ }
)
+
+ if (imageState !is AsyncImagePainter.State.Success) {
+ ClickableUrl(urlText = "$word ", url = word)
+
+ val myId = "inlineContent"
+ val emptytext = buildAnnotatedString {
+ withStyle(
+ LocalTextStyle.current.copy(color = MaterialTheme.colors.primary).toSpanStyle()
+ ) {
+ append("")
+ appendInlineContent(myId, "[icon]")
+ }
+ }
+ val inlineContent = mapOf(
+ Pair(
+ myId,
+ InlineTextContent(
+ Placeholder(
+ width = 17.sp,
+ height = 17.sp,
+ placeholderVerticalAlign = PlaceholderVerticalAlign.Center
+ )
+ ) {
+ LoadingAnimation()
+ }
+ )
+ )
+
+ // Empty Text for Size of Icon
+ Text(
+ text = emptytext,
+ inlineContent = inlineContent
+ )
+ }
} else {
VideoView(word) { dialogOpen = true }
}
@@ -86,7 +142,9 @@ fun ZoomableImageDialog(imageUrl: String, allImages: List = listOf(image
var pagerState: PagerState = remember { PagerState() }
Row(
- modifier = Modifier.padding(10.dp).fillMaxWidth(),
+ modifier = Modifier
+ .padding(10.dp)
+ .fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
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 2b01244b0..507e5ccab 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
@@ -45,4 +45,8 @@ object NotificationFeedFilter : FeedFilter() {
.toList()
.reversed()
}
+
+ fun isDifferentAccount(account: Account): Boolean {
+ return this::account.isInitialized && this.account != account
+ }
}
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 c536fb83c..d7418272c 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
@@ -1,8 +1,12 @@
package com.vitorpamplona.amethyst.ui.navigation
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
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.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
@@ -39,6 +43,7 @@ fun AppNavigation(
nextPage: String? = null
) {
val homePagerState = rememberPagerState()
+ var actionableNextPage by remember { mutableStateOf(nextPage) }
// Avoids creating ViewModels for performance reasons (up to 1 second delays)
val accountState by accountViewModel.accountLiveData.observeAsState()
@@ -76,8 +81,9 @@ fun AppNavigation(
}
Route.Home.let { route ->
- composable(route.route, route.arguments, content = {
+ composable(route.route, route.arguments, content = { it ->
val scrollToTop = it.arguments?.getBoolean("scrollToTop") ?: false
+ val nip47 = it.arguments?.getString("nip47")
HomeScreen(
homeFeedViewModel = homeFeedViewModel,
@@ -85,6 +91,28 @@ fun AppNavigation(
accountViewModel = accountViewModel,
navController = navController,
pagerState = homePagerState,
+ scrollToTop = scrollToTop,
+ nip47 = nip47
+ )
+
+ // Avoids running scroll to top when back button is pressed
+ if (scrollToTop) {
+ it.arguments?.remove("scrollToTop")
+ }
+ if (nip47 != null) {
+ it.arguments?.remove("nip47")
+ }
+ })
+ }
+
+ Route.Notification.let { route ->
+ composable(route.route, route.arguments, content = {
+ val scrollToTop = it.arguments?.getBoolean("scrollToTop") ?: false
+
+ NotificationScreen(
+ notifFeedViewModel = notifFeedViewModel,
+ accountViewModel = accountViewModel,
+ navController = navController,
scrollToTop = scrollToTop
)
@@ -96,7 +124,6 @@ fun AppNavigation(
}
composable(Route.Message.route, content = { ChatroomListScreen(accountViewModel, navController) })
- composable(Route.Notification.route, content = { NotificationScreen(notifFeedViewModel, accountViewModel, navController) })
composable(Route.BlockedUsers.route, content = { HiddenUsersScreen(accountViewModel, navController) })
composable(Route.Bookmarks.route, content = { BookmarkListScreen(accountViewModel, navController) })
@@ -161,7 +188,10 @@ fun AppNavigation(
}
}
- if (nextPage != null) {
- navController.navigate(nextPage)
+ actionableNextPage?.let {
+ LaunchedEffect(it) {
+ navController.navigate(it)
+ }
+ actionableNextPage = null
}
}
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 99ea241af..ddbe4d307 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
@@ -24,9 +24,12 @@ sealed class Route(
get() = route.substringBefore("?")
object Home : Route(
- route = "Home?scrollToTop={scrollToTop}",
+ route = "Home?scrollToTop={scrollToTop}&nip47={nip47}",
icon = R.drawable.ic_home,
- arguments = listOf(navArgument("scrollToTop") { type = NavType.BoolType; defaultValue = false }),
+ arguments = listOf(
+ navArgument("scrollToTop") { type = NavType.BoolType; defaultValue = false },
+ navArgument("nip47") { type = NavType.StringType; nullable = true; defaultValue = null }
+ ),
hasNewItems = { accountViewModel, cache -> homeHasNewItems(accountViewModel, cache) }
)
@@ -37,8 +40,9 @@ sealed class Route(
)
object Notification : Route(
- route = "Notification",
+ route = "Notification?scrollToTop={scrollToTop}",
icon = R.drawable.ic_notifications,
+ arguments = listOf(navArgument("scrollToTop") { type = NavType.BoolType; defaultValue = false }),
hasNewItems = { accountViewModel, cache -> notificationHasNewItems(accountViewModel, cache) }
)
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 1f329f01d..c4f2927e9 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
@@ -346,7 +346,7 @@ fun NoteComposeInner(
}
val pow = noteEvent.getPoWRank()
- if (pow > 1) {
+ if (pow > 20) {
DisplayPoW(pow)
}
}
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 83e6599a1..74748ab49 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
@@ -303,10 +303,11 @@ fun ZapReaction(
val zapsState by baseNote.live().zaps.observeAsState()
val zappedNote = zapsState?.note
+ val zapMessage = ""
var wantsToZap by remember { mutableStateOf(false) }
var wantsToChangeZapAmount by remember { mutableStateOf(false) }
-
+ var wantsToSetCustomZap by remember { mutableStateOf(false) }
val grayTint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
val context = LocalContext.current
val scope = rememberCoroutineScope()
@@ -348,7 +349,7 @@ fun ZapReaction(
baseNote,
account.zapAmountChoices.first() * 1000,
null,
- "",
+ zapMessage,
context,
onError = {
scope.launch {
@@ -371,6 +372,9 @@ fun ZapReaction(
},
onLongClick = {
wantsToChangeZapAmount = true
+ },
+ onDoubleClick = {
+ wantsToSetCustomZap = true
}
)
) {
@@ -403,6 +407,10 @@ fun ZapReaction(
UpdateZapAmountDialog({ wantsToChangeZapAmount = false }, account = account)
}
+ if (wantsToSetCustomZap) {
+ ZapCustomDialog({ wantsToSetCustomZap = false }, account = account, accountViewModel, baseNote)
+ }
+
if (zappedNote?.isZappedBy(account.userProfile()) == true) {
zappingProgress = 1f
Icon(
@@ -531,7 +539,7 @@ fun ZapAmountChoicePopup(
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
-
+ val zapMessage = ""
val scope = rememberCoroutineScope()
Popup(
@@ -549,7 +557,7 @@ fun ZapAmountChoicePopup(
baseNote,
amountInSats * 1000,
null,
- "",
+ zapMessage,
context,
onError,
onProgress
@@ -574,7 +582,7 @@ fun ZapAmountChoicePopup(
baseNote,
amountInSats * 1000,
null,
- "",
+ zapMessage,
context,
onError,
onProgress
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt
index ef6e46360..7b0899c6b 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt
@@ -49,6 +49,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
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.input.ImeAction
@@ -114,20 +115,38 @@ class UpdateZapAmountViewModel : ViewModel() {
account?.changeZapAmounts(amountSet)
if (walletConnectRelay.text.isNotBlank() && walletConnectPubkey.text.isNotBlank()) {
+ val pubkeyHex = try {
+ decodePublicKey(walletConnectPubkey.text.trim()).toHexKey()
+ } catch (e: Exception) {
+ null
+ }
+
+ val relayUrl = walletConnectRelay.text.ifBlank { null }?.let {
+ var addedWSS =
+ if (!it.startsWith("wss://") && !it.startsWith("ws://")) "wss://$it" else it
+ if (addedWSS.endsWith("/")) addedWSS = addedWSS.dropLast(1)
+
+ addedWSS
+ }
+
val unverifiedPrivKey = walletConnectSecret.text.ifBlank { null }
- val privKey = try {
+ val privKeyHex = try {
unverifiedPrivKey?.let { decodePublicKey(it).toHexKey() }
} catch (e: Exception) {
null
}
- account?.changeZapPaymentRequest(
- Nip47URI(
- walletConnectPubkey.text,
- walletConnectRelay.text.ifBlank { null },
- privKey
+ if (pubkeyHex != null) {
+ account?.changeZapPaymentRequest(
+ Nip47URI(
+ pubkeyHex,
+ relayUrl,
+ privKeyHex
+ )
)
- )
+ } else {
+ account?.changeZapPaymentRequest(null)
+ }
} else {
account?.changeZapPaymentRequest(null)
}
@@ -147,17 +166,40 @@ class UpdateZapAmountViewModel : ViewModel() {
walletConnectSecret.text != (account?.zapPaymentRequest?.secret ?: "")
)
}
+
+ fun updateNIP47(uri: String) {
+ val contact = Nip47.parse(uri)
+ if (contact != null) {
+ walletConnectPubkey =
+ TextFieldValue(contact.pubKeyHex)
+ walletConnectRelay =
+ TextFieldValue(contact.relayUri ?: "")
+ walletConnectSecret =
+ TextFieldValue(contact.secret ?: "")
+ }
+ }
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
-fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account) {
+fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account, nip47uri: String? = null) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val postViewModel: UpdateZapAmountViewModel = viewModel()
+ val uri = LocalUriHandler.current
LaunchedEffect(account) {
postViewModel.load(account)
+ if (nip47uri != null) {
+ try {
+ postViewModel.updateNIP47(nip47uri)
+ } catch (e: IllegalArgumentException) {
+ scope.launch {
+ Toast.makeText(context, e.message, Toast.LENGTH_SHORT)
+ .show()
+ }
+ }
+ }
}
Dialog(
@@ -281,6 +323,18 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account) {
stringResource(id = R.string.wallet_connect_service),
Modifier.weight(1f)
)
+
+ IconButton(onClick = {
+ runCatching { uri.openUri("https://nwc.getalby.com/apps/new?c=Amethyst") }
+ }) {
+ Icon(
+ painter = painterResource(R.drawable.alby),
+ null,
+ modifier = Modifier.size(24.dp),
+ tint = Color.Unspecified
+ )
+ }
+
IconButton(onClick = {
qrScanning = true
}) {
@@ -312,15 +366,7 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account) {
qrScanning = false
if (!it.isNullOrEmpty()) {
try {
- val contact = Nip47.parse(it)
- if (contact != null) {
- postViewModel.walletConnectPubkey =
- TextFieldValue(contact.pubKeyHex)
- postViewModel.walletConnectRelay =
- TextFieldValue(contact.relayUri ?: "")
- postViewModel.walletConnectSecret =
- TextFieldValue(contact.secret ?: "")
- }
+ postViewModel.updateNIP47(it)
} catch (e: IllegalArgumentException) {
scope.launch {
Toast.makeText(context, e.message, Toast.LENGTH_SHORT)
@@ -370,7 +416,7 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account) {
onValueChange = { postViewModel.walletConnectRelay = it },
placeholder = {
Text(
- text = "relay.server.com",
+ text = "wss://relay.server.com",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
maxLines = 1
)
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt
new file mode 100644
index 000000000..9cbb35b50
--- /dev/null
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt
@@ -0,0 +1,193 @@
+package com.vitorpamplona.amethyst.ui.note
+
+import android.widget.Toast
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.*
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.rememberCoroutineScope
+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.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.KeyboardCapitalization
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import androidx.compose.ui.window.DialogProperties
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.vitorpamplona.amethyst.R
+import com.vitorpamplona.amethyst.model.Account
+import com.vitorpamplona.amethyst.model.Note
+import com.vitorpamplona.amethyst.ui.actions.CloseButton
+import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+class ZapOptionstViewModel : ViewModel() {
+ private var account: Account? = null
+
+ var customAmount by mutableStateOf(TextFieldValue("1000"))
+ var customMessage by mutableStateOf(TextFieldValue(""))
+
+ fun load(account: Account) {
+ this.account = account
+ }
+
+ fun canSend(): Boolean {
+ return value() != null
+ }
+
+ fun value(): Long? {
+ return try {
+ customAmount.text.trim().toLongOrNull()
+ } catch (e: Exception) {
+ null
+ }
+ }
+
+ fun cancel() {
+ }
+}
+
+@Composable
+fun ZapCustomDialog(onClose: () -> Unit, account: Account, accountViewModel: AccountViewModel, baseNote: Note) {
+ val context = LocalContext.current
+ val scope = rememberCoroutineScope()
+ val postViewModel: ZapOptionstViewModel = viewModel()
+
+ LaunchedEffect(account) {
+ postViewModel.load(account)
+ }
+
+ Dialog(
+ onDismissRequest = { onClose() },
+ properties = DialogProperties(
+ dismissOnClickOutside = false,
+ usePlatformDefaultWidth = false
+ )
+ ) {
+ Surface() {
+ Column(modifier = Modifier.padding(10.dp)) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ CloseButton(onCancel = {
+ postViewModel.cancel()
+ onClose()
+ })
+
+ ZapButton(
+ isActive = postViewModel.canSend()
+ ) {
+ scope.launch(Dispatchers.IO) {
+ accountViewModel.zap(
+ baseNote,
+ postViewModel.value()!! * 1000L,
+ postViewModel.customMessage.text,
+ context,
+ onError = {
+ scope.launch {
+ Toast
+ .makeText(context, it, Toast.LENGTH_SHORT).show()
+ }
+ },
+ onProgress = {
+ scope.launch(Dispatchers.Main) {
+ }
+ }
+ )
+ }
+ onClose()
+ }
+ }
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 5.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ OutlinedTextField(
+ // stringResource(R.string.new_amount_in_sats
+ label = { Text(text = stringResource(id = R.string.amount_in_sats)) },
+ value = postViewModel.customAmount,
+ onValueChange = {
+ postViewModel.customAmount = it
+ },
+ keyboardOptions = KeyboardOptions.Default.copy(
+ capitalization = KeyboardCapitalization.None,
+ keyboardType = KeyboardType.Number
+ ),
+ placeholder = {
+ Text(
+ text = "100, 1000, 5000",
+ color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
+ )
+ },
+ singleLine = true,
+ modifier = Modifier
+ .padding(end = 10.dp)
+ .weight(1f)
+ )
+ }
+ Spacer(modifier = Modifier.height(5.dp))
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 5.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ OutlinedTextField(
+ // stringResource(R.string.new_amount_in_sats
+ label = { Text(text = stringResource(id = R.string.custom_zaps_add_a_message)) },
+ value = postViewModel.customMessage,
+ onValueChange = {
+ postViewModel.customMessage = it
+ },
+ keyboardOptions = KeyboardOptions.Default.copy(
+ capitalization = KeyboardCapitalization.None,
+ keyboardType = KeyboardType.Text
+ ),
+ placeholder = {
+ Text(
+ text = stringResource(id = R.string.custom_zaps_add_a_message_example),
+ color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
+ )
+ },
+ singleLine = true,
+ modifier = Modifier
+ .padding(end = 10.dp)
+ .weight(1f)
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun ZapButton(isActive: Boolean, onPost: () -> Unit) {
+ Button(
+ onClick = { onPost() },
+ shape = RoundedCornerShape(20.dp),
+ colors = ButtonDefaults
+ .buttonColors(
+ backgroundColor = if (isActive) MaterialTheme.colors.primary else Color.Gray
+ )
+ ) {
+ Text(text = "⚡Zap ", color = Color.White)
+ }
+}
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt
index 0744d4b1d..ed5e46c62 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt
@@ -13,6 +13,7 @@ import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -33,7 +34,14 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@OptIn(ExperimentalMaterialApi::class)
@Composable
-fun CardFeedView(viewModel: CardFeedViewModel, accountViewModel: AccountViewModel, navController: NavController, routeForLastRead: String) {
+fun CardFeedView(
+ viewModel: CardFeedViewModel,
+ accountViewModel: AccountViewModel,
+ navController: NavController,
+ routeForLastRead: String,
+ scrollStateKey: String? = null,
+ scrollToTop: Boolean = false
+) {
val feedState by viewModel.feedContent.collectAsState()
var refreshing by remember { mutableStateOf(false) }
@@ -57,10 +65,12 @@ fun CardFeedView(viewModel: CardFeedViewModel, accountViewModel: AccountViewMode
is CardFeedState.Loaded -> {
refreshing = false
FeedLoaded(
- state,
- accountViewModel,
- navController,
- routeForLastRead
+ state = state,
+ accountViewModel = accountViewModel,
+ navController = navController,
+ routeForLastRead = routeForLastRead,
+ scrollStateKey = scrollStateKey,
+ scrollToTop = scrollToTop
)
}
CardFeedState.Loading -> {
@@ -79,9 +89,21 @@ private fun FeedLoaded(
state: CardFeedState.Loaded,
accountViewModel: AccountViewModel,
navController: NavController,
- routeForLastRead: String
+ routeForLastRead: String,
+ scrollStateKey: String?,
+ scrollToTop: Boolean = false
) {
- val listState = rememberLazyListState()
+ val listState = if (scrollStateKey != null) {
+ rememberForeverLazyListState(scrollStateKey)
+ } else {
+ rememberLazyListState()
+ }
+
+ if (scrollToTop) {
+ LaunchedEffect(Unit) {
+ listState.scrollToItem(index = 0)
+ }
+ }
LazyColumn(
contentPadding = PaddingValues(
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt
index 34337677e..4e547522d 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.screen
import android.util.Log
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
+import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.LocalCacheState
import com.vitorpamplona.amethyst.model.Note
@@ -32,6 +33,7 @@ open class CardFeedViewModel(val dataSource: FeedFilter) : ViewModel() {
private val _feedContent = MutableStateFlow(CardFeedState.Loading)
val feedContent = _feedContent.asStateFlow()
+ private var lastAccount: Account? = null
private var lastNotes: List? = null
private fun refresh() {
@@ -45,18 +47,21 @@ open class CardFeedViewModel(val dataSource: FeedFilter) : ViewModel() {
private fun refreshSuspended() {
val notes = dataSource.loadTop()
- val lastNotesCopy = lastNotes
+ val thisAccount = (dataSource as? NotificationFeedFilter)?.account
+ val lastNotesCopy = if (thisAccount == lastAccount) lastNotes else null
val oldNotesState = feedContent.value
if (lastNotesCopy != null && oldNotesState is CardFeedState.Loaded) {
val newCards = convertToCard(notes.minus(lastNotesCopy))
if (newCards.isNotEmpty()) {
lastNotes = notes
+ lastAccount = (dataSource as? NotificationFeedFilter)?.account
updateFeed((oldNotesState.feed.value + newCards).distinctBy { it.id() }.sortedBy { it.createdAt() }.reversed())
}
} else {
val cards = convertToCard(notes)
lastNotes = notes
+ lastAccount = (dataSource as? NotificationFeedFilter)?.account
updateFeed(cards)
}
}
@@ -100,14 +105,21 @@ open class CardFeedViewModel(val dataSource: FeedFilter) : ViewModel() {
}
val allBaseNotes = zapsPerEvent.keys + boostsPerEvent.keys + reactionsPerEvent.keys
- val multiCards = allBaseNotes.map {
- MultiSetCard(
- it,
- boostsPerEvent.get(it) ?: emptyList(),
- reactionsPerEvent.get(it) ?: emptyList(),
- zapsPerEvent.get(it) ?: emptyMap()
- )
- }
+ val multiCards = allBaseNotes.map { baseNote ->
+ val boostsInCard = boostsPerEvent[baseNote] ?: emptyList()
+ val reactionsInCard = reactionsPerEvent[baseNote] ?: emptyList()
+ val zapsInCard = zapsPerEvent[baseNote] ?: emptyMap()
+
+ val singleList = (boostsInCard + zapsInCard.values + reactionsInCard).sortedBy { it.createdAt() }.reversed()
+ singleList.chunked(50).map { chunk ->
+ MultiSetCard(
+ baseNote,
+ boostsInCard.filter { it in chunk },
+ reactionsInCard.filter { it in chunk },
+ zapsInCard.filter { it.value in chunk }
+ )
+ }
+ }.flatten()
val textNoteCards = notes.filter { it.event !is ReactionEvent && it.event !is RepostEvent && it.event !is LnZapEvent }.map {
if (it.event is PrivateDmEvent) {
@@ -160,7 +172,13 @@ open class CardFeedViewModel(val dataSource: FeedFilter) : ViewModel() {
LocalCache.live.observeForever(cacheListener)
}
+ fun clear() {
+ lastAccount = null
+ lastNotes = null
+ }
+
override fun onCleared() {
+ clear()
LocalCache.live.removeObserver(cacheListener)
super.onCleared()
}
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LazyListState.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LazyListState.kt
index 0a4a2d5cf..ebf122ee5 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LazyListState.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LazyListState.kt
@@ -11,6 +11,7 @@ private data class ScrollState(val index: Int, val scrollOffset: Int)
object ScrollStateKeys {
const val GLOBAL_SCREEN = "Global"
+ const val NOTIFICATION_SCREEN = "Notifications"
val HOME_FOLLOWS = Route.Home.base + "Follows"
val HOME_REPLIES = Route.Home.base + "FollowsReplies"
}
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt
index 73bdf39b5..f1c38eaaa 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt
@@ -61,6 +61,7 @@ import com.vitorpamplona.amethyst.ui.note.*
import com.vitorpamplona.amethyst.ui.note.BadgeDisplay
import com.vitorpamplona.amethyst.ui.note.BlankNote
import com.vitorpamplona.amethyst.ui.note.DisplayFollowingHashtagsInPost
+import com.vitorpamplona.amethyst.ui.note.DisplayPoW
import com.vitorpamplona.amethyst.ui.note.DisplayReward
import com.vitorpamplona.amethyst.ui.note.DisplayUncitedHashtags
import com.vitorpamplona.amethyst.ui.note.HiddenNote
@@ -286,6 +287,11 @@ fun NoteMaster(
if (baseReward != null) {
DisplayReward(baseReward, baseNote, account, navController)
}
+
+ val pow = noteEvent.getPoWRank()
+ if (pow > 20) {
+ DisplayPoW(pow)
+ }
}
}
}
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt
index a82deb3bb..73330c690 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt
@@ -60,7 +60,7 @@ class AccountViewModel(private val account: Account) : ViewModel() {
return
}
- val zapRequest = account.createZapRequestFor(note, pollOption)
+ val zapRequest = account.createZapRequestFor(note, pollOption, message)
onProgress(0.10f)
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt
index 15676ba3c..c191de3aa 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt
@@ -11,7 +11,11 @@ import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
@@ -28,6 +32,7 @@ import com.vitorpamplona.amethyst.service.NostrHomeDataSource
import com.vitorpamplona.amethyst.ui.dal.HomeConversationsFeedFilter
import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter
import com.vitorpamplona.amethyst.ui.navigation.Route
+import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountDialog
import com.vitorpamplona.amethyst.ui.screen.FeedView
import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel
@@ -42,10 +47,12 @@ fun HomeScreen(
accountViewModel: AccountViewModel,
navController: NavController,
pagerState: PagerState,
- scrollToTop: Boolean = false
+ scrollToTop: Boolean = false,
+ nip47: String? = null
) {
val coroutineScope = rememberCoroutineScope()
val account = accountViewModel.accountLiveData.value?.account ?: return
+ var wantsToAddNip47 by remember { mutableStateOf(nip47) }
LaunchedEffect(accountViewModel) {
HomeNewThreadFeedFilter.account = account
@@ -55,6 +62,10 @@ fun HomeScreen(
repliesFeedViewModel.invalidateData()
}
+ if (wantsToAddNip47 != null) {
+ UpdateZapAmountDialog({ wantsToAddNip47 = null }, account = account, wantsToAddNip47)
+ }
+
val lifeCycleOwner = LocalLifecycleOwner.current
DisposableEffect(accountViewModel) {
val observer = LifecycleEventObserver { _, event ->
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt
index 5411488c9..96149588d 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt
@@ -18,12 +18,22 @@ import com.vitorpamplona.amethyst.ui.dal.NotificationFeedFilter
import com.vitorpamplona.amethyst.ui.navigation.Route
import com.vitorpamplona.amethyst.ui.screen.CardFeedView
import com.vitorpamplona.amethyst.ui.screen.NotificationViewModel
+import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
@Composable
-fun NotificationScreen(notifFeedViewModel: NotificationViewModel, accountViewModel: AccountViewModel, navController: NavController) {
+fun NotificationScreen(
+ notifFeedViewModel: NotificationViewModel,
+ accountViewModel: AccountViewModel,
+ navController: NavController,
+ scrollToTop: Boolean = false
+) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
+ if (scrollToTop) {
+ notifFeedViewModel.clear()
+ }
+
LaunchedEffect(accountViewModel) {
NotificationFeedFilter.account = account
notifFeedViewModel.invalidateData()
@@ -48,7 +58,14 @@ fun NotificationScreen(notifFeedViewModel: NotificationViewModel, accountViewMod
Column(
modifier = Modifier.padding(vertical = 0.dp)
) {
- CardFeedView(notifFeedViewModel, accountViewModel = accountViewModel, navController, Route.Notification.route)
+ CardFeedView(
+ viewModel = notifFeedViewModel,
+ accountViewModel = accountViewModel,
+ navController = navController,
+ routeForLastRead = Route.Notification.base,
+ scrollStateKey = ScrollStateKeys.NOTIFICATION_SCREEN,
+ scrollToTop = scrollToTop
+ )
}
}
}
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt
index 82d515f7e..88159e4d5 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt
@@ -430,12 +430,13 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account, accountViewMode
fontSize = 25.sp
)
}
-
+ }
+ Row(verticalAlignment = Alignment.CenterVertically) {
user.bestUsername()?.let {
Text(
"@$it",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
- modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = 5.dp)
+ modifier = Modifier.padding(top = 1.dp, bottom = 1.dp)
)
}
}
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt
index 319cd8383..5337064d8 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt
@@ -140,6 +140,9 @@ private fun SearchBar(accountViewModel: AccountViewModel, navController: NavCont
val onlineSearch = NostrSearchEventOrUserDataSource
+ val dbState = LocalCache.live.observeAsState()
+ val db = dbState.value ?: return
+
val isTrailingIconVisible by remember {
derivedStateOf {
searchValue.isNotBlank()
@@ -151,6 +154,18 @@ private fun SearchBar(accountViewModel: AccountViewModel, navController: NavCont
CoroutineChannel(CoroutineChannel.CONFLATED)
}
+ LaunchedEffect(db) {
+ if (searchValue.length > 1) {
+ withContext(Dispatchers.IO) {
+ searchResults.value = LocalCache.findUsersStartingWith(searchValue)
+ searchResultsNotes.value =
+ LocalCache.findNotesStartingWith(searchValue).sortedBy { it.createdAt() }
+ .reversed()
+ searchResultsChannels.value = LocalCache.findChannelsStartingWith(searchValue)
+ }
+ }
+ }
+
LaunchedEffect(Unit) {
// Wait for text changes to stop for 300 ms before firing off search.
withContext(Dispatchers.IO) {
@@ -161,7 +176,7 @@ private fun SearchBar(accountViewModel: AccountViewModel, navController: NavCont
.collectLatest {
hashtagResults.value = findHashtags(it)
- if (it.removePrefix("npub").removePrefix("note").length >= 4) {
+ if (it.length >= 4) {
onlineSearch.search(it.trim())
}
diff --git a/app/src/main/res/drawable/alby.xml b/app/src/main/res/drawable/alby.xml
new file mode 100644
index 000000000..cb036c35e
--- /dev/null
+++ b/app/src/main/res/drawable/alby.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
index 6b7b6b0b0..81296d243 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -22,6 +22,7 @@
A felhasználó PubKulcs másolása
A bejegyzés azonosítójának másolása
Közvetít
+ Törlés Kérése
Spam / Csalás bejelentés
Megszemélyesítés bejelentés
@@ -72,6 +73,7 @@
Nem sikerült feltölteni a képet
Csomópont Címe
Hozzászólások
+ Bájtok
Hibák
Hírfolyamod
Privát Üzenetek listája
@@ -119,6 +121,7 @@
Közvetlen üzenet küldése
Szerkeszti a felhasználó metaadatait
Követés
+ Visszakövetem
Tiltás feloldása
Felhasználói azonosító másolása
Felhasználó feloldása
@@ -172,6 +175,7 @@
Az összes ismert megjelölése olvasottként
Az összes új megjelölése olvasottként
Összes megjelölése olvasottként
+ Biztonsági Kulcsok
## Kulcs- és biztonsági mentési tippek
\n\nFiókját titkos kulcs védi. A kulcs egy hosszú véletlenszerű karakterlánc, amely **nsec1**-el kezdődik. Bárki, aki az Ön titkos kulcsához hozzáfér, az Ön személyazonosságának használatával bármilyen tartalmat közzétehet.
@@ -187,4 +191,81 @@
"Kitűző %1$s"
Új Jelvényt kapott
Kitüntetésben részesült
+ Bejegyzés vágólapra helyezve
+ A szerző @npub-ja vágólapra helyezve
+ A bejegyzés azonosítója (@note1) vágólapra helyezve
+ Szöveg Kijelölése
+ Github Gist w/ Proof
+ Telegram
+ Mastodon Post ID w/ Proof
+ Twitter Status w/ Proof
+ https://gist.github.com/<user>/<gist>
+ https://t.me/<proof post>
+ https://<server>/<user>/<proof post>
+ https://twitter.com/<user>/status/<proof post>
+ "<Nem sikerült a privát üzenetet visszafejteni>\n\nTéged egy privát/titkosított beszélgetésben %1$s és %2$s idéztek."
+ Új Fiók Hozzáadása
+ Fiókok
+ Fiók Kiválasztása
+ Új Fiók Hozzáadása
+ Aktív fiók
+ Tartalmaz privát kulcsot
+ Csak olvasásra, nincs privát kulcs
+ Vissza
+ Kiválaszt
+ Link megosztása
+ Megosztás
+ Szerző azonosító
+ Bejegyzés azonosító
+ Szöveg Másolása
+ Törlés
+ Kikövetés
+ Követem
+ Törlés Kérése
+ Az Amethyst kérni fogja, hogy töröljék a bejegyzésed azokról a csomópontokról, amelyekhez jelenleg csatlakozol. Nincs garancia arra, hogy a bejegyzésed ezekről a csomópontokról vagy más közvetítőkből (ahol tárolni lehet) véglegesen törlődik.
+ Tiltás
+ Törlés
+ Tiltás
+ Jelentés
+ Törlés
+ Ne mutasd újra
+ Spam vagy átverés
+ Trágárság vagy gyűlöletkeltő magatartás
+ Rosszindulatú megszemélyesítés
+ Meztelenség vagy pornográf tartalom
+ Illegális viselkedés
+ Az alkalmazásban egy felhasználó letiltása minden hozzá köthető tartalmat elrejt. Jegyzeteid továbbra is nyilvánosan megtekinthetők, beleértve a letiltott személyek számára is. A letiltott felhasználók a Biztonsági szűrők felületen jelennek meg.
+
+ Visszaélés jelentése
+ Minden közzétett jelentés nyilvánosan látható lesz.
+ Opcionálisan a jelentéséhez további kontextust addhat…
+ További kontextus
+ Ok
+ Válassz egy okot…
+ Jelentés Elküldése
+ Tiltás és Jelentés
+ Tiltás
+
+ Könyvjelzők
+ Privát Könyvjelzők
+ Publikus Könyvjelzők
+ Hozzáadás a Privát Könyvjelzőimhez
+ Hozzáadás a Publikus Könyvjelzőimhez
+ Törlés a Privát Könyvjelzőimből
+ Törlés a Publikus Könyvjelzőimből
+
+ Wallet Connect Szolgáltatás
+ Engedélyezed a Nostr Secret-et, hogy az alkalmazás elhagyása nélkül Zap-ot küldjön. Tartsa a titkot biztonságban, és ha lehetséges, privát csomópontot használjon
+ Wallet Connect Publikus Kulcs
+ Wallet Connect Csomópont
+ Wallet Connect Titok
+ Titkos Kulcs Megjelenítése
+ nsec / hex privát kulcs
+
+ Hozzájárulás összege sats-ban
+
+ "%1$s esemény keresése"
+
+ Nyilvános üzenet hozzáadása
+ Köszönöm a kemény munkát!
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index e3b99fe47..40e656b03 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -285,4 +285,7 @@
Only one vote per user is allowed on this type of poll
"Looking for Event %1$s"
+
+ Add a public message
+ Thank you for all your work!
diff --git a/build.gradle b/build.gradle
index 26862ebcf..ee67b5b0c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,11 +1,12 @@
buildscript {
ext {
- fragment_version = "1.5.5"
- lifecycle_version = '2.6.0'
- compose_ui_version = '1.4.0-rc01'
+ fragment_version = "1.5.6"
+ lifecycle_version = '2.6.1'
+ compose_ui_version = '1.5.0-alpha01'
nav_version = "2.5.3"
room_version = "2.4.3"
- accompanist_version = "0.28.0"
+ accompanist_version = '0.30.0'
+ coil_version = '2.3.0'
}
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {