merge with recent changes to main
This commit is contained in:
+7
-7
@@ -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"
|
||||
|
||||
@@ -43,6 +43,13 @@
|
||||
<data android:scheme="nostr" />
|
||||
</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
|
||||
android:name="android.app.lib_name"
|
||||
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
|
||||
|
||||
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 = "") {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+16
-16
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -24,9 +24,10 @@ class LnZapRequestEvent(
|
||||
relays: Set<String>,
|
||||
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<String>,
|
||||
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())
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -49,6 +49,9 @@ open class NewPostViewModel : ViewModel() {
|
||||
this.mentions = currentMentions.plus(replyUser)
|
||||
}
|
||||
}
|
||||
} ?: {
|
||||
replyTos = null
|
||||
mentions = null
|
||||
}
|
||||
|
||||
quote?.let {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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<String?>()
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<String> = listOf(word)) {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
// store the dialog open or close state
|
||||
var imageState by remember {
|
||||
mutableStateOf<AsyncImagePainter.State?>(null)
|
||||
}
|
||||
|
||||
if (imageExtension.matcher(word).matches()) {
|
||||
AsyncImage(
|
||||
model = word,
|
||||
@@ -63,8 +79,48 @@ fun ZoomableImageView(word: String, images: List<String> = 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<String> = 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
|
||||
) {
|
||||
|
||||
@@ -45,4 +45,8 @@ object NotificationFeedFilter : FeedFilter<Note>() {
|
||||
.toList()
|
||||
.reversed()
|
||||
}
|
||||
|
||||
fun isDifferentAccount(account: Account): Boolean {
|
||||
return this::account.isInitialized && this.account != account
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String?>(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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
)
|
||||
|
||||
|
||||
@@ -346,7 +346,7 @@ fun NoteComposeInner(
|
||||
}
|
||||
|
||||
val pow = noteEvent.getPoWRank()
|
||||
if (pow > 1) {
|
||||
if (pow > 20) {
|
||||
DisplayPoW(pow)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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.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(
|
||||
|
||||
@@ -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<Note>) : ViewModel() {
|
||||
private val _feedContent = MutableStateFlow<CardFeedState>(CardFeedState.Loading)
|
||||
val feedContent = _feedContent.asStateFlow()
|
||||
|
||||
private var lastAccount: Account? = null
|
||||
private var lastNotes: List<Note>? = null
|
||||
|
||||
private fun refresh() {
|
||||
@@ -45,18 +47,21 @@ open class CardFeedViewModel(val dataSource: FeedFilter<Note>) : 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<Note>) : 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<Note>) : ViewModel() {
|
||||
LocalCache.live.observeForever(cacheListener)
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
lastAccount = null
|
||||
lastNotes = null
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
clear()
|
||||
LocalCache.live.removeObserver(cacheListener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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<String?>(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 ->
|
||||
|
||||
+19
-2
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<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) {
|
||||
// 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())
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -22,6 +22,7 @@
|
||||
<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="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="report_spam_scam">Spam / Csalá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="relay_address">Csomópont Címe</string>
|
||||
<string name="posts">Hozzászólások</string>
|
||||
<string name="bytes">Bájtok</string>
|
||||
<string name="errors">Hibák</string>
|
||||
<string name="home_feed">Hírfolyamod</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="edits_the_user_s_metadata">Szerkeszti a felhasználó metaadatait</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="copy_user_id">Felhasználói azonosító másolá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_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="backup_keys">Biztonsági Kulcsok</string>
|
||||
<string name="account_backup_tips_md">
|
||||
## 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 @@
|
||||
<string name="badge_award_image_for">"Kitűző %1$s"</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="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/<user>/<gist></string>
|
||||
<string name="telegram_proof_url_template" translatable="false">https://t.me/<proof post></string>
|
||||
<string name="mastodon_proof_url_template" translatable="false">https://<server>/<user>/<proof post></string>
|
||||
<string name="twitter_proof_url_template" translatable="false">https://twitter.com/<user>/status/<proof post></string>
|
||||
<string name="private_conversation_notification">"<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."</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>
|
||||
|
||||
@@ -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="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>
|
||||
|
||||
+5
-4
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user