Merge pull request #2 from believethehype/gallery2

Gallery2
This commit is contained in:
believethehype
2024-06-28 23:57:32 +02:00
committed by GitHub
29 changed files with 850 additions and 169 deletions
+12 -12
View File
@@ -99,7 +99,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: amethyst/build/outputs/apk/play/release/app-play-universal-release-unsigned-signed.apk asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-universal-release-unsigned-signed.apk
asset_name: amethyst-googleplay-universal-${{ github.ref_name }}.apk asset_name: amethyst-googleplay-universal-${{ github.ref_name }}.apk
asset_content_type: application/zip asset_content_type: application/zip
@@ -110,7 +110,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: amethyst/build/outputs/apk/play/release/app-play-x86-release-unsigned-signed.apk asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-x86-release-unsigned-signed.apk
asset_name: amethyst-googleplay-x86-${{ github.ref_name }}.apk asset_name: amethyst-googleplay-x86-${{ github.ref_name }}.apk
asset_content_type: application/zip asset_content_type: application/zip
@@ -121,7 +121,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: amethyst/build/outputs/apk/play/release/app-play-x86_64-release-unsigned-signed.apk asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-x86_64-release-unsigned-signed.apk
asset_name: amethyst-googleplay-x86_64-${{ github.ref_name }}.apk asset_name: amethyst-googleplay-x86_64-${{ github.ref_name }}.apk
asset_content_type: application/zip asset_content_type: application/zip
@@ -132,7 +132,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: amethyst/build/outputs/apk/play/release/app-play-arm64-v8a-release-unsigned-signed.apk asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-arm64-v8a-release-unsigned-signed.apk
asset_name: amethyst-googleplay-arm64-v8a-${{ github.ref_name }}.apk asset_name: amethyst-googleplay-arm64-v8a-${{ github.ref_name }}.apk
asset_content_type: application/zip asset_content_type: application/zip
@@ -143,7 +143,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: amethyst/build/outputs/apk/play/release/app-play-armeabi-v7a-release-unsigned-signed.apk asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-armeabi-v7a-release-unsigned-signed.apk
asset_name: amethyst-googleplay-armeabi-v7a-${{ github.ref_name }}.apk asset_name: amethyst-googleplay-armeabi-v7a-${{ github.ref_name }}.apk
asset_content_type: application/zip asset_content_type: application/zip
@@ -155,7 +155,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: amethyst/build/outputs/apk/fdroid/release/app-fdroid-universal-release-unsigned-signed.apk asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-universal-release-unsigned-signed.apk
asset_name: amethyst-fdroid-universal-${{ github.ref_name }}.apk asset_name: amethyst-fdroid-universal-${{ github.ref_name }}.apk
asset_content_type: application/zip asset_content_type: application/zip
@@ -166,7 +166,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: amethyst/build/outputs/apk/fdroid/release/app-fdroid-x86-release-unsigned-signed.apk asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-x86-release-unsigned-signed.apk
asset_name: amethyst-fdroid-x86-${{ github.ref_name }}.apk asset_name: amethyst-fdroid-x86-${{ github.ref_name }}.apk
asset_content_type: application/zip asset_content_type: application/zip
@@ -177,7 +177,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: amethyst/build/outputs/apk/fdroid/release/app-fdroid-x86_64-release-unsigned-signed.apk asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-x86_64-release-unsigned-signed.apk
asset_name: amethyst-fdroid-x86_64-${{ github.ref_name }}.apk asset_name: amethyst-fdroid-x86_64-${{ github.ref_name }}.apk
asset_content_type: application/zip asset_content_type: application/zip
@@ -188,7 +188,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: amethyst/build/outputs/apk/fdroid/release/app-fdroid-arm64-v8a-release-unsigned-signed.apk asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-arm64-v8a-release-unsigned-signed.apk
asset_name: amethyst-fdroid-arm64-v8a-${{ github.ref_name }}.apk asset_name: amethyst-fdroid-arm64-v8a-${{ github.ref_name }}.apk
asset_content_type: application/zip asset_content_type: application/zip
@@ -199,7 +199,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: amethyst/build/outputs/apk/fdroid/release/app-fdroid-armeabi-v7a-release-unsigned-signed.apk asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-armeabi-v7a-release-unsigned-signed.apk
asset_name: amethyst-fdroid-armeabi-v7a-${{ github.ref_name }}.apk asset_name: amethyst-fdroid-armeabi-v7a-${{ github.ref_name }}.apk
asset_content_type: application/zip asset_content_type: application/zip
@@ -213,7 +213,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: amethyst/build/outputs/bundle/playRelease/app-play-release.aab asset_path: amethyst/build/outputs/bundle/playRelease/amethyst-play-release.aab
asset_name: amethyst-googleplay-${{ github.ref_name }}.aab asset_name: amethyst-googleplay-${{ github.ref_name }}.aab
asset_content_type: application/zip asset_content_type: application/zip
@@ -225,6 +225,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: amethyst/build/outputs/bundle/fdroidRelease/app-fdroid-release.aab asset_path: amethyst/build/outputs/bundle/fdroidRelease/amethyst-fdroid-release.aab
asset_name: amethyst-fdroid-${{ github.ref_name }}.aab asset_name: amethyst-fdroid-${{ github.ref_name }}.aab
asset_content_type: application/zip asset_content_type: application/zip
+2 -2
View File
@@ -12,8 +12,8 @@ android {
applicationId "com.vitorpamplona.amethyst" applicationId "com.vitorpamplona.amethyst"
minSdk 26 minSdk 26
targetSdk 34 targetSdk 34
versionCode 381 versionCode 382
versionName "0.88.4" versionName "0.88.5"
buildConfigField "String", "RELEASE_NOTES_ID", "\"2a34cbadd03212c8162e1ff896ba12641821088a2ec8d5e40d54aa80c0510800\"" buildConfigField "String", "RELEASE_NOTES_ID", "\"2a34cbadd03212c8162e1ff896ba12641821088a2ec8d5e40d54aa80c0510800\""
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -2203,6 +2203,7 @@ class Account(
url: String, url: String,
) { ) {
if (!isWriteable()) return if (!isWriteable()) return
GalleryListEvent.addEvent( GalleryListEvent.addEvent(
userProfile().latestGalleryList, userProfile().latestGalleryList,
idHex, idHex,
@@ -2226,7 +2227,7 @@ class Account(
GalleryListEvent.removeReplaceable( GalleryListEvent.removeReplaceable(
galleryentries, galleryentries,
note.address, note.address,
false, url,
signer, signer,
) { ) {
Client.send(it) Client.send(it)
@@ -2236,7 +2237,7 @@ class Account(
GalleryListEvent.removeEvent( GalleryListEvent.removeEvent(
galleryentries, galleryentries,
note.idHex, note.idHex,
false, url,
signer, signer,
) { ) {
Client.send(it) Client.send(it)
@@ -0,0 +1,63 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.R
class HttpStatusMessages {
companion object {
fun resourceIdFor(statusCode: Int = 0): Int? =
when (statusCode) {
400 -> R.string.http_status_400
401 -> R.string.http_status_401
402 -> R.string.http_status_402
403 -> R.string.http_status_403
404 -> R.string.http_status_404
405 -> R.string.http_status_405
406 -> R.string.http_status_406
407 -> R.string.http_status_407
408 -> R.string.http_status_408
409 -> R.string.http_status_409
410 -> R.string.http_status_410
411 -> R.string.http_status_411
412 -> R.string.http_status_412
413 -> R.string.http_status_413
414 -> R.string.http_status_414
415 -> R.string.http_status_415
416 -> R.string.http_status_416
417 -> R.string.http_status_417
426 -> R.string.http_status_426
500 -> R.string.http_status_500
501 -> R.string.http_status_501
502 -> R.string.http_status_502
503 -> R.string.http_status_503
504 -> R.string.http_status_504
505 -> R.string.http_status_505
506 -> R.string.http_status_506
507 -> R.string.http_status_507
508 -> R.string.http_status_508
511 -> R.string.http_status_511
else -> null
}
}
}
@@ -21,6 +21,7 @@
package com.vitorpamplona.amethyst.service package com.vitorpamplona.amethyst.service
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context
import android.net.Uri import android.net.Uri
import android.provider.OpenableColumns import android.provider.OpenableColumns
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
@@ -29,7 +30,9 @@ import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.ammolite.service.HttpClientManager import com.vitorpamplona.ammolite.service.HttpClientManager
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
@@ -48,7 +51,9 @@ val charPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')
fun randomChars() = List(16) { charPool.random() }.joinToString("") fun randomChars() = List(16) { charPool.random() }.joinToString("")
class Nip96Uploader(val account: Account?) { class Nip96Uploader(
val account: Account?,
) {
suspend fun uploadImage( suspend fun uploadImage(
uri: Uri, uri: Uri,
contentType: String?, contentType: String?,
@@ -58,6 +63,7 @@ class Nip96Uploader(val account: Account?) {
server: Nip96MediaServers.ServerName, server: Nip96MediaServers.ServerName,
contentResolver: ContentResolver, contentResolver: ContentResolver,
onProgress: (percentage: Float) -> Unit, onProgress: (percentage: Float) -> Unit,
context: Context,
): PartialEvent { ): PartialEvent {
val serverInfo = val serverInfo =
Nip96Retriever() Nip96Retriever()
@@ -74,6 +80,7 @@ class Nip96Uploader(val account: Account?) {
serverInfo, serverInfo,
contentResolver, contentResolver,
onProgress, onProgress,
context,
) )
} }
@@ -86,6 +93,7 @@ class Nip96Uploader(val account: Account?) {
server: Nip96Retriever.ServerInfo, server: Nip96Retriever.ServerInfo,
contentResolver: ContentResolver, contentResolver: ContentResolver,
onProgress: (percentage: Float) -> Unit, onProgress: (percentage: Float) -> Unit,
context: Context,
): PartialEvent { ): PartialEvent {
checkNotInMainThread() checkNotInMainThread()
@@ -111,6 +119,7 @@ class Nip96Uploader(val account: Account?) {
sensitiveContent, sensitiveContent,
server, server,
onProgress, onProgress,
context,
) )
} }
@@ -122,6 +131,7 @@ class Nip96Uploader(val account: Account?) {
sensitiveContent: String?, sensitiveContent: String?,
server: Nip96Retriever.ServerInfo, server: Nip96Retriever.ServerInfo,
onProgress: (percentage: Float) -> Unit, onProgress: (percentage: Float) -> Unit,
context: Context,
): PartialEvent { ): PartialEvent {
checkNotInMainThread() checkNotInMainThread()
@@ -130,11 +140,11 @@ class Nip96Uploader(val account: Account?) {
contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: "" contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: ""
val client = HttpClientManager.getHttpClient() val client = HttpClientManager.getHttpClient()
val requestBody: RequestBody
val requestBuilder = Request.Builder() val requestBuilder = Request.Builder()
requestBody = val requestBody: RequestBody =
MultipartBody.Builder() MultipartBody
.Builder()
.setType(MultipartBody.FORM) .setType(MultipartBody.FORM)
.addFormDataPart("expiration", "") .addFormDataPart("expiration", "")
.addFormDataPart("size", length.toString()) .addFormDataPart("size", length.toString())
@@ -142,8 +152,7 @@ class Nip96Uploader(val account: Account?) {
alt?.let { body.addFormDataPart("alt", it) } alt?.let { body.addFormDataPart("alt", it) }
sensitiveContent?.let { body.addFormDataPart("content-warning", it) } sensitiveContent?.let { body.addFormDataPart("content-warning", it) }
contentType?.let { body.addFormDataPart("content_type", it) } contentType?.let { body.addFormDataPart("content_type", it) }
} }.addFormDataPart(
.addFormDataPart(
"file", "file",
"$fileName.$extension", "$fileName.$extension",
object : RequestBody() { object : RequestBody() {
@@ -155,8 +164,7 @@ class Nip96Uploader(val account: Account?) {
inputStream.source().use(sink::writeAll) inputStream.source().use(sink::writeAll)
} }
}, },
) ).build()
.build()
nip98Header(server.apiUrl)?.let { requestBuilder.addHeader("Authorization", it) } nip98Header(server.apiUrl)?.let { requestBuilder.addHeader("Authorization", it) }
@@ -178,11 +186,16 @@ class Nip96Uploader(val account: Account?) {
} else if (result.status == "success" && result.nip94Event != null) { } else if (result.status == "success" && result.nip94Event != null) {
return result.nip94Event return result.nip94Event
} else { } else {
throw RuntimeException("Failed to upload with message: ${result.message}") throw RuntimeException(stringRes(context, R.string.failed_to_upload_with_message, result.message))
} }
} }
} else { } else {
throw RuntimeException("Error Uploading image: ${response.code}") val explanation = HttpStatusMessages.resourceIdFor(response.code)
if (explanation != null) {
throw RuntimeException(stringRes(context, R.string.failed_to_upload_with_message, stringRes(context, explanation)))
} else {
throw RuntimeException(stringRes(context, R.string.failed_to_upload_with_message, response.code))
}
} }
} }
} }
@@ -191,6 +204,7 @@ class Nip96Uploader(val account: Account?) {
hash: String, hash: String,
contentType: String?, contentType: String?,
server: Nip96Retriever.ServerInfo, server: Nip96Retriever.ServerInfo,
context: Context,
): Boolean { ): Boolean {
val extension = val extension =
contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: "" contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: ""
@@ -216,7 +230,12 @@ class Nip96Uploader(val account: Account?) {
return result.status == "success" return result.status == "success"
} }
} else { } else {
throw RuntimeException("Error Uploading image: ${response.code}") val explanation = HttpStatusMessages.resourceIdFor(response.code)
if (explanation != null) {
throw RuntimeException(stringRes(context, R.string.failed_to_delete_with_message, stringRes(context, explanation)))
} else {
throw RuntimeException(stringRes(context, R.string.failed_to_delete_with_message, response.code))
}
} }
} }
} }
@@ -233,7 +252,8 @@ class Nip96Uploader(val account: Account?) {
onProgress((currentResult.percentage ?: 100) / 100f) onProgress((currentResult.percentage ?: 100) / 100f)
val request: Request = val request: Request =
Request.Builder() Request
.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.url(result.processingUrl) .url(result.processingUrl)
.build() .build()
@@ -257,13 +277,12 @@ class Nip96Uploader(val account: Account?) {
} }
} }
suspend fun nip98Header(url: String): String? { suspend fun nip98Header(url: String): String? =
return withTimeoutOrNull(5000) { withTimeoutOrNull(5000) {
suspendCancellableCoroutine { continuation -> suspendCancellableCoroutine { continuation ->
nip98Header(url, "POST") { authorizationToken -> continuation.resume(authorizationToken) } nip98Header(url, "POST") { authorizationToken -> continuation.resume(authorizationToken) }
} }
} }
}
fun nip98Header( fun nip98Header(
url: String, url: String,
@@ -118,7 +118,8 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
) )
fun createOtherAccountsBaseFilter(): TypedFilter? { fun createOtherAccountsBaseFilter(): TypedFilter? {
if (otherAccounts.isEmpty()) return null val otherAuthors = otherAccounts.filter { it != account.userProfile().pubkeyHex }
if (otherAuthors.isEmpty()) return null
return TypedFilter( return TypedFilter(
types = EVENT_FINDER_TYPES, types = EVENT_FINDER_TYPES,
filter = filter =
@@ -134,7 +135,7 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
MuteListEvent.KIND, MuteListEvent.KIND,
PeopleListEvent.KIND, PeopleListEvent.KIND,
), ),
authors = otherAccounts.filter { it != account.userProfile().pubkeyHex }, authors = otherAuthors,
limit = 100, limit = 100,
), ),
) )
@@ -205,38 +205,54 @@ class EventNotificationConsumer(
event: LnZapEvent, event: LnZapEvent,
acc: Account, acc: Account,
) { ) {
Log.d("EventNotificationConsumer", "Notify Start ${event.toNostrUri()}")
val noteZapEvent = LocalCache.getNoteIfExists(event.id) ?: return val noteZapEvent = LocalCache.getNoteIfExists(event.id) ?: return
Log.d("EventNotificationConsumer", "Notify Not Notified Yet")
// old event being re-broadcast // old event being re-broadcast
if (event.createdAt < TimeUtils.fifteenMinutesAgo()) return if (event.createdAt < TimeUtils.fifteenMinutesAgo()) return
Log.d("EventNotificationConsumer", "Notify Not an old event")
val noteZapRequest = event.zapRequest?.id?.let { LocalCache.checkGetOrCreateNote(it) } ?: return val noteZapRequest = event.zapRequest?.id?.let { LocalCache.checkGetOrCreateNote(it) } ?: return
val noteZapped = val noteZapped =
event.zappedPost().firstOrNull()?.let { LocalCache.checkGetOrCreateNote(it) } ?: return event.zappedPost().firstOrNull()?.let { LocalCache.checkGetOrCreateNote(it) } ?: return
Log.d("EventNotificationConsumer", "Notify ZapRequest $noteZapRequest zapped $noteZapped")
if ((event.amount ?: BigDecimal.ZERO) < BigDecimal.TEN) return if ((event.amount ?: BigDecimal.ZERO) < BigDecimal.TEN) return
Log.d("EventNotificationConsumer", "Notify Amount Bigger than 10")
if (event.isTaggedUser(acc.userProfile().pubkeyHex)) { if (event.isTaggedUser(acc.userProfile().pubkeyHex)) {
val amount = showAmount(event.amount) val amount = showAmount(event.amount)
Log.d("EventNotificationConsumer", "Notify Amount $amount")
(noteZapRequest.event as? LnZapRequestEvent)?.let { event -> (noteZapRequest.event as? LnZapRequestEvent)?.let { event ->
acc.decryptZapContentAuthor(noteZapRequest) { acc.decryptZapContentAuthor(noteZapRequest) {
Log.d("EventNotificationConsumer", "Notify Decrypted if Private Zap ${event.id}")
val author = LocalCache.getOrCreateUser(it.pubKey) val author = LocalCache.getOrCreateUser(it.pubKey)
val senderInfo = Pair(author, it.content.ifBlank { null }) val senderInfo = Pair(author, it.content.ifBlank { null })
acc.decryptContent(noteZapped) { acc.decryptContent(noteZapped) {
Log.d("EventNotificationConsumer", "Notify Decrypted if Private Note")
val zappedContent = it.split("\n").get(0) val zappedContent = it.split("\n").get(0)
val user = senderInfo.first.toBestDisplayName() val user = senderInfo.first.toBestDisplayName()
var title = var title = stringRes(applicationContext, R.string.app_notification_zaps_channel_message, amount)
stringRes(applicationContext, R.string.app_notification_zaps_channel_message, amount)
senderInfo.second?.ifBlank { null }?.let { title += " ($it)" } senderInfo.second?.ifBlank { null }?.let { title += " ($it)" }
var content = var content =
stringRes( stringRes(
applicationContext, applicationContext,
R.string.app_notification_zaps_channel_message_from, R.string.app_notification_zaps_channel_message_from,
user, user,
) )
zappedContent?.let { zappedContent.let {
content += content +=
" " + " " +
stringRes( stringRes(
@@ -247,6 +263,9 @@ class EventNotificationConsumer(
} }
val userPicture = senderInfo?.first?.profilePicture() val userPicture = senderInfo?.first?.profilePicture()
val noteUri = "nostr:Notifications" val noteUri = "nostr:Notifications"
Log.d("EventNotificationConsumer", "Notify ${event.id} $content $title $noteUri")
notificationManager() notificationManager()
.sendZapNotification( .sendZapNotification(
event.id, event.id,
@@ -73,7 +73,7 @@ fun stringRes(
return String return String
.format( .format(
res.configuration.locales.get(0), res.configuration.locales.get(0),
resourceCache.get(id) ?: res.getString(id), resourceCache.get(id) ?: res.getString(id).also { resourceCache.put(id, it) },
*args, *args,
).also { resourceCache.put(id, it) } )
} }
@@ -53,7 +53,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Stable @Stable
open class EditPostViewModel() : ViewModel() { open class EditPostViewModel : ViewModel() {
var accountViewModel: AccountViewModel? = null var accountViewModel: AccountViewModel? = null
var account: Account? = null var account: Account? = null
@@ -185,6 +185,7 @@ open class EditPostViewModel() : ViewModel() {
server = server.server, server = server.server,
contentResolver = contentResolver, contentResolver = contentResolver,
onProgress = {}, onProgress = {},
context = context,
) )
createNIP94Record( createNIP94Record(
@@ -235,13 +236,12 @@ open class EditPostViewModel() : ViewModel() {
NostrSearchEventOrUserDataSource.clear() NostrSearchEventOrUserDataSource.clear()
} }
open fun findUrlInMessage(): String? { open fun findUrlInMessage(): String? =
return message.text.split('\n').firstNotNullOfOrNull { paragraph -> message.text.split('\n').firstNotNullOfOrNull { paragraph ->
paragraph.split(' ').firstOrNull { word: String -> paragraph.split(' ').firstOrNull { word: String ->
RichTextParser.isValidURL(word) || RichTextParser.isUrlWithoutScheme(word) RichTextParser.isValidURL(word) || RichTextParser.isUrlWithoutScheme(word)
} }
} }
}
open fun updateMessage(it: TextFieldValue) { open fun updateMessage(it: TextFieldValue) {
message = it message = it
@@ -249,14 +249,18 @@ open class EditPostViewModel() : ViewModel() {
if (it.selection.collapsed) { if (it.selection.collapsed) {
val lastWord = val lastWord =
it.text.substring(0, it.selection.end).substringAfterLast("\n").substringAfterLast(" ") it.text
.substring(0, it.selection.end)
.substringAfterLast("\n")
.substringAfterLast(" ")
userSuggestionAnchor = it.selection userSuggestionAnchor = it.selection
userSuggestionsMainMessage = UserSuggestionAnchor.MAIN_MESSAGE userSuggestionsMainMessage = UserSuggestionAnchor.MAIN_MESSAGE
if (lastWord.startsWith("@") && lastWord.length > 2) { if (lastWord.startsWith("@") && lastWord.length > 2) {
NostrSearchEventOrUserDataSource.search(lastWord.removePrefix("@")) NostrSearchEventOrUserDataSource.search(lastWord.removePrefix("@"))
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
userSuggestions = userSuggestions =
LocalCache.findUsersStartingWith(lastWord.removePrefix("@")) LocalCache
.findUsersStartingWith(lastWord.removePrefix("@"))
.sortedWith(compareBy({ account?.isFollowing(it) }, { it.toBestDisplayName() }, { it.pubkeyHex })) .sortedWith(compareBy({ account?.isFollowing(it) }, { it.toBestDisplayName() }, { it.pubkeyHex }))
.reversed() .reversed()
} }
@@ -271,7 +275,10 @@ open class EditPostViewModel() : ViewModel() {
userSuggestionAnchor?.let { userSuggestionAnchor?.let {
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) { if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
val lastWord = val lastWord =
message.text.substring(0, it.end).substringAfterLast("\n").substringAfterLast(" ") message.text
.substring(0, it.end)
.substringAfterLast("\n")
.substringAfterLast(" ")
val lastWordStart = it.end - lastWord.length val lastWordStart = it.end - lastWord.length
val wordToInsert = "@${item.pubkeyNpub()}" val wordToInsert = "@${item.pubkeyNpub()}"
@@ -288,12 +295,11 @@ open class EditPostViewModel() : ViewModel() {
} }
} }
fun canPost(): Boolean { fun canPost(): Boolean =
return message.text.isNotBlank() && message.text.isNotBlank() &&
!isUploadingImage && !isUploadingImage &&
!wantsInvoice && !wantsInvoice &&
contentToAddUrl == null contentToAddUrl == null
}
suspend fun createNIP94Record( suspend fun createNIP94Record(
uploadingResult: Nip96Uploader.PartialEvent, uploadingResult: Nip96Uploader.PartialEvent,
@@ -304,11 +310,20 @@ open class EditPostViewModel() : ViewModel() {
// Images don't seem to be ready immediately after upload // Images don't seem to be ready immediately after upload
val imageUrl = uploadingResult.tags?.firstOrNull { it.size > 1 && it[0] == "url" }?.get(1) val imageUrl = uploadingResult.tags?.firstOrNull { it.size > 1 && it[0] == "url" }?.get(1)
val remoteMimeType = val remoteMimeType =
uploadingResult.tags?.firstOrNull { it.size > 1 && it[0] == "m" }?.get(1)?.ifBlank { null } uploadingResult.tags
?.firstOrNull { it.size > 1 && it[0] == "m" }
?.get(1)
?.ifBlank { null }
val originalHash = val originalHash =
uploadingResult.tags?.firstOrNull { it.size > 1 && it[0] == "ox" }?.get(1)?.ifBlank { null } uploadingResult.tags
?.firstOrNull { it.size > 1 && it[0] == "ox" }
?.get(1)
?.ifBlank { null }
val dim = val dim =
uploadingResult.tags?.firstOrNull { it.size > 1 && it[0] == "dim" }?.get(1)?.ifBlank { null } uploadingResult.tags
?.firstOrNull { it.size > 1 && it[0] == "dim" }
?.get(1)
?.ifBlank { null }
val magnet = val magnet =
uploadingResult.tags uploadingResult.tags
?.firstOrNull { it.size > 1 && it[0] == "magnet" } ?.firstOrNull { it.size > 1 && it[0] == "magnet" }
@@ -139,6 +139,7 @@ open class NewMediaModel : ViewModel() {
onProgress = { percent: Float -> onProgress = { percent: Float ->
uploadingPercentage.value = 0.2f + (0.2f * percent) uploadingPercentage.value = 0.2f + (0.2f * percent)
}, },
context = context,
) )
createNIP94Record( createNIP94Record(
@@ -801,6 +801,7 @@ open class NewPostViewModel : ViewModel() {
server = server.server, server = server.server,
contentResolver = contentResolver, contentResolver = contentResolver,
onProgress = {}, onProgress = {},
context = context,
) )
createNIP94Record( createNIP94Record(
@@ -184,6 +184,7 @@ class NewUserMetadataViewModel : ViewModel() {
server = account.defaultFileServer, server = account.defaultFileServer,
contentResolver = contentResolver, contentResolver = contentResolver,
onProgress = {}, onProgress = {},
context = context,
) )
val url = result.tags?.firstOrNull { it.size > 1 && it[0] == "url" }?.get(1) val url = result.tags?.firstOrNull { it.size > 1 && it[0] == "url" }?.get(1)
@@ -142,7 +142,6 @@ fun LoadThumbAndThenVideoView(
roundedCorner: Boolean, roundedCorner: Boolean,
isFiniteHeight: Boolean, isFiniteHeight: Boolean,
nostrUriCallback: String? = null, nostrUriCallback: String? = null,
nostrIdCallback: String? = null,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
onDialog: ((Boolean) -> Unit)? = null, onDialog: ((Boolean) -> Unit)? = null,
) { ) {
@@ -177,7 +176,6 @@ fun LoadThumbAndThenVideoView(
artworkUri = thumbUri, artworkUri = thumbUri,
authorName = authorName, authorName = authorName,
nostrUriCallback = nostrUriCallback, nostrUriCallback = nostrUriCallback,
nostrIdCallback = nostrIdCallback,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
onDialog = onDialog, onDialog = onDialog,
) )
@@ -192,7 +190,6 @@ fun LoadThumbAndThenVideoView(
artworkUri = thumbUri, artworkUri = thumbUri,
authorName = authorName, authorName = authorName,
nostrUriCallback = nostrUriCallback, nostrUriCallback = nostrUriCallback,
nostrIdCallback = nostrIdCallback,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
onDialog = onDialog, onDialog = onDialog,
) )
@@ -214,7 +211,6 @@ fun VideoView(
dimensions: String? = null, dimensions: String? = null,
blurhash: String? = null, blurhash: String? = null,
nostrUriCallback: String? = null, nostrUriCallback: String? = null,
nostrIdCallback: String? = null,
onDialog: ((Boolean) -> Unit)? = null, onDialog: ((Boolean) -> Unit)? = null,
onControllerVisibilityChanged: ((Boolean) -> Unit)? = null, onControllerVisibilityChanged: ((Boolean) -> Unit)? = null,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
@@ -256,7 +252,6 @@ fun VideoView(
artworkUri = artworkUri, artworkUri = artworkUri,
authorName = authorName, authorName = authorName,
nostrUriCallback = nostrUriCallback, nostrUriCallback = nostrUriCallback,
nostrIDCallback = nostrIdCallback,
automaticallyStartPlayback = automaticallyStartPlayback, automaticallyStartPlayback = automaticallyStartPlayback,
onControllerVisibilityChanged = onControllerVisibilityChanged, onControllerVisibilityChanged = onControllerVisibilityChanged,
onDialog = onDialog, onDialog = onDialog,
@@ -310,7 +305,6 @@ fun VideoView(
artworkUri = artworkUri, artworkUri = artworkUri,
authorName = authorName, authorName = authorName,
nostrUriCallback = nostrUriCallback, nostrUriCallback = nostrUriCallback,
nostrIDCallback = nostrIdCallback,
automaticallyStartPlayback = automaticallyStartPlayback, automaticallyStartPlayback = automaticallyStartPlayback,
onControllerVisibilityChanged = onControllerVisibilityChanged, onControllerVisibilityChanged = onControllerVisibilityChanged,
onDialog = onDialog, onDialog = onDialog,
@@ -335,7 +329,6 @@ fun VideoViewInner(
artworkUri: String? = null, artworkUri: String? = null,
authorName: String? = null, authorName: String? = null,
nostrUriCallback: String? = null, nostrUriCallback: String? = null,
nostrIDCallback: String? = null,
automaticallyStartPlayback: State<Boolean>, automaticallyStartPlayback: State<Boolean>,
onControllerVisibilityChanged: ((Boolean) -> Unit)? = null, onControllerVisibilityChanged: ((Boolean) -> Unit)? = null,
onDialog: ((Boolean) -> Unit)? = null, onDialog: ((Boolean) -> Unit)? = null,
@@ -357,7 +350,6 @@ fun VideoViewInner(
roundedCorner = roundedCorner, roundedCorner = roundedCorner,
isFiniteHeight = isFiniteHeight, isFiniteHeight = isFiniteHeight,
nostrUriCallback = nostrUriCallback, nostrUriCallback = nostrUriCallback,
nostrIDCallback = nostrIDCallback,
waveform = waveform, waveform = waveform,
keepPlaying = keepPlaying, keepPlaying = keepPlaying,
automaticallyStartPlayback = automaticallyStartPlayback, automaticallyStartPlayback = automaticallyStartPlayback,
@@ -705,7 +697,6 @@ private fun RenderVideoPlayer(
roundedCorner: Boolean, roundedCorner: Boolean,
isFiniteHeight: Boolean, isFiniteHeight: Boolean,
nostrUriCallback: String?, nostrUriCallback: String?,
nostrIDCallback: String?,
waveform: ImmutableList<Int>? = null, waveform: ImmutableList<Int>? = null,
keepPlaying: MutableState<Boolean>, keepPlaying: MutableState<Boolean>,
automaticallyStartPlayback: State<Boolean>, automaticallyStartPlayback: State<Boolean>,
@@ -819,7 +810,7 @@ private fun RenderVideoPlayer(
} }
AnimatedShareButton(controllerVisible, Modifier.align(Alignment.TopEnd).padding(end = Size165dp)) { popupExpanded, toggle -> AnimatedShareButton(controllerVisible, Modifier.align(Alignment.TopEnd).padding(end = Size165dp)) { popupExpanded, toggle ->
ShareImageAction(accountViewModel = accountViewModel, popupExpanded, videoUri, nostrUriCallback, nostrIDCallback, toggle) ShareImageAction(accountViewModel = accountViewModel, popupExpanded, videoUri, nostrUriCallback, toggle)
} }
} }
} }
@@ -95,6 +95,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size75dp
import com.vitorpamplona.amethyst.ui.theme.hashVerifierMark import com.vitorpamplona.amethyst.ui.theme.hashVerifierMark
import com.vitorpamplona.amethyst.ui.theme.imageModifier import com.vitorpamplona.amethyst.ui.theme.imageModifier
import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.Nip19Bech32
import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.encoders.toHexKey
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@@ -138,7 +139,6 @@ fun ZoomableContentView(
roundedCorner = roundedCorner, roundedCorner = roundedCorner,
isFiniteHeight = isFiniteHeight, isFiniteHeight = isFiniteHeight,
nostrUriCallback = content.uri, nostrUriCallback = content.uri,
nostrIdCallback = content.id,
onDialog = { dialogOpen = true }, onDialog = { dialogOpen = true },
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
) )
@@ -163,7 +163,6 @@ fun ZoomableContentView(
roundedCorner = roundedCorner, roundedCorner = roundedCorner,
isFiniteHeight = isFiniteHeight, isFiniteHeight = isFiniteHeight,
nostrUriCallback = content.uri, nostrUriCallback = content.uri,
nostrIdCallback = content.id,
onDialog = { dialogOpen = true }, onDialog = { dialogOpen = true },
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
) )
@@ -609,7 +608,6 @@ fun ShareImageAction(
popupExpanded = popupExpanded, popupExpanded = popupExpanded,
videoUri = content.url, videoUri = content.url,
postNostrUri = content.uri, postNostrUri = content.uri,
postNostrid = content.id,
onDismiss = onDismiss, onDismiss = onDismiss,
) )
} else if (content is MediaPreloadedContent) { } else if (content is MediaPreloadedContent) {
@@ -618,7 +616,6 @@ fun ShareImageAction(
popupExpanded = popupExpanded, popupExpanded = popupExpanded,
videoUri = content.localFile?.toUri().toString(), videoUri = content.localFile?.toUri().toString(),
postNostrUri = content.uri, postNostrUri = content.uri,
postNostrid = content.id,
onDismiss = onDismiss, onDismiss = onDismiss,
) )
} }
@@ -631,7 +628,6 @@ fun ShareImageAction(
popupExpanded: MutableState<Boolean>, popupExpanded: MutableState<Boolean>,
videoUri: String?, videoUri: String?,
postNostrUri: String?, postNostrUri: String?,
postNostrid: String?,
onDismiss: () -> Unit, onDismiss: () -> Unit,
) { ) {
DropdownMenu( DropdownMenu(
@@ -665,13 +661,13 @@ fun ShareImageAction(
text = { Text(stringRes(R.string.add_media_to_gallery)) }, text = { Text(stringRes(R.string.add_media_to_gallery)) },
onClick = { onClick = {
if (videoUri != null) { if (videoUri != null) {
if (postNostrid != null) { var n19 = Nip19Bech32.uriToRoute(postNostrUri)?.entity as? Nip19Bech32.NEvent
print("TODO") if (n19 != null) {
print(postNostrid) accountViewModel.addMediaToGallery(n19.hex, videoUri)
// TODO this still crashes accountViewModel.toast(R.string.image_saved_to_the_gallery, R.string.image_saved_to_the_gallery)
accountViewModel.account.addToGallery(postNostrid, videoUri)
} }
} }
onDismiss() onDismiss()
}, },
) )
@@ -0,0 +1,52 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
class UserProfileGalleryFeedFilter(val user: User, val account: Account) : FeedFilter<Note>() {
override fun feedKey(): String {
return account.userProfile().pubkeyHex + "-Gallery-" + user.pubkeyHex
}
override fun feed(): List<Note> {
val notes =
user.latestGalleryList
?.taggedGalleryEntries()
?.mapNotNull { LocalCache.checkGetOrCreateNote(it.id) }
?.toSet()
?: emptySet()
/*val addresses =
user.latestGalleryList
?.taggedAddresses()
?.map { LocalCache.getOrCreateAddressableNote(it) }
?.toSet()
?: emptySet() */
// .sortedWith(DefaultFeedOrder)
return (notes)
.filter { account.isAcceptable(it) }
}
}
@@ -154,6 +154,38 @@ fun LongPressToQuickAction(
} }
} }
@Composable
fun LongPressToQuickActionGallery(
baseNote: Note,
accountViewModel: AccountViewModel,
content: @Composable (() -> Unit) -> Unit,
) {
val popupExpanded = remember { mutableStateOf(false) }
content { popupExpanded.value = true }
if (popupExpanded.value) {
NoteQuickActionMenuGallery(
note = baseNote,
onDismiss = { popupExpanded.value = false },
accountViewModel = accountViewModel,
nav = {},
)
}
}
@Composable
fun NoteQuickActionMenuGallery(
note: Note,
onDismiss: () -> Unit,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
DeleteFromGalleryDialog(note, accountViewModel) {
onDismiss()
}
}
@Composable @Composable
fun NoteQuickActionMenu( fun NoteQuickActionMenu(
note: Note, note: Note,
@@ -435,6 +467,169 @@ private fun RenderMainPopup(
} }
} }
@Composable
private fun RenderDeleteFromGalleryPopup(
accountViewModel: AccountViewModel,
note: Note,
showDeleteAlertDialog: MutableState<Boolean>,
onDismiss: () -> Unit,
) {
val context = LocalContext.current
val primaryLight = lightenColor(MaterialTheme.colorScheme.primary, 0.1f)
val cardShape = RoundedCornerShape(5.dp)
val clipboardManager = LocalClipboardManager.current
val scope = rememberCoroutineScope()
val backgroundColor =
if (MaterialTheme.colorScheme.isLight) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.secondaryButtonBackground
}
val showToast = { stringRes: Int ->
scope.launch {
Toast
.makeText(
context,
stringRes(context, stringRes),
Toast.LENGTH_SHORT,
).show()
}
}
val isOwnNote = accountViewModel.isLoggedUser(note.author)
val isFollowingUser = !isOwnNote && accountViewModel.isFollowing(note.author)
Popup(onDismissRequest = onDismiss, alignment = Alignment.Center) {
Card(
modifier = Modifier.shadow(elevation = 6.dp, shape = cardShape),
shape = cardShape,
colors = CardDefaults.cardColors(containerColor = backgroundColor),
) {
Column(modifier = Modifier.width(IntrinsicSize.Min)) {
Row(modifier = Modifier.height(IntrinsicSize.Min)) {
NoteQuickActionItem(
icon = Icons.Default.ContentCopy,
label = stringRes(R.string.quick_action_copy_text),
) {
accountViewModel.decrypt(note) {
clipboardManager.setText(AnnotatedString(it))
showToast(R.string.copied_note_text_to_clipboard)
}
onDismiss()
}
VerticalDivider(color = primaryLight)
NoteQuickActionItem(
Icons.Default.AlternateEmail,
stringRes(R.string.quick_action_copy_user_id),
) {
note.author?.let {
scope.launch {
clipboardManager.setText(AnnotatedString(it.toNostrUri()))
showToast(R.string.copied_user_id_to_clipboard)
onDismiss()
}
}
}
VerticalDivider(color = primaryLight)
NoteQuickActionItem(
Icons.Default.FormatQuote,
stringRes(R.string.quick_action_copy_note_id),
) {
scope.launch {
clipboardManager.setText(AnnotatedString(note.toNostrUri()))
showToast(R.string.copied_note_id_to_clipboard)
onDismiss()
}
}
}
HorizontalDivider(
color = primaryLight,
)
Row(modifier = Modifier.height(IntrinsicSize.Min)) {
if (isOwnNote) {
NoteQuickActionItem(
Icons.Default.Delete,
stringRes(R.string.quick_action_delete),
) {
if (accountViewModel.hideDeleteRequestDialog) {
accountViewModel.delete(note)
onDismiss()
} else {
showDeleteAlertDialog.value = true
}
}
} else if (isFollowingUser) {
NoteQuickActionItem(
Icons.Default.PersonRemove,
stringRes(R.string.quick_action_unfollow),
) {
accountViewModel.unfollow(note.author!!)
onDismiss()
}
} else {
NoteQuickActionItem(
Icons.Default.PersonAdd,
stringRes(R.string.quick_action_follow),
) {
accountViewModel.follow(note.author!!)
onDismiss()
}
}
VerticalDivider(color = primaryLight)
NoteQuickActionItem(
icon = ImageVector.vectorResource(id = R.drawable.relays),
label = stringRes(R.string.broadcast),
) {
accountViewModel.broadcast(note)
// showSelectTextDialog = true
onDismiss()
}
VerticalDivider(color = primaryLight)
if (isOwnNote && note.isDraft()) {
NoteQuickActionItem(
Icons.Default.Edit,
stringRes(R.string.edit_draft),
) {
onDismiss()
}
} else {
NoteQuickActionItem(
icon = Icons.Default.Share,
label = stringRes(R.string.quick_action_share),
) {
val sendIntent =
Intent().apply {
action = Intent.ACTION_SEND
type = "text/plain"
putExtra(
Intent.EXTRA_TEXT,
externalLinkForNote(note),
)
putExtra(
Intent.EXTRA_TITLE,
stringRes(context, R.string.quick_action_share_browser_link),
)
}
val shareIntent =
Intent.createChooser(
sendIntent,
stringRes(context, R.string.quick_action_share),
)
ContextCompat.startActivity(context, shareIntent, null)
onDismiss()
}
}
}
}
}
}
}
@Composable @Composable
fun NoteQuickActionItem( fun NoteQuickActionItem(
icon: ImageVector, icon: ImageVector,
@@ -462,6 +657,25 @@ fun NoteQuickActionItem(
} }
} }
@Composable
fun DeleteFromGalleryDialog(
note: Note,
accountViewModel: AccountViewModel,
onDismiss: () -> Unit,
) {
QuickActionAlertDialogOneButton(
title = stringRes(R.string.quick_action_request_deletion_gallery_title),
textContent = stringRes(R.string.quick_action_request_deletion_gallery_alert_body),
buttonIcon = Icons.Default.Delete,
buttonText = stringRes(R.string.quick_action_delete_dialog_btn),
onClickDoOnce = {
note.event?.firstTaggedUrl()?.let { accountViewModel.removefromMediaGallery(note, it) }
onDismiss()
},
onDismiss = onDismiss,
)
}
@Composable @Composable
fun DeleteAlertDialog( fun DeleteAlertDialog(
note: Note, note: Note,
@@ -612,3 +826,95 @@ fun QuickActionAlertDialog(
}, },
) )
} }
@Composable
fun QuickActionAlertDialogOneButton(
title: String,
textContent: String,
buttonIcon: ImageVector,
buttonText: String,
buttonColors: ButtonColors = ButtonDefaults.buttonColors(),
onClickDoOnce: () -> Unit,
onDismiss: () -> Unit,
) {
QuickActionAlertDialogOneButton(
title = title,
textContent = textContent,
icon = {
Icon(
imageVector = buttonIcon,
contentDescription = null,
)
},
buttonText = buttonText,
buttonColors = buttonColors,
onClickDoOnce = onClickDoOnce,
onDismiss = onDismiss,
)
}
@Composable
fun QuickActionAlertDialogOneButton(
title: String,
textContent: String,
buttonIconResource: Int,
buttonText: String,
buttonColors: ButtonColors = ButtonDefaults.buttonColors(),
onClickDoOnce: () -> Unit,
onDismiss: () -> Unit,
) {
QuickActionAlertDialogOneButton(
title = title,
textContent = textContent,
icon = {
Icon(
painter = painterResource(buttonIconResource),
contentDescription = null,
)
},
buttonText = buttonText,
buttonColors = buttonColors,
onClickDoOnce = onClickDoOnce,
onDismiss = onDismiss,
)
}
@Composable
fun QuickActionAlertDialogOneButton(
title: String,
textContent: String,
icon: @Composable () -> Unit,
buttonText: String,
buttonColors: ButtonColors = ButtonDefaults.buttonColors(),
onClickDoOnce: () -> Unit,
onDismiss: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(title) },
text = { Text(textContent) },
confirmButton = {
Row(
modifier =
Modifier
.padding(all = 8.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Button(
onClick = onClickDoOnce,
colors = buttonColors,
contentPadding = PaddingValues(horizontal = 16.dp),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
icon()
Spacer(Modifier.width(8.dp))
Text(buttonText)
}
}
}
},
)
}
@@ -212,7 +212,6 @@ fun AudioHeader(
isFiniteHeight = isFiniteHeight, isFiniteHeight = isFiniteHeight,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nostrUriCallback = note.toNostrUri(), nostrUriCallback = note.toNostrUri(),
nostrIdCallback = note.idHex,
) )
} }
} }
@@ -160,7 +160,6 @@ fun RenderLiveActivityEventInner(
isFiniteHeight = false, isFiniteHeight = false,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nostrUriCallback = "nostr:${baseNote.toNEvent()}", nostrUriCallback = "nostr:${baseNote.toNEvent()}",
nostrIdCallback = baseNote.idHex,
) )
} }
} else { } else {
@@ -57,6 +57,7 @@ import com.vitorpamplona.amethyst.ui.dal.ThreadFeedFilter
import com.vitorpamplona.amethyst.ui.dal.UserProfileAppRecommendationsFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileAppRecommendationsFeedFilter
import com.vitorpamplona.amethyst.ui.dal.UserProfileBookmarksFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileBookmarksFeedFilter
import com.vitorpamplona.amethyst.ui.dal.UserProfileConversationsFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileConversationsFeedFilter
import com.vitorpamplona.amethyst.ui.dal.UserProfileGalleryFeedFilter
import com.vitorpamplona.amethyst.ui.dal.UserProfileNewThreadFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileNewThreadFeedFilter
import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter
import com.vitorpamplona.amethyst.ui.dal.VideoFeedFilter import com.vitorpamplona.amethyst.ui.dal.VideoFeedFilter
@@ -214,6 +215,16 @@ class NostrUserProfileReportFeedViewModel(val user: User) :
} }
} }
class NostrUserProfileGalleryFeedViewModel(val user: User, val account: Account) :
FeedViewModel(UserProfileGalleryFeedFilter(user, account)) {
class Factory(val user: User, val account: Account) : ViewModelProvider.Factory {
override fun <NostrUserProfileGalleryFeedViewModel : ViewModel> create(modelClass: Class<NostrUserProfileGalleryFeedViewModel>): NostrUserProfileGalleryFeedViewModel {
return NostrUserProfileGalleryFeedViewModel(user, account)
as NostrUserProfileGalleryFeedViewModel
}
}
}
class NostrUserProfileBookmarksFeedViewModel(val user: User, val account: Account) : class NostrUserProfileBookmarksFeedViewModel(val user: User, val account: Account) :
FeedViewModel(UserProfileBookmarksFeedFilter(user, account)) { FeedViewModel(UserProfileBookmarksFeedFilter(user, account)) {
class Factory(val user: User, val account: Account) : ViewModelProvider.Factory { class Factory(val user: User, val account: Account) : ViewModelProvider.Factory {
@@ -669,6 +669,20 @@ class AccountViewModel(
viewModelScope.launch(Dispatchers.IO) { account.addEmojiPack(usersEmojiList, emojiList) } viewModelScope.launch(Dispatchers.IO) { account.addEmojiPack(usersEmojiList, emojiList) }
} }
fun addMediaToGallery(
hex: String,
url: String,
) {
viewModelScope.launch(Dispatchers.IO) { account.addToGallery(hex, url) }
}
fun removefromMediaGallery(
note: Note,
url: String,
) {
viewModelScope.launch(Dispatchers.IO) { account.removeFromGallery(note, url) }
}
fun addPrivateBookmark(note: Note) { fun addPrivateBookmark(note: Note) {
viewModelScope.launch(Dispatchers.IO) { account.addBookmark(note, true) } viewModelScope.launch(Dispatchers.IO) { account.addBookmark(note, true) }
} }
@@ -20,77 +20,158 @@
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn package com.vitorpamplona.amethyst.ui.screen.loggedIn
import androidx.compose.foundation.background import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize 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.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
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.remember
import androidx.compose.ui.Alignment.Companion.BottomStart import androidx.compose.ui.Alignment.Companion.BottomStart
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map import androidx.lifecycle.map
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
import com.vitorpamplona.amethyst.ui.note.CheckHiddenFeedWatchBlockAndReport import com.vitorpamplona.amethyst.ui.note.CheckHiddenFeedWatchBlockAndReport
import com.vitorpamplona.amethyst.ui.note.ClickableNote import com.vitorpamplona.amethyst.ui.note.ClickableNote
import com.vitorpamplona.amethyst.ui.note.LongPressToQuickAction import com.vitorpamplona.amethyst.ui.note.LongPressToQuickActionGallery
import com.vitorpamplona.amethyst.ui.note.WatchAuthor import com.vitorpamplona.amethyst.ui.note.WatchAuthor
import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent
import com.vitorpamplona.amethyst.ui.note.calculateBackgroundColor import com.vitorpamplona.amethyst.ui.note.calculateBackgroundColor
import com.vitorpamplona.amethyst.ui.note.elements.BannerImage import com.vitorpamplona.amethyst.ui.note.elements.BannerImage
import com.vitorpamplona.amethyst.ui.screen.FeedEmpty
import com.vitorpamplona.amethyst.ui.screen.FeedError
import com.vitorpamplona.amethyst.ui.screen.FeedState
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
import com.vitorpamplona.amethyst.ui.screen.LoadingFeed
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.HalfPadding import com.vitorpamplona.amethyst.ui.theme.HalfPadding
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.Size5dp import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.GalleryListEvent
// TODO This is to large parts from the ChannelCardCompose
// Why does it not be in a grid, like the marketplace
@Composable @Composable
fun ProfileGallery( fun RenderGalleryFeed(
baseNotes: List<GalleryThumb>, viewModel: FeedViewModel,
routeForLastRead: String?,
forceEventKind: Int?,
listState: LazyGridState,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
val feedState by viewModel.feedContent.collectAsStateWithLifecycle()
CrossfadeIfEnabled(
targetState = feedState,
animationSpec = tween(durationMillis = 100),
label = "RenderDiscoverFeed",
accountViewModel = accountViewModel,
) { state ->
when (state) {
is FeedState.Empty -> {
FeedEmpty { viewModel.invalidateData() }
}
is FeedState.FeedError -> {
FeedError(state.errorMessage) { viewModel.invalidateData() }
}
is FeedState.Loaded -> {
GalleryFeedLoaded(
state,
routeForLastRead,
listState,
forceEventKind,
accountViewModel,
nav,
)
}
is FeedState.Loading -> {
LoadingFeed()
}
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun GalleryFeedLoaded(
state: FeedState.Loaded,
routeForLastRead: String?,
listState: LazyGridState,
forceEventKind: Int?,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
LazyVerticalGrid(
columns = GridCells.Fixed(3),
contentPadding = FeedPadding,
state = listState,
) {
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item ->
val defaultModifier = remember { Modifier.fillMaxWidth().animateItemPlacement() }
Row(defaultModifier) {
GalleryCardCompose(
baseNote = item,
routeForLastRead = routeForLastRead,
modifier = Modifier,
forceEventKind = forceEventKind,
accountViewModel = accountViewModel,
nav = nav,
)
}
HorizontalDivider(
thickness = DividerThickness,
)
}
}
}
@Composable
fun GalleryCardCompose(
baseNote: Note,
routeForLastRead: String? = null,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
parentBackgroundColor: MutableState<Color>? = null, parentBackgroundColor: MutableState<Color>? = null,
forceEventKind: Int?,
isHiddenFeed: Boolean = false, isHiddenFeed: Boolean = false,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
for (thumb in baseNotes) { WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel) {
thumb.baseNote?.let {
WatchNoteEvent(baseNote = it, accountViewModel = accountViewModel) {
if (thumb.baseNote.event?.kind() == GalleryListEvent.KIND) {
CheckHiddenFeedWatchBlockAndReport( CheckHiddenFeedWatchBlockAndReport(
note = thumb.baseNote, note = baseNote,
modifier = modifier, modifier = modifier,
ignoreAllBlocksAndReports = isHiddenFeed, ignoreAllBlocksAndReports = isHiddenFeed,
showHiddenWarning = false, showHiddenWarning = false,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, nav = nav,
) { canPreview -> ) { canPreview ->
thumb.image?.let { it1 ->
GalleryCard( GalleryCard(
baseNote = thumb.baseNote, baseNote = baseNote,
url = it1,
modifier = modifier, modifier = modifier,
parentBackgroundColor = parentBackgroundColor, parentBackgroundColor = parentBackgroundColor,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
@@ -99,26 +180,19 @@ fun ProfileGallery(
} }
} }
} }
}
}
}
}
@Composable @Composable
fun GalleryCard( fun GalleryCard(
baseNote: Note, baseNote: Note,
url: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
parentBackgroundColor: MutableState<Color>? = null, parentBackgroundColor: MutableState<Color>? = null,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
// baseNote.event?.let { Text(text = it.pubKey()) } // baseNote.event?.let { Text(text = it.pubKey()) }
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup -> LongPressToQuickActionGallery(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
CheckNewAndRenderChannelCard( CheckNewAndRenderChannelCard(
baseNote, baseNote,
url,
modifier, modifier,
parentBackgroundColor, parentBackgroundColor,
accountViewModel, accountViewModel,
@@ -131,7 +205,6 @@ fun GalleryCard(
@Composable @Composable
private fun CheckNewAndRenderChannelCard( private fun CheckNewAndRenderChannelCard(
baseNote: Note, baseNote: Note,
url: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
parentBackgroundColor: MutableState<Color>? = null, parentBackgroundColor: MutableState<Color>? = null,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
@@ -156,7 +229,6 @@ private fun CheckNewAndRenderChannelCard(
// baseNote.event?.let { Text(text = it.pubKey()) } // baseNote.event?.let { Text(text = it.pubKey()) }
InnerGalleryCardWithReactions( InnerGalleryCardWithReactions(
baseNote = baseNote, baseNote = baseNote,
url = url,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, nav = nav,
) )
@@ -166,17 +238,15 @@ private fun CheckNewAndRenderChannelCard(
@Composable @Composable
fun InnerGalleryCardWithReactions( fun InnerGalleryCardWithReactions(
baseNote: Note, baseNote: Note,
url: String,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
InnerGalleryCardBox(baseNote, url, accountViewModel, nav) InnerGalleryCardBox(baseNote, accountViewModel, nav)
} }
@Composable @Composable
fun InnerGalleryCardBox( fun InnerGalleryCardBox(
baseNote: Note, baseNote: Note,
url: String,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
@@ -185,14 +255,13 @@ fun InnerGalleryCardBox(
note = baseNote, note = baseNote,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
) { ) {
RenderGalleryThumb(baseNote, url, accountViewModel, nav) RenderGalleryThumb(baseNote, accountViewModel, nav)
} }
} }
} }
@Immutable @Immutable
data class GalleryThumb( data class GalleryThumb(
val baseNote: Note?,
val id: String?, val id: String?,
val image: String?, val image: String?,
val title: String?, val title: String?,
@@ -202,34 +271,31 @@ data class GalleryThumb(
@Composable @Composable
fun RenderGalleryThumb( fun RenderGalleryThumb(
baseNote: Note, baseNote: Note,
url: String,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
val noteEvent = baseNote.event as? GalleryListEvent ?: return val noteEvent = baseNote.event as? TextNoteEvent ?: return
val card by val card by
baseNote baseNote
.live() .live()
.metadata .metadata
.map { .map {
val noteEvent = baseNote.event as GalleryListEvent val noteEvent = baseNote.event as TextNoteEvent
GalleryThumb( GalleryThumb(
baseNote = baseNote,
id = "", id = "",
image = url, image = noteEvent.firstTaggedUrl(),
title = "Hello", title = noteEvent.content(),
// noteEvent?.title(), // noteEvent?.title(),
// price = noteEvent?.price(), // price = noteEvent?.price(),
) )
}.distinctUntilChanged() }.distinctUntilChanged()
.observeAsState( .observeAsState(
GalleryThumb( GalleryThumb(
baseNote = baseNote,
id = "", id = "",
image = "https://gokaygokay-aurasr.hf.space/file=/tmp/gradio/68292f324a38d7071453cf6912dfb1da9d1305c8/image3.png", image = noteEvent.firstTaggedUrl(),
title = "Hello", title = noteEvent.content(),
// image = noteEvent.image(), // image = noteEvent.image(),
// title = noteEvent.title(), // title = noteEvent.title(),
// price = noteEvent.price(), // price = noteEvent.price(),
@@ -246,7 +312,6 @@ fun RenderGalleryThumbPreview() {
InnerRenderGalleryThumb( InnerRenderGalleryThumb(
card = card =
GalleryThumb( GalleryThumb(
baseNote = null,
id = "", id = "",
image = null, image = null,
title = "Like New", title = "Like New",
@@ -277,7 +342,7 @@ fun InnerRenderGalleryThumb(
) )
} ?: run { DisplayGalleryAuthorBanner(note) } } ?: run { DisplayGalleryAuthorBanner(note) }
Row( /* Row(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.background(Color.Black.copy(0.6f)) .background(Color.Black.copy(0.6f))
@@ -294,7 +359,7 @@ fun InnerRenderGalleryThumb(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) )
} }
/*
card.price?.let { card.price?.let {
val priceTag = val priceTag =
remember(card) { remember(card) {
@@ -315,8 +380,8 @@ fun InnerRenderGalleryThumb(
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
color = Color.White, color = Color.White,
) )
}*/
} }
}*/
} }
} }
@@ -42,6 +42,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.PagerState
@@ -148,6 +149,7 @@ import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileBookmarksFeedViewMod
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileConversationsFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileConversationsFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileFollowersUserFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileFollowersUserFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileFollowsUserFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileFollowsUserFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileGalleryFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileNewThreadsFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileNewThreadsFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileReportFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileReportFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileZapsFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileZapsFeedViewModel
@@ -155,6 +157,7 @@ import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
import com.vitorpamplona.amethyst.ui.screen.RefreshingFeedUserFeedView import com.vitorpamplona.amethyst.ui.screen.RefreshingFeedUserFeedView
import com.vitorpamplona.amethyst.ui.screen.RelayFeedView import com.vitorpamplona.amethyst.ui.screen.RelayFeedView
import com.vitorpamplona.amethyst.ui.screen.RelayFeedViewModel import com.vitorpamplona.amethyst.ui.screen.RelayFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.SaveableGridFeedState
import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
@@ -294,6 +297,16 @@ fun PrepareViewModels(
), ),
) )
val galleryFeedViewModel: NostrUserProfileGalleryFeedViewModel =
viewModel(
key = baseUser.pubkeyHex + "UserGalleryFeedViewModel",
factory =
NostrUserProfileGalleryFeedViewModel.Factory(
baseUser,
accountViewModel.account,
),
)
val reportsFeedViewModel: NostrUserProfileReportFeedViewModel = val reportsFeedViewModel: NostrUserProfileReportFeedViewModel =
viewModel( viewModel(
key = baseUser.pubkeyHex + "UserProfileReportFeedViewModel", key = baseUser.pubkeyHex + "UserProfileReportFeedViewModel",
@@ -312,6 +325,7 @@ fun PrepareViewModels(
appRecommendations, appRecommendations,
zapFeedViewModel, zapFeedViewModel,
bookmarksFeedViewModel, bookmarksFeedViewModel,
galleryFeedViewModel,
reportsFeedViewModel, reportsFeedViewModel,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, nav = nav,
@@ -328,6 +342,7 @@ fun ProfileScreen(
appRecommendations: NostrUserAppRecommendationsFeedViewModel, appRecommendations: NostrUserAppRecommendationsFeedViewModel,
zapFeedViewModel: NostrUserProfileZapsFeedViewModel, zapFeedViewModel: NostrUserProfileZapsFeedViewModel,
bookmarksFeedViewModel: NostrUserProfileBookmarksFeedViewModel, bookmarksFeedViewModel: NostrUserProfileBookmarksFeedViewModel,
galleryFeedViewModel: NostrUserProfileGalleryFeedViewModel,
reportsFeedViewModel: NostrUserProfileReportFeedViewModel, reportsFeedViewModel: NostrUserProfileReportFeedViewModel,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
@@ -372,6 +387,7 @@ fun ProfileScreen(
followersFeedViewModel, followersFeedViewModel,
zapFeedViewModel, zapFeedViewModel,
bookmarksFeedViewModel, bookmarksFeedViewModel,
galleryFeedViewModel,
reportsFeedViewModel, reportsFeedViewModel,
accountViewModel, accountViewModel,
nav, nav,
@@ -388,6 +404,7 @@ private fun RenderSurface(
followersFeedViewModel: NostrUserProfileFollowersUserFeedViewModel, followersFeedViewModel: NostrUserProfileFollowersUserFeedViewModel,
zapFeedViewModel: NostrUserProfileZapsFeedViewModel, zapFeedViewModel: NostrUserProfileZapsFeedViewModel,
bookmarksFeedViewModel: NostrUserProfileBookmarksFeedViewModel, bookmarksFeedViewModel: NostrUserProfileBookmarksFeedViewModel,
galleryFeedViewModel: NostrUserProfileGalleryFeedViewModel,
reportsFeedViewModel: NostrUserProfileReportFeedViewModel, reportsFeedViewModel: NostrUserProfileReportFeedViewModel,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
@@ -448,6 +465,7 @@ private fun RenderSurface(
followersFeedViewModel, followersFeedViewModel,
zapFeedViewModel, zapFeedViewModel,
bookmarksFeedViewModel, bookmarksFeedViewModel,
galleryFeedViewModel,
reportsFeedViewModel, reportsFeedViewModel,
accountViewModel, accountViewModel,
nav, nav,
@@ -470,6 +488,7 @@ private fun RenderScreen(
followersFeedViewModel: NostrUserProfileFollowersUserFeedViewModel, followersFeedViewModel: NostrUserProfileFollowersUserFeedViewModel,
zapFeedViewModel: NostrUserProfileZapsFeedViewModel, zapFeedViewModel: NostrUserProfileZapsFeedViewModel,
bookmarksFeedViewModel: NostrUserProfileBookmarksFeedViewModel, bookmarksFeedViewModel: NostrUserProfileBookmarksFeedViewModel,
galleryFeedViewModel: NostrUserProfileGalleryFeedViewModel,
reportsFeedViewModel: NostrUserProfileReportFeedViewModel, reportsFeedViewModel: NostrUserProfileReportFeedViewModel,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
@@ -501,6 +520,7 @@ private fun RenderScreen(
followersFeedViewModel, followersFeedViewModel,
zapFeedViewModel, zapFeedViewModel,
bookmarksFeedViewModel, bookmarksFeedViewModel,
galleryFeedViewModel,
reportsFeedViewModel, reportsFeedViewModel,
accountViewModel, accountViewModel,
nav, nav,
@@ -519,6 +539,7 @@ private fun CreateAndRenderPages(
followersFeedViewModel: NostrUserProfileFollowersUserFeedViewModel, followersFeedViewModel: NostrUserProfileFollowersUserFeedViewModel,
zapFeedViewModel: NostrUserProfileZapsFeedViewModel, zapFeedViewModel: NostrUserProfileZapsFeedViewModel,
bookmarksFeedViewModel: NostrUserProfileBookmarksFeedViewModel, bookmarksFeedViewModel: NostrUserProfileBookmarksFeedViewModel,
galleryFeedViewModel: NostrUserProfileGalleryFeedViewModel,
reportsFeedViewModel: NostrUserProfileReportFeedViewModel, reportsFeedViewModel: NostrUserProfileReportFeedViewModel,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
@@ -533,7 +554,7 @@ private fun CreateAndRenderPages(
when (page) { when (page) {
0 -> TabNotesNewThreads(threadsViewModel, accountViewModel, nav) 0 -> TabNotesNewThreads(threadsViewModel, accountViewModel, nav)
1 -> TabNotesConversations(repliesViewModel, accountViewModel, nav) 1 -> TabNotesConversations(repliesViewModel, accountViewModel, nav)
2 -> Gallery(baseUser, followsFeedViewModel, accountViewModel, nav) 2 -> TabGallery(galleryFeedViewModel, accountViewModel, nav)
3 -> TabFollows(baseUser, followsFeedViewModel, accountViewModel, nav) 3 -> TabFollows(baseUser, followsFeedViewModel, accountViewModel, nav)
4 -> TabFollowers(baseUser, followersFeedViewModel, accountViewModel, nav) 4 -> TabFollowers(baseUser, followersFeedViewModel, accountViewModel, nav)
5 -> TabReceivedZaps(baseUser, zapFeedViewModel, accountViewModel, nav) 5 -> TabReceivedZaps(baseUser, zapFeedViewModel, accountViewModel, nav)
@@ -1537,7 +1558,36 @@ fun TabNotesConversations(
} }
} }
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun TabGallery(
feedViewModel: NostrUserProfileGalleryFeedViewModel,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
LaunchedEffect(Unit) { feedViewModel.invalidateData() }
Column(Modifier.fillMaxHeight()) {
Column(
modifier = Modifier.padding(vertical = 0.dp),
) {
var state = LazyGridState()
SaveableGridFeedState(feedViewModel, scrollStateKey = "gallery") { listState ->
RenderGalleryFeed(
feedViewModel,
null,
0,
state,
accountViewModel = accountViewModel,
nav = nav,
)
}
}
}
}
/*@Composable
fun Gallery( fun Gallery(
baseUser: User, baseUser: User,
feedViewModel: UserFeedViewModel, feedViewModel: UserFeedViewModel,
@@ -1579,7 +1629,7 @@ fun Gallery(
} }
} }
} }
} } */
@Composable @Composable
fun TabFollowedTags( fun TabFollowedTags(
@@ -14,6 +14,7 @@
<string name="could_not_decrypt_the_message">Nie można odszyfrować wiadomości</string> <string name="could_not_decrypt_the_message">Nie można odszyfrować wiadomości</string>
<string name="group_picture">Zdjęcie grupy</string> <string name="group_picture">Zdjęcie grupy</string>
<string name="explicit_content">Niedozwolona zawartość</string> <string name="explicit_content">Niedozwolona zawartość</string>
<string name="spam">Spam</string>
<string name="spam_description">Liczba spamu z tego transmitera</string> <string name="spam_description">Liczba spamu z tego transmitera</string>
<string name="impersonation">Podszywanie się</string> <string name="impersonation">Podszywanie się</string>
<string name="illegal_behavior">Antyspołeczne zachowanie</string> <string name="illegal_behavior">Antyspołeczne zachowanie</string>
@@ -199,6 +200,8 @@
<string name="never">nigdy</string> <string name="never">nigdy</string>
<string name="now">teraz</string> <string name="now">teraz</string>
<string name="h">godz.</string> <string name="h">godz.</string>
<string name="m">m</string>
<string name="d">d</string>
<string name="nudity">Nagość</string> <string name="nudity">Nagość</string>
<string name="profanity_hateful_speech">Wulgaryzmy / Mowa nienawiści</string> <string name="profanity_hateful_speech">Wulgaryzmy / Mowa nienawiści</string>
<string name="report_hateful_speech">Zgłoś nienawistną mowę</string> <string name="report_hateful_speech">Zgłoś nienawistną mowę</string>
@@ -301,6 +304,7 @@
<string name="poll_zap_value_min">Minimalny Zap</string> <string name="poll_zap_value_min">Minimalny Zap</string>
<string name="poll_zap_value_max">Maksymalny Zap</string> <string name="poll_zap_value_max">Maksymalny Zap</string>
<string name="poll_consensus_threshold">Konsensus</string> <string name="poll_consensus_threshold">Konsensus</string>
<string name="poll_consensus_threshold_percent">(0100)%</string>
<string name="poll_closing_time">Zamknij po</string> <string name="poll_closing_time">Zamknij po</string>
<string name="poll_closing_time_days">dni</string> <string name="poll_closing_time_days">dni</string>
<string name="poll_unable_to_vote">Nie można głosować</string> <string name="poll_unable_to_vote">Nie można głosować</string>
@@ -383,6 +387,7 @@
<string name="warn_when_posts_have_reports_from_your_follows">Ostrzegaj, gdy posty zostały zgłoszone przez osoby które obserwujesz</string> <string name="warn_when_posts_have_reports_from_your_follows">Ostrzegaj, gdy posty zostały zgłoszone przez osoby które obserwujesz</string>
<string name="new_reaction_symbol">Nowy Symbol Odzewu</string> <string name="new_reaction_symbol">Nowy Symbol Odzewu</string>
<string name="no_reaction_type_setup_long_press_to_change">Brak wstępnie wybranych typów reakcji dla tego użytkownika. Przytrzymaj przycisk serce, aby zmienić</string> <string name="no_reaction_type_setup_long_press_to_change">Brak wstępnie wybranych typów reakcji dla tego użytkownika. Przytrzymaj przycisk serce, aby zmienić</string>
<string name="zapraiser">Zapraiser</string>
<string name="zapraiser_explainer">Dodaje docelową liczbę satów do podniesienia dla tego wpisu. W zależności od aplikacji może być pokazywany to jako pasek postępu, aby zachęcić do darowizn</string> <string name="zapraiser_explainer">Dodaje docelową liczbę satów do podniesienia dla tego wpisu. W zależności od aplikacji może być pokazywany to jako pasek postępu, aby zachęcić do darowizn</string>
<string name="zapraiser_target_amount_in_sats">Docelowa kwota w Satach</string> <string name="zapraiser_target_amount_in_sats">Docelowa kwota w Satach</string>
<string name="sats_to_complete">Zapraiser przy: %1$s. %2$s satach do celu</string> <string name="sats_to_complete">Zapraiser przy: %1$s. %2$s satach do celu</string>
@@ -414,6 +419,7 @@
<string name="minimum_pow">Minimalny poW</string> <string name="minimum_pow">Minimalny poW</string>
<string name="auth">Autoryzacja</string> <string name="auth">Autoryzacja</string>
<string name="payment">Płatność</string> <string name="payment">Płatność</string>
<string name="cashu">Cashu Token</string>
<string name="cashu_redeem">Wykup</string> <string name="cashu_redeem">Wykup</string>
<string name="cashu_redeem_to_zap">Wyślij do Zap Wallet</string> <string name="cashu_redeem_to_zap">Wyślij do Zap Wallet</string>
<string name="cashu_redeem_to_cashu">Otwórz w Portfelu Cashu</string> <string name="cashu_redeem_to_cashu">Otwórz w Portfelu Cashu</string>
@@ -507,6 +513,7 @@
<string name="poll_zap_value_min_max_explainer">Głosy są oceniane na podstawie ilości zapperów. Możesz ustawić minimalną kwotę, aby uniknąć spamerów i maksymalną kwotę, aby uniknąć przejęcia ankiety przez dużych zapperów. Użyj tej samej kwoty w obu polach, aby upewnić się, że każdy głos ma taką samą wartość. Pozostaw to pole puste, aby zaakceptować dowolną kwotę.</string> <string name="poll_zap_value_min_max_explainer">Głosy są oceniane na podstawie ilości zapperów. Możesz ustawić minimalną kwotę, aby uniknąć spamerów i maksymalną kwotę, aby uniknąć przejęcia ankiety przez dużych zapperów. Użyj tej samej kwoty w obu polach, aby upewnić się, że każdy głos ma taką samą wartość. Pozostaw to pole puste, aby zaakceptować dowolną kwotę.</string>
<string name="error_dialog_zap_error">Nie można wysłać zapa</string> <string name="error_dialog_zap_error">Nie można wysłać zapa</string>
<string name="error_dialog_talk_to_user">Wyślij wiadomość użytkownikowi</string> <string name="error_dialog_talk_to_user">Wyślij wiadomość użytkownikowi</string>
<string name="error_dialog_button_ok">OK</string>
<string name="relay_information_document_error_assemble_url">Nieudane połączenie z %1$s: %2$s</string> <string name="relay_information_document_error_assemble_url">Nieudane połączenie z %1$s: %2$s</string>
<string name="relay_information_document_error_failed_to_assemble_url">Nie udało się skompilować adresu URL NIP-11 dla %1$s: %2$s</string> <string name="relay_information_document_error_failed_to_assemble_url">Nie udało się skompilować adresu URL NIP-11 dla %1$s: %2$s</string>
<string name="relay_information_document_error_failed_to_reach_server">Nieudane połączenie z %1$s: %2$s</string> <string name="relay_information_document_error_failed_to_reach_server">Nieudane połączenie z %1$s: %2$s</string>
@@ -527,6 +534,7 @@
<string name="missing_lud16">Brakująca konfiguracja LN</string> <string name="missing_lud16">Brakująca konfiguracja LN</string>
<string name="user_x_does_not_have_a_lightning_address_setup_to_receive_sats">Użytkownik %1$s nie ma skonfigurowanego adresu LN, aby odbierać saty</string> <string name="user_x_does_not_have_a_lightning_address_setup_to_receive_sats">Użytkownik %1$s nie ma skonfigurowanego adresu LN, aby odbierać saty</string>
<string name="zap_split_weight">Procentowo</string> <string name="zap_split_weight">Procentowo</string>
<string name="zap_split_weight_placeholder">25</string>
<string name="splitting_zaps_with">Podziel zapsy z</string> <string name="splitting_zaps_with">Podziel zapsy z</string>
<string name="forwarding_zaps_to">Przesyłanie zapasów do</string> <string name="forwarding_zaps_to">Przesyłanie zapasów do</string>
<string name="lightning_wallets_not_found2">Nie znaleziono portfeli Lightning</string> <string name="lightning_wallets_not_found2">Nie znaleziono portfeli Lightning</string>
@@ -593,9 +601,11 @@
<string name="hi_there_is_this_still_available">Cześć, czy to jest nadal dostępne?</string> <string name="hi_there_is_this_still_available">Cześć, czy to jest nadal dostępne?</string>
<string name="classifieds">Sprzedaj przedmiot</string> <string name="classifieds">Sprzedaj przedmiot</string>
<string name="classifieds_title">Tytuł</string> <string name="classifieds_title">Tytuł</string>
<string name="classifieds_title_placeholder">iPhone 13</string>
<string name="classifieds_condition">Stan</string> <string name="classifieds_condition">Stan</string>
<string name="classifieds_category">Kategoria</string> <string name="classifieds_category">Kategoria</string>
<string name="classifieds_price">Cena (w Satach)</string> <string name="classifieds_price">Cena (w Satach)</string>
<string name="classifieds_price_placeholder">1000</string>
<string name="classifieds_location">Lokalizacja</string> <string name="classifieds_location">Lokalizacja</string>
<string name="classifieds_location_placeholder">Miasto, Województwo, Kraj</string> <string name="classifieds_location_placeholder">Miasto, Województwo, Kraj</string>
<string name="classifieds_condition_new">Nowość</string> <string name="classifieds_condition_new">Nowość</string>
@@ -614,6 +624,7 @@
<string name="classifieds_category_books">Książki</string> <string name="classifieds_category_books">Książki</string>
<string name="classifieds_category_pets">Zwierzęta domowe</string> <string name="classifieds_category_pets">Zwierzęta domowe</string>
<string name="classifieds_category_sports">Sporty</string> <string name="classifieds_category_sports">Sporty</string>
<string name="classifieds_category_fitness">Fitness</string>
<string name="classifieds_category_art">Sztuka</string> <string name="classifieds_category_art">Sztuka</string>
<string name="classifieds_category_crafts">Rzemiosło</string> <string name="classifieds_category_crafts">Rzemiosło</string>
<string name="classifieds_category_home">AGD</string> <string name="classifieds_category_home">AGD</string>
@@ -628,6 +639,8 @@
<string name="server_did_not_provide_a_url_after_uploading">Serwer nie podał adresu URL po załadowaniu</string> <string name="server_did_not_provide_a_url_after_uploading">Serwer nie podał adresu URL po załadowaniu</string>
<string name="could_not_download_from_the_server">Nie można pobrać przesłanych plików z serwera</string> <string name="could_not_download_from_the_server">Nie można pobrać przesłanych plików z serwera</string>
<string name="could_not_prepare_local_file_to_upload">Nie można przygotować pliku lokalnego do przesłania: %1$s</string> <string name="could_not_prepare_local_file_to_upload">Nie można przygotować pliku lokalnego do przesłania: %1$s</string>
<string name="failed_to_upload_with_message">Nie udało się przesłać: %1$s</string>
<string name="failed_to_delete_with_message">Nie udało się usunąć: %1$s</string>
<string name="edit_draft">Edytuj wersję roboczą</string> <string name="edit_draft">Edytuj wersję roboczą</string>
<string name="login_with_qr_code">Zaloguj się przy użyciu QR kodu</string> <string name="login_with_qr_code">Zaloguj się przy użyciu QR kodu</string>
<string name="route">Ścieżka</string> <string name="route">Ścieżka</string>
@@ -647,6 +660,7 @@
<string name="reply_description">Odpowiedz</string> <string name="reply_description">Odpowiedz</string>
<string name="boost_or_quote_description">Zrepostuj lub Zacytuj</string> <string name="boost_or_quote_description">Zrepostuj lub Zacytuj</string>
<string name="like_description">Lubię</string> <string name="like_description">Lubię</string>
<string name="zap_description">Zap</string>
<string name="change_reaction">Zmień odzew</string> <string name="change_reaction">Zmień odzew</string>
<string name="profile_image_of_user">Zdjęcie profilowe %1$s</string> <string name="profile_image_of_user">Zdjęcie profilowe %1$s</string>
<string name="relay_info">Transmiter %1$s</string> <string name="relay_info">Transmiter %1$s</string>
@@ -658,6 +672,7 @@
<string name="add_bitcoin_invoice">Faktura Bitcoin</string> <string name="add_bitcoin_invoice">Faktura Bitcoin</string>
<string name="cancel_bitcoin_invoice">Anuluj fakturę Bitcoin</string> <string name="cancel_bitcoin_invoice">Anuluj fakturę Bitcoin</string>
<string name="cancel_classifieds">Anuluj sprzedaż przedmiotu</string> <string name="cancel_classifieds">Anuluj sprzedaż przedmiotu</string>
<string name="add_zapraiser">Zapraiser</string>
<string name="cancel_zapraiser">Anuluj Zapraiser</string> <string name="cancel_zapraiser">Anuluj Zapraiser</string>
<string name="add_location">Lokalizacja</string> <string name="add_location">Lokalizacja</string>
<string name="remove_location">Usuń lokalizację</string> <string name="remove_location">Usuń lokalizację</string>
@@ -710,6 +725,7 @@
<string name="git_repository">Repozytorium Git: %1$s</string> <string name="git_repository">Repozytorium Git: %1$s</string>
<string name="git_web_address">Strona internetowa:</string> <string name="git_web_address">Strona internetowa:</string>
<string name="git_clone_address">Klonuj:</string> <string name="git_clone_address">Klonuj:</string>
<string name="existed_since">OTS: %1$s</string>
<string name="ots_info_title">Potwierdzenie znacznika czasu</string> <string name="ots_info_title">Potwierdzenie znacznika czasu</string>
<string name="ots_info_description">Istnieje dowód na to, że ten post został podpisany przed %1$s. Dowód został opatrzony pieczęcią w łańcuchu bloków Bitcoin w tym dniu i czasie.</string> <string name="ots_info_description">Istnieje dowód na to, że ten post został podpisany przed %1$s. Dowód został opatrzony pieczęcią w łańcuchu bloków Bitcoin w tym dniu i czasie.</string>
<string name="edit_post">Edytuj wpis</string> <string name="edit_post">Edytuj wpis</string>
@@ -737,4 +753,27 @@
<string name="dvm_requesting_job">Zapytanie o pracę z DVM</string> <string name="dvm_requesting_job">Zapytanie o pracę z DVM</string>
<string name="nwc_payment_request">Prośba o płatność została wysłana, oczekiwanie na potwierdzenie z Twojego portfela</string> <string name="nwc_payment_request">Prośba o płatność została wysłana, oczekiwanie na potwierdzenie z Twojego portfela</string>
<string name="dvm_waiting_to_confirm_payment">Oczekiwanie na potwierdzenie płatności lub wysłanie wyników DVM</string> <string name="dvm_waiting_to_confirm_payment">Oczekiwanie na potwierdzenie płatności lub wysłanie wyników DVM</string>
<string name="http_status_400">Błędne żądanie-Serwer nie może lub nie chce przetworzyć żądania.</string>
<string name="http_status_401">Nieautoryzowany - użytkownik nie ma prawidłowych danych uwierzytelniających</string>
<string name="http_status_402">Wymagana płatność - serwer wymaga płatności, aby wypełnić żądanie</string>
<string name="http_status_403">Zabronione - użytkownik nie ma praw dostępu, aby złożyć żądanie</string>
<string name="http_status_404">Nie znaleziono - serwer nie może odnaleźć żądanego adresu</string>
<string name="http_status_405">Metoda niedozwolona - serwer obsługuje metodę żądania, ale nie docelowy zasób</string>
<string name="http_status_406">Niedopuszczalne - Serwer nie znalazł żadnej zawartości spełniającej żądanie.</string>
<string name="http_status_407">Wymagane uwierzytelnianie proxy - użytkownik nie ma prawidłowych danych uwierzytelniania</string>
<string name="http_status_408">Limit czasu żądania - serwer przekroczył limit czasu, oczekując na kogoś innego</string>
<string name="http_status_409">Konflikt - serwer nie może spełnić żądania, ponieważ istnieje konflikt z zasobem</string>
<string name="http_status_410">Przepadło - Żądana zawartość została trwale usunięta z serwera i nie zostanie przywrócona</string>
<string name="http_status_411">Wymagana długość - serwer odrzuca żądanie, ponieważ wymaga zdefiniowanej</string>
<string name="http_status_414">URI Zbyt długi - URL klienta jest zbyt długi, aby serwer mógł go przetworzyć. </string>
<string name="http_status_415">Nieobsługiwany format plików - żądanie używa formatu multimediów, którego serwer nie obsługuje</string>
<string name="http_status_426">Wymagana aktualizacja - serwer odmawia przetwarzania żądania przy użyciu bieżącego protokołu, chyba że klient uaktualni go do innego protokołu.</string>
<string name="http_status_500">Wewnętrzny błąd serwera - serwer napotkał nieoczekiwany błąd i nie może zakończyć żądania</string>
<string name="http_status_501">Nie zaimplementowano - serwer nie może spełnić żądania lub nie rozpoznaje metody żądania</string>
<string name="http_status_503">Usługa niedostępna - Często zdarza się, gdy serwer jest przeciążony lub wyłączony w celu konserwacji</string>
<string name="http_status_505">Wersja HTTP nie jest obsługiwana - serwer nie obsługuje wersji HTTP w zapytaniu</string>
<string name="http_status_506">Wariant również Negocjacje - serwer ma błąd konfiguracji wewnętrznej</string>
<string name="http_status_507">Niewystarczająca pojemność - serwer nie ma wystarczającej pojemność, aby pomyślnie przetworzyć żądanie</string>
<string name="http_status_508">Wykryta pętla - serwer wykrywa nieskończoną pętlę podczas przetwarzania żądania</string>
<string name="http_status_511">Wymagane uwierzytelnienie sieciowe - Klient musi być uwierzytelniony aby uzyskać dostęp do sieci</string>
</resources> </resources>
@@ -103,6 +103,7 @@
<string name="add_a_relay">添加中继器</string> <string name="add_a_relay">添加中继器</string>
<string name="display_name">显示名称</string> <string name="display_name">显示名称</string>
<string name="my_display_name">我的显示名称</string> <string name="my_display_name">我的显示名称</string>
<string name="my_awesome_name">Nostr 太酷了</string>
<string name="welcome">欢迎!</string> <string name="welcome">欢迎!</string>
<string name="username">用户名</string> <string name="username">用户名</string>
<string name="my_username">我的用户名</string> <string name="my_username">我的用户名</string>
@@ -386,6 +387,7 @@
<string name="warn_when_posts_have_reports_from_your_follows">当帖子有你的关注的报告时警告</string> <string name="warn_when_posts_have_reports_from_your_follows">当帖子有你的关注的报告时警告</string>
<string name="new_reaction_symbol">新回应符号</string> <string name="new_reaction_symbol">新回应符号</string>
<string name="no_reaction_type_setup_long_press_to_change">未选择回应类型。长按可更改</string> <string name="no_reaction_type_setup_long_press_to_change">未选择回应类型。长按可更改</string>
<string name="zapraiser">Zapraiser</string>
<string name="zapraiser_explainer">为这个帖子添加目标聪金额。支持的客户端可以将此显示为奖励捐赠的进度条</string> <string name="zapraiser_explainer">为这个帖子添加目标聪金额。支持的客户端可以将此显示为奖励捐赠的进度条</string>
<string name="zapraiser_target_amount_in_sats">目标聪金额</string> <string name="zapraiser_target_amount_in_sats">目标聪金额</string>
<string name="sats_to_complete">Zapraiser 位于 %1$s。距离目标%2$s聪</string> <string name="sats_to_complete">Zapraiser 位于 %1$s。距离目标%2$s聪</string>
@@ -671,6 +673,7 @@
<string name="add_bitcoin_invoice">比特币发票</string> <string name="add_bitcoin_invoice">比特币发票</string>
<string name="cancel_bitcoin_invoice">取消比特币发票</string> <string name="cancel_bitcoin_invoice">取消比特币发票</string>
<string name="cancel_classifieds">取消出售物品</string> <string name="cancel_classifieds">取消出售物品</string>
<string name="add_zapraiser">Zapraiser</string>
<string name="cancel_zapraiser">取消 Zapraiser</string> <string name="cancel_zapraiser">取消 Zapraiser</string>
<string name="add_location">地点</string> <string name="add_location">地点</string>
<string name="remove_location">移除地点</string> <string name="remove_location">移除地点</string>
+35
View File
@@ -270,6 +270,8 @@
<string name="quick_action_delete">Delete</string> <string name="quick_action_delete">Delete</string>
<string name="quick_action_unfollow">Unfollow</string> <string name="quick_action_unfollow">Unfollow</string>
<string name="quick_action_follow">Follow</string> <string name="quick_action_follow">Follow</string>
<string name="quick_action_request_deletion_gallery_title">Delete from Gallery</string>
<string name="quick_action_request_deletion_gallery_alert_body">Remove this media from your Gallery, you can readd it later</string>
<string name="quick_action_request_deletion_alert_title">Request Deletion</string> <string name="quick_action_request_deletion_alert_title">Request Deletion</string>
<string name="quick_action_request_deletion_alert_body">Amethyst will request that your note be deleted from the relays you are currently connected to. There is no guarantee that your note will be permanently deleted from those relays, or from other relays where it may be stored.</string> <string name="quick_action_request_deletion_alert_body">Amethyst will request that your note be deleted from the relays you are currently connected to. There is no guarantee that your note will be permanently deleted from those relays, or from other relays where it may be stored.</string>
<string name="quick_action_block_dialog_btn">Block</string> <string name="quick_action_block_dialog_btn">Block</string>
@@ -761,6 +763,8 @@
<string name="server_did_not_provide_a_url_after_uploading">Server did not provide a URL after uploading</string> <string name="server_did_not_provide_a_url_after_uploading">Server did not provide a URL after uploading</string>
<string name="could_not_download_from_the_server">Could not download uploaded media from the server</string> <string name="could_not_download_from_the_server">Could not download uploaded media from the server</string>
<string name="could_not_prepare_local_file_to_upload">Could not prepare local file to upload: %1$s</string> <string name="could_not_prepare_local_file_to_upload">Could not prepare local file to upload: %1$s</string>
<string name="failed_to_upload_with_message">Failed to upload: %1$s</string>
<string name="failed_to_delete_with_message">Failed to delete: %1$s</string>
<string name="edit_draft">Edit draft</string> <string name="edit_draft">Edit draft</string>
@@ -896,4 +900,35 @@
<string name="dvm_requesting_job">Requesting Job from DVM</string> <string name="dvm_requesting_job">Requesting Job from DVM</string>
<string name="nwc_payment_request">Payment request sent, waiting for confirmation from your wallet</string> <string name="nwc_payment_request">Payment request sent, waiting for confirmation from your wallet</string>
<string name="dvm_waiting_to_confirm_payment">Waiting for DVM to confirm payment or send results</string> <string name="dvm_waiting_to_confirm_payment">Waiting for DVM to confirm payment or send results</string>
<string name="http_status_400">Bad Request - The server cant or wont process the request.</string>
<string name="http_status_401">Unauthorized - The user doesnt have valid authentication credentials</string>
<string name="http_status_402">Payment Required - The server requires payment to complete the request</string>
<string name="http_status_403">Forbidden - The user doesnt have access rights to make the request</string>
<string name="http_status_404">Not Found - The server cant find the requested address</string>
<string name="http_status_405">Method Not Allowed - The server supports the request method, but not the target resource</string>
<string name="http_status_406">Not Acceptable - The server doesnt find any content that satisfies the request.</string>
<string name="http_status_407">Proxy Authentication Required - The user doesnt have valid authentication credentials</string>
<string name="http_status_408">Request Timeout - The server timed out waiting for somebody else</string>
<string name="http_status_409">Conflict - The server cant fulfill the request because theres a conflict with the resource</string>
<string name="http_status_410">Gone - The content requested has been permanently deleted from the server and will not be reinstated</string>
<string name="http_status_411">Length Required - The server rejects the request because it requires a defined</string>
<string name="http_status_412">Precondition Failed - The request\'s preconditions in the header fields that the server fails to meet</string>
<string name="http_status_413">Payload Too Large - The request is larger than the servers defined limits, and the server refuses to process it</string>
<string name="http_status_414">URI Too Long - The url requested by the client is too long for the server to process. </string>
<string name="http_status_415">Unsupported Media Type - The request uses a media format the server does not support</string>
<string name="http_status_416">Range Not Satisfiable - The server cant fulfill the value indicated in the requests Range header field. </string>
<string name="http_status_417">Expectation Failed - The server cant meet the requirements indicated by the Expect request header field</string>
<string name="http_status_426">Upgrade Required - The server refuses to process the request using the current protocol unless the client upgrades to a different protocol.</string>
<string name="http_status_500">Internal Server Error - The server has encountered an unexpected error and cannot complete the request</string>
<string name="http_status_501">Not Implemented - The server cant fulfill the request or doesnt recognize the request method</string>
<string name="http_status_502">Bad Gateway - The server acts as a gateway and got an invalid response from the host</string>
<string name="http_status_503">Service Unavailable - This often occurs when a server is overloaded or down for maintenance</string>
<string name="http_status_504">Gateway Timeout - The server was acting as a gateway or proxy and timed out, waiting for a response</string>
<string name="http_status_505">HTTP Version Not Supported - The server doesnt support the HTTP version in the request</string>
<string name="http_status_506">Variant Also Negotiates - The server has an internal configuration error</string>
<string name="http_status_507">Insufficient Storage - The server doesnt have enough storage to process the request successfully</string>
<string name="http_status_508">Loop Detected - The server detects an infinite loop while processing the request</string>
<string name="http_status_511">Network Authentication Required - The client must be authenticated to access the network</string>
</resources> </resources>
@@ -72,3 +72,21 @@ data class EmojiUrl(val code: String, val url: String) {
} }
} }
} }
@Immutable
data class GalleryUrl(val id: String, val url: String) {
fun encode(): String {
return ":$id:$url"
}
companion object {
fun decode(encodedGallerySetup: String): EmojiUrl? {
val emojiParts = encodedGallerySetup.split(":", limit = 3)
return if (emojiParts.size > 2) {
EmojiUrl(emojiParts[1], emojiParts[2])
} else {
null
}
}
}
}
@@ -119,6 +119,8 @@ open class Event(
override fun taggedEvents() = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] } override fun taggedEvents() = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] }
override fun taggedGalleryEntries() = tags.filter { it.size > 2 && it[0] == "g" }.map { GalleryUrl(it[1], it[2]) }
override fun taggedUrls() = tags.filter { it.size > 1 && it[0] == "r" }.map { it[1] } override fun taggedUrls() = tags.filter { it.size > 1 && it[0] == "r" }.map { it[1] }
override fun firstTagFor(vararg key: String) = tags.firstOrNull { it.size > 1 && it[0] in key }?.let { it[1] } override fun firstTagFor(vararg key: String) = tags.firstOrNull { it.size > 1 && it[0] in key }?.let { it[1] }
@@ -145,6 +145,8 @@ interface EventInterface {
fun firstTaggedK(): Int? fun firstTaggedK(): Int?
fun taggedGalleryEntries(): List<GalleryUrl>
fun taggedEmojis(): List<EmojiUrl> fun taggedEmojis(): List<EmojiUrl>
fun matchTag1With(text: String): Boolean fun matchTag1With(text: String): Boolean
@@ -60,7 +60,7 @@ class GalleryListEvent(
) { ) {
add( add(
earlierVersion, earlierVersion,
arrayOf(arrayOf(tagName, url, tagValue)), arrayOf(arrayOf(tagName, tagValue, url)),
signer, signer,
createdAt, createdAt,
onReady, onReady,
@@ -76,7 +76,7 @@ class GalleryListEvent(
) { ) {
create( create(
content = earlierVersion?.content ?: "", content = earlierVersion?.content ?: "",
tags = (earlierVersion?.tags ?: arrayOf(arrayOf("d", DEFAULT_D_TAG_GALLERY))).plus(listNewTags), tags = listNewTags.plus(earlierVersion?.tags ?: arrayOf(arrayOf("d", DEFAULT_D_TAG_GALLERY))),
signer = signer, signer = signer,
createdAt = createdAt, createdAt = createdAt,
onReady = onReady, onReady = onReady,
@@ -86,23 +86,26 @@ class GalleryListEvent(
fun removeEvent( fun removeEvent(
earlierVersion: GalleryListEvent, earlierVersion: GalleryListEvent,
eventId: HexKey, eventId: HexKey,
url: String,
signer: NostrSigner, signer: NostrSigner,
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
onReady: (GalleryListEvent) -> Unit, onReady: (GalleryListEvent) -> Unit,
) = removeTag(earlierVersion, "e", eventId, signer, createdAt, onReady) ) = removeTag(earlierVersion, "g", eventId, url, signer, createdAt, onReady)
fun removeReplaceable( fun removeReplaceable(
earlierVersion: GalleryListEvent, earlierVersion: GalleryListEvent,
aTag: ATag, aTag: ATag,
url: String,
signer: NostrSigner, signer: NostrSigner,
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
onReady: (GalleryListEvent) -> Unit, onReady: (GalleryListEvent) -> Unit,
) = removeTag(earlierVersion, "a", aTag.toTag(), signer, createdAt, onReady) ) = removeTag(earlierVersion, "g", aTag.toTag(), url, signer, createdAt, onReady)
private fun removeTag( private fun removeTag(
earlierVersion: GalleryListEvent, earlierVersion: GalleryListEvent,
tagName: String, tagName: String,
tagValue: HexKey, tagValue: HexKey,
url: String,
signer: NostrSigner, signer: NostrSigner,
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
onReady: (GalleryListEvent) -> Unit, onReady: (GalleryListEvent) -> Unit,
@@ -111,7 +114,7 @@ class GalleryListEvent(
content = earlierVersion.content, content = earlierVersion.content,
tags = tags =
earlierVersion.tags earlierVersion.tags
.filter { it.size <= 1 || !(it[0] == tagName && it[1] == tagValue) } .filter { it.size <= 1 || !(it[0] == tagName && it[1] == tagValue && it[2] == url) }
.toTypedArray(), .toTypedArray(),
signer = signer, signer = signer,
createdAt = createdAt, createdAt = createdAt,
@@ -135,30 +138,5 @@ class GalleryListEvent(
signer.sign(createdAt, KIND, newTags, content, onReady) signer.sign(createdAt, KIND, newTags, content, onReady)
} }
fun create(
name: String = "",
images: List<String>? = null,
videos: List<String>? = null,
audios: List<String>? = null,
privEvents: List<String>? = null,
privUsers: List<String>? = null,
privAddresses: List<ATag>? = null,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (GalleryListEvent) -> Unit,
) {
val tags = mutableListOf<Array<String>>()
tags.add(arrayOf("d", name))
images?.forEach { tags.add(arrayOf("g", it)) }
videos?.forEach { tags.add(arrayOf("g", it)) }
audios?.forEach { tags.add(arrayOf("g", it)) }
tags.add(arrayOf("alt", ALT))
createPrivateTags(privEvents, privUsers, privAddresses, signer) { content ->
signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady)
}
}
} }
} }