merge with recent changes to main

This commit is contained in:
toadlyBroodle
2023-03-31 10:50:37 +09:00
35 changed files with 795 additions and 104 deletions
+7 -7
View File
@@ -12,8 +12,8 @@ android {
applicationId "com.vitorpamplona.amethyst" applicationId "com.vitorpamplona.amethyst"
minSdk 26 minSdk 26
targetSdk 33 targetSdk 33
versionCode 108 versionCode 109
versionName "0.30.1" versionName "0.30.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
@@ -114,7 +114,7 @@ dependencies {
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05" implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
// Bitcoin secp256k1 bindings to Android // 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 // Nostr Base Protocol
implementation('com.github.vitorpamplona.NostrPostr:nostrpostrlib:master-SNAPSHOT') { implementation('com.github.vitorpamplona.NostrPostr:nostrpostrlib:master-SNAPSHOT') {
@@ -137,14 +137,14 @@ dependencies {
implementation 'androidx.security:security-crypto-ktx:1.1.0-alpha05' implementation 'androidx.security:security-crypto-ktx:1.1.0-alpha05'
// view videos // 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. // Load images from the web.
implementation "io.coil-kt:coil-compose:2.2.2" implementation "io.coil-kt:coil-compose:$coil_version"
// view gifs // view gifs
implementation "io.coil-kt:coil-gif:2.2.2" implementation "io.coil-kt:coil-gif:$coil_version"
// view svgs // view svgs
implementation("io.coil-kt:coil-svg:2.2.2") implementation "io.coil-kt:coil-svg:$coil_version"
// Rendering clickable text // Rendering clickable text
implementation "com.google.accompanist:accompanist-flowlayout:$accompanist_version" implementation "com.google.accompanist:accompanist-flowlayout:$accompanist_version"
+7
View File
@@ -43,6 +43,13 @@
<data android:scheme="nostr" /> <data android:scheme="nostr" />
</intent-filter> </intent-filter>
<intent-filter android:label="Amethyst">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="nostrwalletconnect" />
</intent-filter>
<meta-data <meta-data
android:name="android.app.lib_name" android:name="android.app.lib_name"
android:value="" /> android:value="" />
@@ -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 if (!isWriteable()) return null
note.event?.let { event -> note.event?.let { event ->
@@ -167,7 +167,10 @@ class Account(
loggedIn.privKey!!, loggedIn.privKey!!,
pollOption pollOption
) )
} /*note.event?.let {
return LnZapRequestEvent.create(it, userProfile().latestContactList?.relays()?.keys?.ifEmpty { null } ?: localRelays.map { it.url }.toSet(), loggedIn.privKey!!, message)
}*/
return null return null
} }
@@ -189,10 +192,10 @@ class Account(
return createZapRequestFor(user.pubkeyHex) return createZapRequestFor(user.pubkeyHex)
} }
fun createZapRequestFor(userPubKeyHex: String): LnZapRequestEvent? { fun createZapRequestFor(userPubKeyHex: String, message: String = ""): LnZapRequestEvent? {
if (!isWriteable()) return null 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 = "") { fun report(note: Note, type: ReportEvent.ReportType, content: String = "") {
@@ -1,10 +1,11 @@
package com.vitorpamplona.amethyst.ui.note package com.vitorpamplona.amethyst.ui.note
import android.net.Uri import android.net.Uri
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.decodePublicKey import com.vitorpamplona.amethyst.model.decodePublicKey
import com.vitorpamplona.amethyst.model.toHexKey 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. // Rename to the corect nip number when ready.
object Nip47 { object Nip47 {
@@ -30,25 +30,25 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SingleEventFeed") {
null null
} }
if (hexToWatch == null) {
return null
}
// downloads all the reactions to a given event. // downloads all the reactions to a given event.
return listOf( return listOfNotNull(
hexToWatch?.let {
TypedFilter( TypedFilter(
types = FeedType.values().toSet(), types = FeedType.values().toSet(),
filter = JsonFilter( filter = JsonFilter(
ids = listOfNotNull(hexToWatch) ids = listOfNotNull(hexToWatch)
) )
), )
},
hexToWatch?.let {
TypedFilter( TypedFilter(
types = FeedType.values().toSet(), types = FeedType.values().toSet(),
filter = JsonFilter( filter = JsonFilter(
kinds = listOf(MetadataEvent.kind), kinds = listOf(MetadataEvent.kind),
authors = listOfNotNull(hexToWatch) authors = listOfNotNull(hexToWatch)
) )
), )
},
TypedFilter( TypedFilter(
types = FeedType.values().toSet(), types = FeedType.values().toSet(),
filter = JsonFilter( filter = JsonFilter(
@@ -71,8 +71,6 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
return null return null
} }
val now = Date().time / 1000
return reactionsToWatch.map { return reactionsToWatch.map {
TypedFilter( TypedFilter(
types = FeedType.values().toSet(), types = FeedType.values().toSet(),
@@ -142,7 +140,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
val addresses = createAddressFilter() val addresses = createAddressFilter()
val addressReactions = createTagToAddressFilter() 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) { fun add(eventId: Note) {
@@ -24,9 +24,10 @@ class LnZapRequestEvent(
relays: Set<String>, relays: Set<String>,
privateKey: ByteArray, privateKey: ByteArray,
pollOption: Int?, pollOption: Int?,
message: String,
createdAt: Long = Date().time / 1000 createdAt: Long = Date().time / 1000
): LnZapRequestEvent { ): LnZapRequestEvent {
val content = "" val content = message
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
var tags = listOf( var tags = listOf(
listOf("e", originalNote.id()), listOf("e", originalNote.id()),
@@ -49,14 +50,19 @@ class LnZapRequestEvent(
userHex: String, userHex: String,
relays: Set<String>, relays: Set<String>,
privateKey: ByteArray, privateKey: ByteArray,
pollOption: Int?,
message: String,
createdAt: Long = Date().time / 1000 createdAt: Long = Date().time / 1000
): LnZapRequestEvent { ): LnZapRequestEvent {
val content = "" val content = message
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = listOf( var tags = listOf(
listOf("p", userHex), listOf("p", userHex),
listOf("relays") + relays 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 id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey) val sig = Utils.sign(id, privateKey)
return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
@@ -4,6 +4,7 @@ import com.vitorpamplona.amethyst.model.RelaySetupInfo
object Constants { object Constants {
val activeTypes = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS) 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 activeTypesGlobalChats = setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.PRIVATE_DMS, FeedType.GLOBAL)
val activeTypesSearch = setOf(FeedType.SEARCH) val activeTypesSearch = setOf(FeedType.SEARCH)
@@ -14,16 +15,18 @@ object Constants {
} }
val defaultRelays = arrayOf( val defaultRelays = arrayOf(
// Free relays // Free relays for DMs and Follows
RelaySetupInfo("wss://nostr.bitcoiner.social", read = true, write = true, feedTypes = activeTypes), RelaySetupInfo("wss://no.str.cr", read = true, write = true, feedTypes = activeTypes),
RelaySetupInfo("wss://relay.nostr.bg", read = true, write = true, feedTypes = activeTypes),
RelaySetupInfo("wss://relay.snort.social", 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://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), // Chats
RelaySetupInfo("wss://nostr.mom", read = true, write = true, feedTypes = activeTypes), RelaySetupInfo("wss://nostr.bitcoiner.social", read = true, write = true, feedTypes = activeTypesChats),
RelaySetupInfo("wss://no.str.cr", read = true, write = true, feedTypes = activeTypes), RelaySetupInfo("wss://relay.nostr.bg", read = true, write = true, feedTypes = activeTypesChats),
RelaySetupInfo("wss://nos.lol", read = true, write = true, feedTypes = activeTypes), 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 // Less Reliable
// NewRelayListViewModel.Relay("wss://nostr.orangepill.dev", read = true, write = true, feedTypes = activeTypes), // NewRelayListViewModel.Relay("wss://nostr.orangepill.dev", read = true, write = true, feedTypes = activeTypes),
@@ -182,7 +182,7 @@ class Relay(
val filters = Client.getSubscriptionFilters(requestId).filter { activeTypes.intersect(it.types).isNotEmpty() } val filters = Client.getSubscriptionFilters(requestId).filter { activeTypes.intersect(it.types).isNotEmpty() }
if (filters.isNotEmpty()) { if (filters.isNotEmpty()) {
val request = 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") // println("FILTERSSENT $url $request")
socket?.send(request) socket?.send(request)
eventUploadCounterInBytes += request.bytesUsedInMemory() eventUploadCounterInBytes += request.bytesUsedInMemory()
@@ -21,6 +21,8 @@ import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.ServiceManager import com.vitorpamplona.amethyst.ServiceManager
import com.vitorpamplona.amethyst.service.nip19.Nip19 import com.vitorpamplona.amethyst.service.nip19.Nip19
import com.vitorpamplona.amethyst.service.relays.Client 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.AccountScreen
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
@@ -28,6 +30,8 @@ import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
class MainActivity : FragmentActivity() { class MainActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -40,6 +44,14 @@ class MainActivity : FragmentActivity() {
Nip19.Type.EVENT -> "Event/${nip19.hex}" Nip19.Type.EVENT -> "Event/${nip19.hex}"
Nip19.Type.ADDRESS -> "Note/${nip19.hex}" Nip19.Type.ADDRESS -> "Note/${nip19.hex}"
else -> null 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 { Coil.setImageLoader {
@@ -49,6 +49,9 @@ open class NewPostViewModel : ViewModel() {
this.mentions = currentMentions.plus(replyUser) this.mentions = currentMentions.plus(replyUser)
} }
} }
} ?: {
replyTos = null
mentions = null
} }
quote?.let { quote?.let {
@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.actions package com.vitorpamplona.amethyst.ui.actions
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@@ -18,6 +19,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -30,9 +32,14 @@ import com.vitorpamplona.amethyst.model.Account
@Composable @Composable
fun NewUserMetadataView(onClose: () -> Unit, account: Account) { fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
val postViewModel: NewUserMetadataViewModel = viewModel() val postViewModel: NewUserMetadataViewModel = viewModel()
val context = LocalContext.current
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
postViewModel.load(account) postViewModel.load(account)
postViewModel.imageUploadingError.collect { error ->
Toast.makeText(context, error, Toast.LENGTH_SHORT).show()
}
} }
Dialog( Dialog(
@@ -141,6 +148,15 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) 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 singleLine = true
) )
@@ -157,6 +173,15 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) 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 singleLine = true
) )
@@ -1,13 +1,20 @@
package com.vitorpamplona.amethyst.ui.actions 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.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.ObjectNode import com.fasterxml.jackson.databind.node.ObjectNode
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.model.GitHubIdentity import com.vitorpamplona.amethyst.service.model.GitHubIdentity
import com.vitorpamplona.amethyst.service.model.MastodonIdentity import com.vitorpamplona.amethyst.service.model.MastodonIdentity
import com.vitorpamplona.amethyst.service.model.TwitterIdentity import com.vitorpamplona.amethyst.service.model.TwitterIdentity
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.StringWriter import java.io.StringWriter
@@ -30,6 +37,10 @@ class NewUserMetadataViewModel : ViewModel() {
val github = mutableStateOf("") val github = mutableStateOf("")
val mastodon = mutableStateOf("") val mastodon = mutableStateOf("")
var isUploadingImageForPicture by mutableStateOf(false)
var isUploadingImageForBanner by mutableStateOf(false)
val imageUploadingError = MutableSharedFlow<String?>()
fun load(account: Account) { fun load(account: Account) {
this.account = account this.account = account
@@ -127,4 +138,49 @@ class NewUserMetadataViewModel : ViewModel() {
github.value = "" github.value = ""
mastodon.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")
}
}
)
}
} }
@@ -130,7 +130,7 @@ fun InvoiceRequest(lud16: String, toUserPubKeyHex: String, account: Account, onC
Button( Button(
modifier = Modifier.fillMaxWidth().padding(vertical = 10.dp), modifier = Modifier.fillMaxWidth().padding(vertical = 10.dp),
onClick = { onClick = {
val zapRequest = account.createZapRequestFor(toUserPubKeyHex) val zapRequest = account.createZapRequestFor(toUserPubKeyHex, message)
LightningAddressResolver().lnAddressInvoice( LightningAddressResolver().lnAddressInvoice(
lud16, lud16,
@@ -10,8 +10,12 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape 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.MaterialTheme
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf 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.platform.LocalClipboardManager
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString 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.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import coil.compose.AsyncImage import coil.compose.AsyncImage
import coil.compose.AsyncImagePainter
import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.PagerState import com.google.accompanist.pager.PagerState
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.CloseButton import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation
import com.vitorpamplona.amethyst.ui.actions.SaveToGallery import com.vitorpamplona.amethyst.ui.actions.SaveToGallery
import net.engawapg.lib.zoomable.rememberZoomState import net.engawapg.lib.zoomable.rememberZoomState
import net.engawapg.lib.zoomable.zoomable import net.engawapg.lib.zoomable.zoomable
@@ -46,6 +57,11 @@ fun ZoomableImageView(word: String, images: List<String> = listOf(word)) {
mutableStateOf(false) mutableStateOf(false)
} }
// store the dialog open or close state
var imageState by remember {
mutableStateOf<AsyncImagePainter.State?>(null)
}
if (imageExtension.matcher(word).matches()) { if (imageExtension.matcher(word).matches()) {
AsyncImage( AsyncImage(
model = word, model = word,
@@ -63,8 +79,48 @@ fun ZoomableImageView(word: String, images: List<String> = listOf(word)) {
.combinedClickable( .combinedClickable(
onClick = { dialogOpen = true }, onClick = { dialogOpen = true },
onLongClick = { clipboardManager.setText(AnnotatedString(word)) } 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 { } else {
VideoView(word) { dialogOpen = true } VideoView(word) { dialogOpen = true }
} }
@@ -86,7 +142,9 @@ fun ZoomableImageDialog(imageUrl: String, allImages: List<String> = listOf(image
var pagerState: PagerState = remember { PagerState() } var pagerState: PagerState = remember { PagerState() }
Row( Row(
modifier = Modifier.padding(10.dp).fillMaxWidth(), modifier = Modifier
.padding(10.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@@ -45,4 +45,8 @@ object NotificationFeedFilter : FeedFilter<Note>() {
.toList() .toList()
.reversed() .reversed()
} }
fun isDifferentAccount(account: Account): Boolean {
return this::account.isInitialized && this.account != account
}
} }
@@ -1,8 +1,12 @@
package com.vitorpamplona.amethyst.ui.navigation package com.vitorpamplona.amethyst.ui.navigation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState 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.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
@@ -39,6 +43,7 @@ fun AppNavigation(
nextPage: String? = null nextPage: String? = null
) { ) {
val homePagerState = rememberPagerState() val homePagerState = rememberPagerState()
var actionableNextPage by remember { mutableStateOf<String?>(nextPage) }
// Avoids creating ViewModels for performance reasons (up to 1 second delays) // Avoids creating ViewModels for performance reasons (up to 1 second delays)
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
@@ -76,8 +81,9 @@ fun AppNavigation(
} }
Route.Home.let { route -> 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 scrollToTop = it.arguments?.getBoolean("scrollToTop") ?: false
val nip47 = it.arguments?.getString("nip47")
HomeScreen( HomeScreen(
homeFeedViewModel = homeFeedViewModel, homeFeedViewModel = homeFeedViewModel,
@@ -85,6 +91,28 @@ fun AppNavigation(
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
navController = navController, navController = navController,
pagerState = homePagerState, 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 scrollToTop = scrollToTop
) )
@@ -96,7 +124,6 @@ fun AppNavigation(
} }
composable(Route.Message.route, content = { ChatroomListScreen(accountViewModel, navController) }) 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.BlockedUsers.route, content = { HiddenUsersScreen(accountViewModel, navController) })
composable(Route.Bookmarks.route, content = { BookmarkListScreen(accountViewModel, navController) }) composable(Route.Bookmarks.route, content = { BookmarkListScreen(accountViewModel, navController) })
@@ -161,7 +188,10 @@ fun AppNavigation(
} }
} }
if (nextPage != null) { actionableNextPage?.let {
navController.navigate(nextPage) LaunchedEffect(it) {
navController.navigate(it)
}
actionableNextPage = null
} }
} }
@@ -24,9 +24,12 @@ sealed class Route(
get() = route.substringBefore("?") get() = route.substringBefore("?")
object Home : Route( object Home : Route(
route = "Home?scrollToTop={scrollToTop}", route = "Home?scrollToTop={scrollToTop}&nip47={nip47}",
icon = R.drawable.ic_home, 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) } hasNewItems = { accountViewModel, cache -> homeHasNewItems(accountViewModel, cache) }
) )
@@ -37,8 +40,9 @@ sealed class Route(
) )
object Notification : Route( object Notification : Route(
route = "Notification", route = "Notification?scrollToTop={scrollToTop}",
icon = R.drawable.ic_notifications, icon = R.drawable.ic_notifications,
arguments = listOf(navArgument("scrollToTop") { type = NavType.BoolType; defaultValue = false }),
hasNewItems = { accountViewModel, cache -> notificationHasNewItems(accountViewModel, cache) } hasNewItems = { accountViewModel, cache -> notificationHasNewItems(accountViewModel, cache) }
) )
@@ -346,7 +346,7 @@ fun NoteComposeInner(
} }
val pow = noteEvent.getPoWRank() val pow = noteEvent.getPoWRank()
if (pow > 1) { if (pow > 20) {
DisplayPoW(pow) DisplayPoW(pow)
} }
} }
@@ -303,10 +303,11 @@ fun ZapReaction(
val zapsState by baseNote.live().zaps.observeAsState() val zapsState by baseNote.live().zaps.observeAsState()
val zappedNote = zapsState?.note val zappedNote = zapsState?.note
val zapMessage = ""
var wantsToZap by remember { mutableStateOf(false) } var wantsToZap by remember { mutableStateOf(false) }
var wantsToChangeZapAmount 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 grayTint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -348,7 +349,7 @@ fun ZapReaction(
baseNote, baseNote,
account.zapAmountChoices.first() * 1000, account.zapAmountChoices.first() * 1000,
null, null,
"", zapMessage,
context, context,
onError = { onError = {
scope.launch { scope.launch {
@@ -371,6 +372,9 @@ fun ZapReaction(
}, },
onLongClick = { onLongClick = {
wantsToChangeZapAmount = true wantsToChangeZapAmount = true
},
onDoubleClick = {
wantsToSetCustomZap = true
} }
) )
) { ) {
@@ -403,6 +407,10 @@ fun ZapReaction(
UpdateZapAmountDialog({ wantsToChangeZapAmount = false }, account = account) UpdateZapAmountDialog({ wantsToChangeZapAmount = false }, account = account)
} }
if (wantsToSetCustomZap) {
ZapCustomDialog({ wantsToSetCustomZap = false }, account = account, accountViewModel, baseNote)
}
if (zappedNote?.isZappedBy(account.userProfile()) == true) { if (zappedNote?.isZappedBy(account.userProfile()) == true) {
zappingProgress = 1f zappingProgress = 1f
Icon( Icon(
@@ -531,7 +539,7 @@ fun ZapAmountChoicePopup(
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return val account = accountState?.account ?: return
val zapMessage = ""
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
Popup( Popup(
@@ -549,7 +557,7 @@ fun ZapAmountChoicePopup(
baseNote, baseNote,
amountInSats * 1000, amountInSats * 1000,
null, null,
"", zapMessage,
context, context,
onError, onError,
onProgress onProgress
@@ -574,7 +582,7 @@ fun ZapAmountChoicePopup(
baseNote, baseNote,
amountInSats * 1000, amountInSats * 1000,
null, null,
"", zapMessage,
context, context,
onError, onError,
onProgress onProgress
@@ -49,6 +49,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
@@ -114,23 +115,41 @@ class UpdateZapAmountViewModel : ViewModel() {
account?.changeZapAmounts(amountSet) account?.changeZapAmounts(amountSet)
if (walletConnectRelay.text.isNotBlank() && walletConnectPubkey.text.isNotBlank()) { 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 unverifiedPrivKey = walletConnectSecret.text.ifBlank { null }
val privKey = try { val privKeyHex = try {
unverifiedPrivKey?.let { decodePublicKey(it).toHexKey() } unverifiedPrivKey?.let { decodePublicKey(it).toHexKey() }
} catch (e: Exception) { } catch (e: Exception) {
null null
} }
if (pubkeyHex != null) {
account?.changeZapPaymentRequest( account?.changeZapPaymentRequest(
Nip47URI( Nip47URI(
walletConnectPubkey.text, pubkeyHex,
walletConnectRelay.text.ifBlank { null }, relayUrl,
privKey privKeyHex
) )
) )
} else { } else {
account?.changeZapPaymentRequest(null) account?.changeZapPaymentRequest(null)
} }
} else {
account?.changeZapPaymentRequest(null)
}
nextAmount = TextFieldValue("") nextAmount = TextFieldValue("")
} }
@@ -147,17 +166,40 @@ class UpdateZapAmountViewModel : ViewModel() {
walletConnectSecret.text != (account?.zapPaymentRequest?.secret ?: "") 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) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account) { fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account, nip47uri: String? = null) {
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val postViewModel: UpdateZapAmountViewModel = viewModel() val postViewModel: UpdateZapAmountViewModel = viewModel()
val uri = LocalUriHandler.current
LaunchedEffect(account) { LaunchedEffect(account) {
postViewModel.load(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( Dialog(
@@ -281,6 +323,18 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account) {
stringResource(id = R.string.wallet_connect_service), stringResource(id = R.string.wallet_connect_service),
Modifier.weight(1f) 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 = { IconButton(onClick = {
qrScanning = true qrScanning = true
}) { }) {
@@ -312,15 +366,7 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account) {
qrScanning = false qrScanning = false
if (!it.isNullOrEmpty()) { if (!it.isNullOrEmpty()) {
try { try {
val contact = Nip47.parse(it) postViewModel.updateNIP47(it)
if (contact != null) {
postViewModel.walletConnectPubkey =
TextFieldValue(contact.pubKeyHex)
postViewModel.walletConnectRelay =
TextFieldValue(contact.relayUri ?: "")
postViewModel.walletConnectSecret =
TextFieldValue(contact.secret ?: "")
}
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
scope.launch { scope.launch {
Toast.makeText(context, e.message, Toast.LENGTH_SHORT) Toast.makeText(context, e.message, Toast.LENGTH_SHORT)
@@ -370,7 +416,7 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account) {
onValueChange = { postViewModel.walletConnectRelay = it }, onValueChange = { postViewModel.walletConnectRelay = it },
placeholder = { placeholder = {
Text( Text(
text = "relay.server.com", text = "wss://relay.server.com",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
maxLines = 1 maxLines = 1
) )
@@ -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)
}
}
@@ -13,6 +13,7 @@ import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -33,7 +34,14 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @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() val feedState by viewModel.feedContent.collectAsState()
var refreshing by remember { mutableStateOf(false) } var refreshing by remember { mutableStateOf(false) }
@@ -57,10 +65,12 @@ fun CardFeedView(viewModel: CardFeedViewModel, accountViewModel: AccountViewMode
is CardFeedState.Loaded -> { is CardFeedState.Loaded -> {
refreshing = false refreshing = false
FeedLoaded( FeedLoaded(
state, state = state,
accountViewModel, accountViewModel = accountViewModel,
navController, navController = navController,
routeForLastRead routeForLastRead = routeForLastRead,
scrollStateKey = scrollStateKey,
scrollToTop = scrollToTop
) )
} }
CardFeedState.Loading -> { CardFeedState.Loading -> {
@@ -79,9 +89,21 @@ private fun FeedLoaded(
state: CardFeedState.Loaded, state: CardFeedState.Loaded,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
navController: NavController, 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( LazyColumn(
contentPadding = PaddingValues( contentPadding = PaddingValues(
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.screen
import android.util.Log import android.util.Log
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.LocalCacheState import com.vitorpamplona.amethyst.model.LocalCacheState
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
@@ -32,6 +33,7 @@ open class CardFeedViewModel(val dataSource: FeedFilter<Note>) : ViewModel() {
private val _feedContent = MutableStateFlow<CardFeedState>(CardFeedState.Loading) private val _feedContent = MutableStateFlow<CardFeedState>(CardFeedState.Loading)
val feedContent = _feedContent.asStateFlow() val feedContent = _feedContent.asStateFlow()
private var lastAccount: Account? = null
private var lastNotes: List<Note>? = null private var lastNotes: List<Note>? = null
private fun refresh() { private fun refresh() {
@@ -45,18 +47,21 @@ open class CardFeedViewModel(val dataSource: FeedFilter<Note>) : ViewModel() {
private fun refreshSuspended() { private fun refreshSuspended() {
val notes = dataSource.loadTop() 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 val oldNotesState = feedContent.value
if (lastNotesCopy != null && oldNotesState is CardFeedState.Loaded) { if (lastNotesCopy != null && oldNotesState is CardFeedState.Loaded) {
val newCards = convertToCard(notes.minus(lastNotesCopy)) val newCards = convertToCard(notes.minus(lastNotesCopy))
if (newCards.isNotEmpty()) { if (newCards.isNotEmpty()) {
lastNotes = notes lastNotes = notes
lastAccount = (dataSource as? NotificationFeedFilter)?.account
updateFeed((oldNotesState.feed.value + newCards).distinctBy { it.id() }.sortedBy { it.createdAt() }.reversed()) updateFeed((oldNotesState.feed.value + newCards).distinctBy { it.id() }.sortedBy { it.createdAt() }.reversed())
} }
} else { } else {
val cards = convertToCard(notes) val cards = convertToCard(notes)
lastNotes = notes lastNotes = notes
lastAccount = (dataSource as? NotificationFeedFilter)?.account
updateFeed(cards) updateFeed(cards)
} }
} }
@@ -100,14 +105,21 @@ open class CardFeedViewModel(val dataSource: FeedFilter<Note>) : ViewModel() {
} }
val allBaseNotes = zapsPerEvent.keys + boostsPerEvent.keys + reactionsPerEvent.keys val allBaseNotes = zapsPerEvent.keys + boostsPerEvent.keys + reactionsPerEvent.keys
val multiCards = allBaseNotes.map { 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( MultiSetCard(
it, baseNote,
boostsPerEvent.get(it) ?: emptyList(), boostsInCard.filter { it in chunk },
reactionsPerEvent.get(it) ?: emptyList(), reactionsInCard.filter { it in chunk },
zapsPerEvent.get(it) ?: emptyMap() zapsInCard.filter { it.value in chunk }
) )
} }
}.flatten()
val textNoteCards = notes.filter { it.event !is ReactionEvent && it.event !is RepostEvent && it.event !is LnZapEvent }.map { val textNoteCards = notes.filter { it.event !is ReactionEvent && it.event !is RepostEvent && it.event !is LnZapEvent }.map {
if (it.event is PrivateDmEvent) { if (it.event is PrivateDmEvent) {
@@ -160,7 +172,13 @@ open class CardFeedViewModel(val dataSource: FeedFilter<Note>) : ViewModel() {
LocalCache.live.observeForever(cacheListener) LocalCache.live.observeForever(cacheListener)
} }
fun clear() {
lastAccount = null
lastNotes = null
}
override fun onCleared() { override fun onCleared() {
clear()
LocalCache.live.removeObserver(cacheListener) LocalCache.live.removeObserver(cacheListener)
super.onCleared() super.onCleared()
} }
@@ -11,6 +11,7 @@ private data class ScrollState(val index: Int, val scrollOffset: Int)
object ScrollStateKeys { object ScrollStateKeys {
const val GLOBAL_SCREEN = "Global" const val GLOBAL_SCREEN = "Global"
const val NOTIFICATION_SCREEN = "Notifications"
val HOME_FOLLOWS = Route.Home.base + "Follows" val HOME_FOLLOWS = Route.Home.base + "Follows"
val HOME_REPLIES = Route.Home.base + "FollowsReplies" val HOME_REPLIES = Route.Home.base + "FollowsReplies"
} }
@@ -61,6 +61,7 @@ import com.vitorpamplona.amethyst.ui.note.*
import com.vitorpamplona.amethyst.ui.note.BadgeDisplay import com.vitorpamplona.amethyst.ui.note.BadgeDisplay
import com.vitorpamplona.amethyst.ui.note.BlankNote import com.vitorpamplona.amethyst.ui.note.BlankNote
import com.vitorpamplona.amethyst.ui.note.DisplayFollowingHashtagsInPost 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.DisplayReward
import com.vitorpamplona.amethyst.ui.note.DisplayUncitedHashtags import com.vitorpamplona.amethyst.ui.note.DisplayUncitedHashtags
import com.vitorpamplona.amethyst.ui.note.HiddenNote import com.vitorpamplona.amethyst.ui.note.HiddenNote
@@ -286,6 +287,11 @@ fun NoteMaster(
if (baseReward != null) { if (baseReward != null) {
DisplayReward(baseReward, baseNote, account, navController) DisplayReward(baseReward, baseNote, account, navController)
} }
val pow = noteEvent.getPoWRank()
if (pow > 20) {
DisplayPoW(pow)
}
} }
} }
} }
@@ -60,7 +60,7 @@ class AccountViewModel(private val account: Account) : ViewModel() {
return return
} }
val zapRequest = account.createZapRequestFor(note, pollOption) val zapRequest = account.createZapRequestFor(note, pollOption, message)
onProgress(0.10f) onProgress(0.10f)
@@ -11,7 +11,11 @@ import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect 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.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource 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.HomeConversationsFeedFilter
import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter
import com.vitorpamplona.amethyst.ui.navigation.Route 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.FeedView
import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel
@@ -42,10 +47,12 @@ fun HomeScreen(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
navController: NavController, navController: NavController,
pagerState: PagerState, pagerState: PagerState,
scrollToTop: Boolean = false scrollToTop: Boolean = false,
nip47: String? = null
) { ) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val account = accountViewModel.accountLiveData.value?.account ?: return val account = accountViewModel.accountLiveData.value?.account ?: return
var wantsToAddNip47 by remember { mutableStateOf<String?>(nip47) }
LaunchedEffect(accountViewModel) { LaunchedEffect(accountViewModel) {
HomeNewThreadFeedFilter.account = account HomeNewThreadFeedFilter.account = account
@@ -55,6 +62,10 @@ fun HomeScreen(
repliesFeedViewModel.invalidateData() repliesFeedViewModel.invalidateData()
} }
if (wantsToAddNip47 != null) {
UpdateZapAmountDialog({ wantsToAddNip47 = null }, account = account, wantsToAddNip47)
}
val lifeCycleOwner = LocalLifecycleOwner.current val lifeCycleOwner = LocalLifecycleOwner.current
DisposableEffect(accountViewModel) { DisposableEffect(accountViewModel) {
val observer = LifecycleEventObserver { _, event -> val observer = LifecycleEventObserver { _, event ->
@@ -18,12 +18,22 @@ import com.vitorpamplona.amethyst.ui.dal.NotificationFeedFilter
import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.navigation.Route
import com.vitorpamplona.amethyst.ui.screen.CardFeedView import com.vitorpamplona.amethyst.ui.screen.CardFeedView
import com.vitorpamplona.amethyst.ui.screen.NotificationViewModel import com.vitorpamplona.amethyst.ui.screen.NotificationViewModel
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
@Composable @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 accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return val account = accountState?.account ?: return
if (scrollToTop) {
notifFeedViewModel.clear()
}
LaunchedEffect(accountViewModel) { LaunchedEffect(accountViewModel) {
NotificationFeedFilter.account = account NotificationFeedFilter.account = account
notifFeedViewModel.invalidateData() notifFeedViewModel.invalidateData()
@@ -48,7 +58,14 @@ fun NotificationScreen(notifFeedViewModel: NotificationViewModel, accountViewMod
Column( Column(
modifier = Modifier.padding(vertical = 0.dp) 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
)
} }
} }
} }
@@ -430,12 +430,13 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account, accountViewMode
fontSize = 25.sp fontSize = 25.sp
) )
} }
}
Row(verticalAlignment = Alignment.CenterVertically) {
user.bestUsername()?.let { user.bestUsername()?.let {
Text( Text(
"@$it", "@$it",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), 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)
) )
} }
} }
@@ -140,6 +140,9 @@ private fun SearchBar(accountViewModel: AccountViewModel, navController: NavCont
val onlineSearch = NostrSearchEventOrUserDataSource val onlineSearch = NostrSearchEventOrUserDataSource
val dbState = LocalCache.live.observeAsState()
val db = dbState.value ?: return
val isTrailingIconVisible by remember { val isTrailingIconVisible by remember {
derivedStateOf { derivedStateOf {
searchValue.isNotBlank() searchValue.isNotBlank()
@@ -151,6 +154,18 @@ private fun SearchBar(accountViewModel: AccountViewModel, navController: NavCont
CoroutineChannel<String>(CoroutineChannel.CONFLATED) CoroutineChannel<String>(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) { LaunchedEffect(Unit) {
// Wait for text changes to stop for 300 ms before firing off search. // Wait for text changes to stop for 300 ms before firing off search.
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
@@ -161,7 +176,7 @@ private fun SearchBar(accountViewModel: AccountViewModel, navController: NavCont
.collectLatest { .collectLatest {
hashtagResults.value = findHashtags(it) hashtagResults.value = findHashtags(it)
if (it.removePrefix("npub").removePrefix("note").length >= 4) { if (it.length >= 4) {
onlineSearch.search(it.trim()) onlineSearch.search(it.trim())
} }
+58
View File
@@ -0,0 +1,58 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="489dp"
android:height="489dp"
android:viewportWidth="489"
android:viewportHeight="489">
<path
android:pathData="M89.58,181.77C96.83,182.51 103.84,181.66 110.26,179.49L110.42,179.63L110.66,179.87C83.35,210.92 64.09,251.1 56.14,295.56C50.5,327.03 67.39,357.29 95.35,370.61C140.92,392.29 191.9,404.42 245.64,404.42C299.39,404.42 349.32,392.54 394.53,371.27C422.59,358.07 439.6,327.82 434.06,296.31C426.2,251.6 406.52,211.48 379.48,179.96C345.33,140.15 379.58,179.8 379.58,179.8C379.58,179.8 391.32,182.34 397.62,181.9C421.93,180.22 441.52,160.37 442.89,136.03C444.51,106.88 420.52,82.9 391.36,84.54C367.36,85.89 347.63,105.01 345.58,128.97C344.95,136.44 346.01,143.62 348.41,150.15L348.02,150.54L347.79,150.77C318.32,129.03 283.17,116.22 245.05,116.22C206.93,116.22 171.54,129.12 141.99,151.01L141.77,150.78L141.4,150.42L140.69,149.71C143.01,143.22 144.01,136.11 143.33,128.71C141.16,104.88 121.47,85.9 97.57,84.55C68.36,82.92 44.37,106.91 46.01,136.09C47.34,159.78 65.97,179.34 89.56,181.78L89.58,181.77Z"
android:strokeWidth="19.962"
android:fillColor="#ffffff"
android:strokeColor="#ffffff"/>
<path
android:pathData="M104.48,351.42C84.72,342.01 73.22,320.89 77.07,299.33C85.11,254.32 105.57,214.8 133.95,186.08C138.28,181.69 142.81,177.57 147.49,173.7C175.19,150.87 208.78,137.51 244.99,137.51C281.2,137.51 314.55,150.78 342.19,173.45C346.89,177.3 351.41,181.43 355.76,185.81C384.43,214.69 405.06,254.58 413.04,300.02C416.84,321.6 405.24,342.72 385.41,352.05C342.98,372.01 295.59,383.17 245.6,383.17C195.6,383.17 147.22,371.79 104.45,351.42H104.48Z"
android:fillColor="#FFDF6F"
android:fillType="evenOdd"/>
<path
android:pathData="M95.07,121.28L82.11,134.24L112.95,165.08C118.32,161.99 122.85,157.57 126.06,152.26L95.07,121.28Z"
android:fillColor="#000000"/>
<path
android:pathData="M394.79,96.61C374.95,96.28 357.68,113.06 357.49,132.9C357.41,140.15 359.45,146.93 363.03,152.66L394.39,121.29L407.36,134.26L376.3,165.3C382.44,168.73 389.68,170.45 397.35,169.8C415.01,168.3 429.27,153.98 430.7,136.32C432.42,114.9 415.73,96.96 394.79,96.61ZM363.03,152.66L348.73,166.94C353.43,170.82 357.94,174.97 362.27,179.33L376.3,165.3C370.87,162.28 366.3,157.91 363.03,152.66ZM422.09,298.44C413.84,251.46 392.38,209.71 362.27,179.33L355.79,185.81C351.44,181.43 346.92,177.31 342.22,173.47L348.73,166.94C319.53,142.71 283.85,128.37 245.01,128.37C206.16,128.37 170.25,142.8 140.99,167.2L147.51,173.71C142.81,177.57 138.3,181.71 133.97,186.1L127.49,179.62C97.66,209.82 76.37,251.19 68.04,297.74C63.44,323.55 77.25,348.61 100.54,359.7C144.51,380.63 193.71,392.35 245.63,392.35C297.54,392.35 345.72,380.87 389.35,360.35C412.72,349.34 426.65,324.29 422.09,298.44ZM381.54,343.75C340.31,363.17 294.24,374.01 245.63,374.01C197.01,374.01 149.99,362.94 108.42,343.15C92.2,335.42 83,318.23 86.09,300.97C93.86,257.51 113.49,219.81 140.45,192.57L154.03,180.24C180.14,158.99 211.55,146.69 245.01,146.69C278.46,146.69 309.63,158.88 335.69,179.98L349.31,192.29C376.54,219.69 396.33,257.73 404.04,301.61C407.08,318.92 397.82,336.11 381.54,343.75ZM126.08,152.28C122.87,157.58 118.34,161.99 112.96,165.09L127.49,179.62C131.82,175.23 136.31,171.09 140.99,167.2L126.08,152.28ZM126.08,152.28C129.56,146.54 131.51,139.77 131.38,132.56C130.98,112.5 114.04,96.21 93.97,96.61C73.05,97.03 56.44,114.95 58.17,136.32C59.61,154.07 74.01,168.43 91.76,169.82C99.51,170.42 106.8,168.61 112.96,165.09L82.13,134.26L95.09,121.29L126.08,152.28Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
<path
android:pathData="M407.36,134.26L376.3,165.3C370.87,162.28 366.3,157.91 363.03,152.66L394.39,121.29L407.36,134.26Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
<path
android:pathData="M362.27,179.33L355.79,185.81C351.44,181.43 346.92,177.32 342.22,173.47L348.73,166.94C353.43,170.82 357.94,174.97 362.27,179.33Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
<path
android:pathData="M147.51,173.71C142.81,177.57 138.3,181.71 133.97,186.1L127.49,179.62C131.82,175.23 136.31,171.09 140.99,167.2L147.51,173.71Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
<path
android:pathData="M126.08,152.28C122.87,157.58 118.34,161.99 112.96,165.09L82.13,134.26L95.09,121.29L126.08,152.28Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
<path
android:pathData="M407.36,134.24L376.3,165.29C370.87,162.26 366.3,157.9 363.03,152.64L394.39,121.28L407.36,134.24Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
<path
android:pathData="M348.73,166.93L342.2,173.45C346.9,177.3 351.43,181.43 355.77,185.81L362.26,179.33C357.92,174.95 353.41,170.82 348.73,166.94V166.93Z"
android:fillColor="#000000"/>
<path
android:pathData="M140.98,167.18C136.3,171.07 131.8,175.23 127.47,179.6L133.95,186.08C138.28,181.69 142.81,177.57 147.49,173.7L140.98,167.18Z"
android:fillColor="#000000"/>
<path
android:pathData="M140.37,329.06C124.47,322.58 115.04,305.67 120.55,289.4C137.54,239.24 187.12,202.92 245.64,202.92C304.16,202.92 353.74,239.23 370.75,289.4C376.27,305.67 366.83,322.6 350.93,329.06C318.44,342.3 282.9,349.58 245.66,349.58C208.42,349.58 172.87,342.3 140.39,329.06H140.37Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
<path
android:pathData="M287.82,305.59C304.7,305.59 318.38,294.65 318.38,281.15C318.38,267.65 304.7,256.7 287.82,256.7C270.94,256.7 257.26,267.65 257.26,281.15C257.26,294.65 270.94,305.59 287.82,305.59Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M200.35,305.61C217.22,305.61 230.91,294.66 230.91,281.16C230.91,267.66 217.22,256.72 200.35,256.72C183.47,256.72 169.78,267.66 169.78,281.16C169.78,294.66 183.47,305.61 200.35,305.61Z"
android:fillColor="#ffffff"/>
</vector>
+81
View File
@@ -22,6 +22,7 @@
<string name="copy_user_pubkey">A felhasználó PubKulcs másolása</string> <string name="copy_user_pubkey">A felhasználó PubKulcs másolása</string>
<string name="copy_note_id">A bejegyzés azonosítójának másolása</string> <string name="copy_note_id">A bejegyzés azonosítójának másolása</string>
<string name="broadcast">Közvetít</string> <string name="broadcast">Közvetít</string>
<string name="request_deletion">Törlés Kérése</string>
<string name="block_hide_user"><![CDATA[Tiltás és Felhasználó elrejtése]]></string> <string name="block_hide_user"><![CDATA[Tiltás és Felhasználó elrejtése]]></string>
<string name="report_spam_scam">Spam / Csalás bejelentés</string> <string name="report_spam_scam">Spam / Csalás bejelentés</string>
<string name="report_impersonation">Megszemélyesítés bejelentés</string> <string name="report_impersonation">Megszemélyesítés bejelentés</string>
@@ -72,6 +73,7 @@
<string name="failed_to_upload_the_image">Nem sikerült feltölteni a képet</string> <string name="failed_to_upload_the_image">Nem sikerült feltölteni a képet</string>
<string name="relay_address">Csomópont Címe</string> <string name="relay_address">Csomópont Címe</string>
<string name="posts">Hozzászólások</string> <string name="posts">Hozzászólások</string>
<string name="bytes">Bájtok</string>
<string name="errors">Hibák</string> <string name="errors">Hibák</string>
<string name="home_feed">Hírfolyamod</string> <string name="home_feed">Hírfolyamod</string>
<string name="private_message_feed">Privát Üzenetek listája</string> <string name="private_message_feed">Privát Üzenetek listája</string>
@@ -119,6 +121,7 @@
<string name="send_a_direct_message">Közvetlen üzenet küldése</string> <string name="send_a_direct_message">Közvetlen üzenet küldése</string>
<string name="edits_the_user_s_metadata">Szerkeszti a felhasználó metaadatait</string> <string name="edits_the_user_s_metadata">Szerkeszti a felhasználó metaadatait</string>
<string name="follow">Követés</string> <string name="follow">Követés</string>
<string name="follow_back">Visszakövetem</string>
<string name="unblock">Tiltás feloldása</string> <string name="unblock">Tiltás feloldása</string>
<string name="copy_user_id">Felhasználói azonosító másolása</string> <string name="copy_user_id">Felhasználói azonosító másolása</string>
<string name="unblock_user">Felhasználó feloldása</string> <string name="unblock_user">Felhasználó feloldása</string>
@@ -172,6 +175,7 @@
<string name="mark_all_known_as_read">Az összes ismert megjelölése olvasottként</string> <string name="mark_all_known_as_read">Az összes ismert megjelölése olvasottként</string>
<string name="mark_all_new_as_read">Az összes új megjelölése olvasottként</string> <string name="mark_all_new_as_read">Az összes új megjelölése olvasottként</string>
<string name="mark_all_as_read">Összes megjelölése olvasottként</string> <string name="mark_all_as_read">Összes megjelölése olvasottként</string>
<string name="backup_keys">Biztonsági Kulcsok</string>
<string name="account_backup_tips_md"> <string name="account_backup_tips_md">
## Kulcs- és biztonsági mentési tippek ## 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. \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 @@
<string name="badge_award_image_for">"Kitűző %1$s"</string> <string name="badge_award_image_for">"Kitűző %1$s"</string>
<string name="new_badge_award_notif">Új Jelvényt kapott</string> <string name="new_badge_award_notif">Új Jelvényt kapott</string>
<string name="award_granted_to">Kitüntetésben részesült</string> <string name="award_granted_to">Kitüntetésben részesült</string>
<string name="copied_note_text_to_clipboard">Bejegyzés vágólapra helyezve</string>
<string name="copied_user_id_to_clipboard">A szerző @npub-ja vágólapra helyezve</string>
<string name="copied_note_id_to_clipboard">A bejegyzés azonosítója (@note1) vágólapra helyezve</string>
<string name="select_text_dialog_top">Szöveg Kijelölése</string>
<string name="github" translatable="false">Github Gist w/ Proof</string>
<string name="telegram" translatable="false">Telegram</string>
<string name="mastodon" translatable="false">Mastodon Post ID w/ Proof</string>
<string name="twitter" translatable="false">Twitter Status w/ Proof</string>
<string name="github_proof_url_template" translatable="false">https://gist.github.com/&lt;user&gt;/&lt;gist&gt;</string>
<string name="telegram_proof_url_template" translatable="false">https://t.me/&lt;proof post&gt;</string>
<string name="mastodon_proof_url_template" translatable="false">https://&lt;server&gt;/&lt;user&gt;/&lt;proof post&gt;</string>
<string name="twitter_proof_url_template" translatable="false">https://twitter.com/&lt;user&gt;/status/&lt;proof post&gt;</string>
<string name="private_conversation_notification">"&lt;Nem sikerült a privát üzenetet visszafejteni&gt;\n\nTéged egy privát/titkosított beszélgetésben %1$s és %2$s idéztek."</string>
<string name="account_switch_add_account_dialog_title">Új Fiók Hozzáadása</string>
<string name="drawer_accounts">Fiókok</string>
<string name="account_switch_select_account">Fiók Kiválasztása</string>
<string name="account_switch_add_account_btn">Új Fiók Hozzáadása</string>
<string name="account_switch_active_account">Aktív fiók</string>
<string name="account_switch_has_private_key">Tartalmaz privát kulcsot</string>
<string name="account_switch_pubkey_only">Csak olvasásra, nincs privát kulcs</string>
<string name="back">Vissza</string>
<string name="quick_action_select">Kiválaszt</string>
<string name="quick_action_share_browser_link">Link megosztása</string>
<string name="quick_action_share">Megosztás</string>
<string name="quick_action_copy_user_id">Szerző azonosító</string>
<string name="quick_action_copy_note_id">Bejegyzés azonosító</string>
<string name="quick_action_copy_text">Szöveg Másolása</string>
<string name="quick_action_delete">Törlés</string>
<string name="quick_action_unfollow">Kikövetés</string>
<string name="quick_action_follow">Követem</string>
<string name="quick_action_request_deletion_alert_title">Törlés Kérése</string>
<string name="quick_action_request_deletion_alert_body">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.</string>
<string name="quick_action_block_dialog_btn">Tiltás</string>
<string name="quick_action_delete_dialog_btn">Törlés</string>
<string name="quick_action_block">Tiltás</string>
<string name="quick_action_report">Jelentés</string>
<string name="quick_action_delete_button">Törlés</string>
<string name="quick_action_dont_show_again_button">Ne mutasd újra</string>
<string name="report_dialog_spam">Spam vagy átverés</string>
<string name="report_dialog_profanity">Trágárság vagy gyűlöletkeltő magatartás</string>
<string name="report_dialog_impersonation">Rosszindulatú megszemélyesítés</string>
<string name="report_dialog_nudity">Meztelenség vagy pornográf tartalom</string>
<string name="report_dialog_illegal">Illegális viselkedés</string>
<string name="report_dialog_blocking_a_user">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.</string>
<string name="report_dialog_block_hide_user_btn"><![CDATA[Felhasználó Tiltása és Elrejtése]]></string>
<string name="report_dialog_report_btn">Visszaélés jelentése</string>
<string name="report_dialog_reminder_public">Minden közzétett jelentés nyilvánosan látható lesz.</string>
<string name="report_dialog_additional_reason_placeholder">Opcionálisan a jelentéséhez további kontextust addhat…</string>
<string name="report_dialog_additional_reason_label">További kontextus</string>
<string name="report_dialog_select_reason_label">Ok</string>
<string name="report_dialog_select_reason_placeholder">Válassz egy okot…</string>
<string name="report_dialog_post_report_btn">Jelentés Elküldése</string>
<string name="report_dialog_title">Tiltás és Jelentés</string>
<string name="block_only">Tiltás</string>
<string name="bookmarks">Könyvjelzők</string>
<string name="private_bookmarks">Privát Könyvjelzők</string>
<string name="public_bookmarks">Publikus Könyvjelzők</string>
<string name="add_to_private_bookmarks">Hozzáadás a Privát Könyvjelzőimhez</string>
<string name="add_to_public_bookmarks">Hozzáadás a Publikus Könyvjelzőimhez</string>
<string name="remove_from_private_bookmarks">Törlés a Privát Könyvjelzőimből</string>
<string name="remove_from_public_bookmarks">Törlés a Publikus Könyvjelzőimből</string>
<string name="wallet_connect_service">Wallet Connect Szolgáltatás</string>
<string name="wallet_connect_service_explainer">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</string>
<string name="wallet_connect_service_pubkey">Wallet Connect Publikus Kulcs</string>
<string name="wallet_connect_service_relay">Wallet Connect Csomópont</string>
<string name="wallet_connect_service_secret">Wallet Connect Titok</string>
<string name="wallet_connect_service_show_secret">Titkos Kulcs Megjelenítése</string>
<string name="wallet_connect_service_secret_placeholder">nsec / hex privát kulcs</string>
<string name="pledge_amount_in_sats">Hozzájárulás összege sats-ban</string>
<string name="looking_for_event">"%1$s esemény keresése"</string>
<string name="custom_zaps_add_a_message">Nyilvános üzenet hozzáadása</string>
<string name="custom_zaps_add_a_message_example">Köszönöm a kemény munkát!</string>
</resources> </resources>
+3
View File
@@ -285,4 +285,7 @@
<string name="one_vote_per_user_on_atomic_votes">Only one vote per user is allowed on this type of poll</string> <string name="one_vote_per_user_on_atomic_votes">Only one vote per user is allowed on this type of poll</string>
<string name="looking_for_event">"Looking for Event %1$s"</string> <string name="looking_for_event">"Looking for Event %1$s"</string>
<string name="custom_zaps_add_a_message">Add a public message</string>
<string name="custom_zaps_add_a_message_example">Thank you for all your work!</string>
</resources> </resources>
+5 -4
View File
@@ -1,11 +1,12 @@
buildscript { buildscript {
ext { ext {
fragment_version = "1.5.5" fragment_version = "1.5.6"
lifecycle_version = '2.6.0' lifecycle_version = '2.6.1'
compose_ui_version = '1.4.0-rc01' compose_ui_version = '1.5.0-alpha01'
nav_version = "2.5.3" nav_version = "2.5.3"
room_version = "2.4.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. }// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { plugins {