@@ -99,7 +99,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
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_content_type: application/zip
|
||||
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
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_content_type: application/zip
|
||||
|
||||
@@ -121,7 +121,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
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_content_type: application/zip
|
||||
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
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_content_type: application/zip
|
||||
|
||||
@@ -143,7 +143,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
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_content_type: application/zip
|
||||
|
||||
@@ -155,7 +155,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
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_content_type: application/zip
|
||||
|
||||
@@ -166,7 +166,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
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_content_type: application/zip
|
||||
|
||||
@@ -177,7 +177,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
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_content_type: application/zip
|
||||
|
||||
@@ -188,7 +188,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
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_content_type: application/zip
|
||||
|
||||
@@ -199,7 +199,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
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_content_type: application/zip
|
||||
|
||||
@@ -213,7 +213,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
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_content_type: application/zip
|
||||
|
||||
@@ -225,6 +225,6 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
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_content_type: application/zip
|
||||
|
||||
@@ -12,8 +12,8 @@ android {
|
||||
applicationId "com.vitorpamplona.amethyst"
|
||||
minSdk 26
|
||||
targetSdk 34
|
||||
versionCode 381
|
||||
versionName "0.88.4"
|
||||
versionCode 382
|
||||
versionName "0.88.5"
|
||||
buildConfigField "String", "RELEASE_NOTES_ID", "\"2a34cbadd03212c8162e1ff896ba12641821088a2ec8d5e40d54aa80c0510800\""
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -2203,6 +2203,7 @@ class Account(
|
||||
url: String,
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
GalleryListEvent.addEvent(
|
||||
userProfile().latestGalleryList,
|
||||
idHex,
|
||||
@@ -2226,7 +2227,7 @@ class Account(
|
||||
GalleryListEvent.removeReplaceable(
|
||||
galleryentries,
|
||||
note.address,
|
||||
false,
|
||||
url,
|
||||
signer,
|
||||
) {
|
||||
Client.send(it)
|
||||
@@ -2236,7 +2237,7 @@ class Account(
|
||||
GalleryListEvent.removeEvent(
|
||||
galleryentries,
|
||||
note.idHex,
|
||||
false,
|
||||
url,
|
||||
signer,
|
||||
) {
|
||||
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
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import android.webkit.MimeTypeMap
|
||||
@@ -29,7 +30,9 @@ import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||
import kotlinx.coroutines.delay
|
||||
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("")
|
||||
|
||||
class Nip96Uploader(val account: Account?) {
|
||||
class Nip96Uploader(
|
||||
val account: Account?,
|
||||
) {
|
||||
suspend fun uploadImage(
|
||||
uri: Uri,
|
||||
contentType: String?,
|
||||
@@ -58,6 +63,7 @@ class Nip96Uploader(val account: Account?) {
|
||||
server: Nip96MediaServers.ServerName,
|
||||
contentResolver: ContentResolver,
|
||||
onProgress: (percentage: Float) -> Unit,
|
||||
context: Context,
|
||||
): PartialEvent {
|
||||
val serverInfo =
|
||||
Nip96Retriever()
|
||||
@@ -74,6 +80,7 @@ class Nip96Uploader(val account: Account?) {
|
||||
serverInfo,
|
||||
contentResolver,
|
||||
onProgress,
|
||||
context,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -86,6 +93,7 @@ class Nip96Uploader(val account: Account?) {
|
||||
server: Nip96Retriever.ServerInfo,
|
||||
contentResolver: ContentResolver,
|
||||
onProgress: (percentage: Float) -> Unit,
|
||||
context: Context,
|
||||
): PartialEvent {
|
||||
checkNotInMainThread()
|
||||
|
||||
@@ -111,6 +119,7 @@ class Nip96Uploader(val account: Account?) {
|
||||
sensitiveContent,
|
||||
server,
|
||||
onProgress,
|
||||
context,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -122,6 +131,7 @@ class Nip96Uploader(val account: Account?) {
|
||||
sensitiveContent: String?,
|
||||
server: Nip96Retriever.ServerInfo,
|
||||
onProgress: (percentage: Float) -> Unit,
|
||||
context: Context,
|
||||
): PartialEvent {
|
||||
checkNotInMainThread()
|
||||
|
||||
@@ -130,11 +140,11 @@ class Nip96Uploader(val account: Account?) {
|
||||
contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: ""
|
||||
|
||||
val client = HttpClientManager.getHttpClient()
|
||||
val requestBody: RequestBody
|
||||
val requestBuilder = Request.Builder()
|
||||
|
||||
requestBody =
|
||||
MultipartBody.Builder()
|
||||
val requestBody: RequestBody =
|
||||
MultipartBody
|
||||
.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("expiration", "")
|
||||
.addFormDataPart("size", length.toString())
|
||||
@@ -142,8 +152,7 @@ class Nip96Uploader(val account: Account?) {
|
||||
alt?.let { body.addFormDataPart("alt", it) }
|
||||
sensitiveContent?.let { body.addFormDataPart("content-warning", it) }
|
||||
contentType?.let { body.addFormDataPart("content_type", it) }
|
||||
}
|
||||
.addFormDataPart(
|
||||
}.addFormDataPart(
|
||||
"file",
|
||||
"$fileName.$extension",
|
||||
object : RequestBody() {
|
||||
@@ -155,8 +164,7 @@ class Nip96Uploader(val account: Account?) {
|
||||
inputStream.source().use(sink::writeAll)
|
||||
}
|
||||
},
|
||||
)
|
||||
.build()
|
||||
).build()
|
||||
|
||||
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) {
|
||||
return result.nip94Event
|
||||
} else {
|
||||
throw RuntimeException("Failed to upload with message: ${result.message}")
|
||||
throw RuntimeException(stringRes(context, R.string.failed_to_upload_with_message, result.message))
|
||||
}
|
||||
}
|
||||
} 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,
|
||||
contentType: String?,
|
||||
server: Nip96Retriever.ServerInfo,
|
||||
context: Context,
|
||||
): Boolean {
|
||||
val extension =
|
||||
contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: ""
|
||||
@@ -216,7 +230,12 @@ class Nip96Uploader(val account: Account?) {
|
||||
return result.status == "success"
|
||||
}
|
||||
} 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)
|
||||
|
||||
val request: Request =
|
||||
Request.Builder()
|
||||
Request
|
||||
.Builder()
|
||||
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
||||
.url(result.processingUrl)
|
||||
.build()
|
||||
@@ -257,13 +277,12 @@ class Nip96Uploader(val account: Account?) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun nip98Header(url: String): String? {
|
||||
return withTimeoutOrNull(5000) {
|
||||
suspend fun nip98Header(url: String): String? =
|
||||
withTimeoutOrNull(5000) {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
nip98Header(url, "POST") { authorizationToken -> continuation.resume(authorizationToken) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun nip98Header(
|
||||
url: String,
|
||||
|
||||
@@ -118,7 +118,8 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
|
||||
)
|
||||
|
||||
fun createOtherAccountsBaseFilter(): TypedFilter? {
|
||||
if (otherAccounts.isEmpty()) return null
|
||||
val otherAuthors = otherAccounts.filter { it != account.userProfile().pubkeyHex }
|
||||
if (otherAuthors.isEmpty()) return null
|
||||
return TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
filter =
|
||||
@@ -134,7 +135,7 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
|
||||
MuteListEvent.KIND,
|
||||
PeopleListEvent.KIND,
|
||||
),
|
||||
authors = otherAccounts.filter { it != account.userProfile().pubkeyHex },
|
||||
authors = otherAuthors,
|
||||
limit = 100,
|
||||
),
|
||||
)
|
||||
|
||||
+22
-3
@@ -205,38 +205,54 @@ class EventNotificationConsumer(
|
||||
event: LnZapEvent,
|
||||
acc: Account,
|
||||
) {
|
||||
Log.d("EventNotificationConsumer", "Notify Start ${event.toNostrUri()}")
|
||||
val noteZapEvent = LocalCache.getNoteIfExists(event.id) ?: return
|
||||
|
||||
Log.d("EventNotificationConsumer", "Notify Not Notified Yet")
|
||||
|
||||
// old event being re-broadcast
|
||||
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 noteZapped =
|
||||
event.zappedPost().firstOrNull()?.let { LocalCache.checkGetOrCreateNote(it) } ?: return
|
||||
|
||||
Log.d("EventNotificationConsumer", "Notify ZapRequest $noteZapRequest zapped $noteZapped")
|
||||
|
||||
if ((event.amount ?: BigDecimal.ZERO) < BigDecimal.TEN) return
|
||||
|
||||
Log.d("EventNotificationConsumer", "Notify Amount Bigger than 10")
|
||||
|
||||
if (event.isTaggedUser(acc.userProfile().pubkeyHex)) {
|
||||
val amount = showAmount(event.amount)
|
||||
|
||||
Log.d("EventNotificationConsumer", "Notify Amount $amount")
|
||||
|
||||
(noteZapRequest.event as? LnZapRequestEvent)?.let { event ->
|
||||
acc.decryptZapContentAuthor(noteZapRequest) {
|
||||
Log.d("EventNotificationConsumer", "Notify Decrypted if Private Zap ${event.id}")
|
||||
|
||||
val author = LocalCache.getOrCreateUser(it.pubKey)
|
||||
val senderInfo = Pair(author, it.content.ifBlank { null })
|
||||
|
||||
acc.decryptContent(noteZapped) {
|
||||
Log.d("EventNotificationConsumer", "Notify Decrypted if Private Note")
|
||||
|
||||
val zappedContent = it.split("\n").get(0)
|
||||
|
||||
val user = senderInfo.first.toBestDisplayName()
|
||||
var title =
|
||||
stringRes(applicationContext, R.string.app_notification_zaps_channel_message, amount)
|
||||
var title = stringRes(applicationContext, R.string.app_notification_zaps_channel_message, amount)
|
||||
senderInfo.second?.ifBlank { null }?.let { title += " ($it)" }
|
||||
|
||||
var content =
|
||||
stringRes(
|
||||
applicationContext,
|
||||
R.string.app_notification_zaps_channel_message_from,
|
||||
user,
|
||||
)
|
||||
zappedContent?.let {
|
||||
zappedContent.let {
|
||||
content +=
|
||||
" " +
|
||||
stringRes(
|
||||
@@ -247,6 +263,9 @@ class EventNotificationConsumer(
|
||||
}
|
||||
val userPicture = senderInfo?.first?.profilePicture()
|
||||
val noteUri = "nostr:Notifications"
|
||||
|
||||
Log.d("EventNotificationConsumer", "Notify ${event.id} $content $title $noteUri")
|
||||
|
||||
notificationManager()
|
||||
.sendZapNotification(
|
||||
event.id,
|
||||
|
||||
@@ -73,7 +73,7 @@ fun stringRes(
|
||||
return String
|
||||
.format(
|
||||
res.configuration.locales.get(0),
|
||||
resourceCache.get(id) ?: res.getString(id),
|
||||
resourceCache.get(id) ?: res.getString(id).also { resourceCache.put(id, it) },
|
||||
*args,
|
||||
).also { resourceCache.put(id, it) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Stable
|
||||
open class EditPostViewModel() : ViewModel() {
|
||||
open class EditPostViewModel : ViewModel() {
|
||||
var accountViewModel: AccountViewModel? = null
|
||||
var account: Account? = null
|
||||
|
||||
@@ -185,6 +185,7 @@ open class EditPostViewModel() : ViewModel() {
|
||||
server = server.server,
|
||||
contentResolver = contentResolver,
|
||||
onProgress = {},
|
||||
context = context,
|
||||
)
|
||||
|
||||
createNIP94Record(
|
||||
@@ -235,13 +236,12 @@ open class EditPostViewModel() : ViewModel() {
|
||||
NostrSearchEventOrUserDataSource.clear()
|
||||
}
|
||||
|
||||
open fun findUrlInMessage(): String? {
|
||||
return message.text.split('\n').firstNotNullOfOrNull { paragraph ->
|
||||
open fun findUrlInMessage(): String? =
|
||||
message.text.split('\n').firstNotNullOfOrNull { paragraph ->
|
||||
paragraph.split(' ').firstOrNull { word: String ->
|
||||
RichTextParser.isValidURL(word) || RichTextParser.isUrlWithoutScheme(word)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open fun updateMessage(it: TextFieldValue) {
|
||||
message = it
|
||||
@@ -249,14 +249,18 @@ open class EditPostViewModel() : ViewModel() {
|
||||
|
||||
if (it.selection.collapsed) {
|
||||
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
|
||||
userSuggestionsMainMessage = UserSuggestionAnchor.MAIN_MESSAGE
|
||||
if (lastWord.startsWith("@") && lastWord.length > 2) {
|
||||
NostrSearchEventOrUserDataSource.search(lastWord.removePrefix("@"))
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
userSuggestions =
|
||||
LocalCache.findUsersStartingWith(lastWord.removePrefix("@"))
|
||||
LocalCache
|
||||
.findUsersStartingWith(lastWord.removePrefix("@"))
|
||||
.sortedWith(compareBy({ account?.isFollowing(it) }, { it.toBestDisplayName() }, { it.pubkeyHex }))
|
||||
.reversed()
|
||||
}
|
||||
@@ -271,7 +275,10 @@ open class EditPostViewModel() : ViewModel() {
|
||||
userSuggestionAnchor?.let {
|
||||
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
|
||||
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 wordToInsert = "@${item.pubkeyNpub()}"
|
||||
|
||||
@@ -288,12 +295,11 @@ open class EditPostViewModel() : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun canPost(): Boolean {
|
||||
return message.text.isNotBlank() &&
|
||||
fun canPost(): Boolean =
|
||||
message.text.isNotBlank() &&
|
||||
!isUploadingImage &&
|
||||
!wantsInvoice &&
|
||||
contentToAddUrl == null
|
||||
}
|
||||
|
||||
suspend fun createNIP94Record(
|
||||
uploadingResult: Nip96Uploader.PartialEvent,
|
||||
@@ -304,11 +310,20 @@ open class EditPostViewModel() : ViewModel() {
|
||||
// Images don't seem to be ready immediately after upload
|
||||
val imageUrl = uploadingResult.tags?.firstOrNull { it.size > 1 && it[0] == "url" }?.get(1)
|
||||
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 =
|
||||
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 =
|
||||
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 =
|
||||
uploadingResult.tags
|
||||
?.firstOrNull { it.size > 1 && it[0] == "magnet" }
|
||||
|
||||
@@ -139,6 +139,7 @@ open class NewMediaModel : ViewModel() {
|
||||
onProgress = { percent: Float ->
|
||||
uploadingPercentage.value = 0.2f + (0.2f * percent)
|
||||
},
|
||||
context = context,
|
||||
)
|
||||
|
||||
createNIP94Record(
|
||||
|
||||
@@ -801,6 +801,7 @@ open class NewPostViewModel : ViewModel() {
|
||||
server = server.server,
|
||||
contentResolver = contentResolver,
|
||||
onProgress = {},
|
||||
context = context,
|
||||
)
|
||||
|
||||
createNIP94Record(
|
||||
|
||||
+1
@@ -184,6 +184,7 @@ class NewUserMetadataViewModel : ViewModel() {
|
||||
server = account.defaultFileServer,
|
||||
contentResolver = contentResolver,
|
||||
onProgress = {},
|
||||
context = context,
|
||||
)
|
||||
|
||||
val url = result.tags?.firstOrNull { it.size > 1 && it[0] == "url" }?.get(1)
|
||||
|
||||
@@ -142,7 +142,6 @@ fun LoadThumbAndThenVideoView(
|
||||
roundedCorner: Boolean,
|
||||
isFiniteHeight: Boolean,
|
||||
nostrUriCallback: String? = null,
|
||||
nostrIdCallback: String? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
onDialog: ((Boolean) -> Unit)? = null,
|
||||
) {
|
||||
@@ -177,7 +176,6 @@ fun LoadThumbAndThenVideoView(
|
||||
artworkUri = thumbUri,
|
||||
authorName = authorName,
|
||||
nostrUriCallback = nostrUriCallback,
|
||||
nostrIdCallback = nostrIdCallback,
|
||||
accountViewModel = accountViewModel,
|
||||
onDialog = onDialog,
|
||||
)
|
||||
@@ -192,7 +190,6 @@ fun LoadThumbAndThenVideoView(
|
||||
artworkUri = thumbUri,
|
||||
authorName = authorName,
|
||||
nostrUriCallback = nostrUriCallback,
|
||||
nostrIdCallback = nostrIdCallback,
|
||||
accountViewModel = accountViewModel,
|
||||
onDialog = onDialog,
|
||||
)
|
||||
@@ -214,7 +211,6 @@ fun VideoView(
|
||||
dimensions: String? = null,
|
||||
blurhash: String? = null,
|
||||
nostrUriCallback: String? = null,
|
||||
nostrIdCallback: String? = null,
|
||||
onDialog: ((Boolean) -> Unit)? = null,
|
||||
onControllerVisibilityChanged: ((Boolean) -> Unit)? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
@@ -256,7 +252,6 @@ fun VideoView(
|
||||
artworkUri = artworkUri,
|
||||
authorName = authorName,
|
||||
nostrUriCallback = nostrUriCallback,
|
||||
nostrIDCallback = nostrIdCallback,
|
||||
automaticallyStartPlayback = automaticallyStartPlayback,
|
||||
onControllerVisibilityChanged = onControllerVisibilityChanged,
|
||||
onDialog = onDialog,
|
||||
@@ -310,7 +305,6 @@ fun VideoView(
|
||||
artworkUri = artworkUri,
|
||||
authorName = authorName,
|
||||
nostrUriCallback = nostrUriCallback,
|
||||
nostrIDCallback = nostrIdCallback,
|
||||
automaticallyStartPlayback = automaticallyStartPlayback,
|
||||
onControllerVisibilityChanged = onControllerVisibilityChanged,
|
||||
onDialog = onDialog,
|
||||
@@ -335,7 +329,6 @@ fun VideoViewInner(
|
||||
artworkUri: String? = null,
|
||||
authorName: String? = null,
|
||||
nostrUriCallback: String? = null,
|
||||
nostrIDCallback: String? = null,
|
||||
automaticallyStartPlayback: State<Boolean>,
|
||||
onControllerVisibilityChanged: ((Boolean) -> Unit)? = null,
|
||||
onDialog: ((Boolean) -> Unit)? = null,
|
||||
@@ -357,7 +350,6 @@ fun VideoViewInner(
|
||||
roundedCorner = roundedCorner,
|
||||
isFiniteHeight = isFiniteHeight,
|
||||
nostrUriCallback = nostrUriCallback,
|
||||
nostrIDCallback = nostrIDCallback,
|
||||
waveform = waveform,
|
||||
keepPlaying = keepPlaying,
|
||||
automaticallyStartPlayback = automaticallyStartPlayback,
|
||||
@@ -705,7 +697,6 @@ private fun RenderVideoPlayer(
|
||||
roundedCorner: Boolean,
|
||||
isFiniteHeight: Boolean,
|
||||
nostrUriCallback: String?,
|
||||
nostrIDCallback: String?,
|
||||
waveform: ImmutableList<Int>? = null,
|
||||
keepPlaying: MutableState<Boolean>,
|
||||
automaticallyStartPlayback: State<Boolean>,
|
||||
@@ -819,7 +810,7 @@ private fun RenderVideoPlayer(
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-10
@@ -95,6 +95,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size75dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.hashVerifierMark
|
||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.encoders.Nip19Bech32
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
@@ -138,7 +139,6 @@ fun ZoomableContentView(
|
||||
roundedCorner = roundedCorner,
|
||||
isFiniteHeight = isFiniteHeight,
|
||||
nostrUriCallback = content.uri,
|
||||
nostrIdCallback = content.id,
|
||||
onDialog = { dialogOpen = true },
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
@@ -163,7 +163,6 @@ fun ZoomableContentView(
|
||||
roundedCorner = roundedCorner,
|
||||
isFiniteHeight = isFiniteHeight,
|
||||
nostrUriCallback = content.uri,
|
||||
nostrIdCallback = content.id,
|
||||
onDialog = { dialogOpen = true },
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
@@ -609,7 +608,6 @@ fun ShareImageAction(
|
||||
popupExpanded = popupExpanded,
|
||||
videoUri = content.url,
|
||||
postNostrUri = content.uri,
|
||||
postNostrid = content.id,
|
||||
onDismiss = onDismiss,
|
||||
)
|
||||
} else if (content is MediaPreloadedContent) {
|
||||
@@ -618,7 +616,6 @@ fun ShareImageAction(
|
||||
popupExpanded = popupExpanded,
|
||||
videoUri = content.localFile?.toUri().toString(),
|
||||
postNostrUri = content.uri,
|
||||
postNostrid = content.id,
|
||||
onDismiss = onDismiss,
|
||||
)
|
||||
}
|
||||
@@ -631,7 +628,6 @@ fun ShareImageAction(
|
||||
popupExpanded: MutableState<Boolean>,
|
||||
videoUri: String?,
|
||||
postNostrUri: String?,
|
||||
postNostrid: String?,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
DropdownMenu(
|
||||
@@ -665,13 +661,13 @@ fun ShareImageAction(
|
||||
text = { Text(stringRes(R.string.add_media_to_gallery)) },
|
||||
onClick = {
|
||||
if (videoUri != null) {
|
||||
if (postNostrid != null) {
|
||||
print("TODO")
|
||||
print(postNostrid)
|
||||
// TODO this still crashes
|
||||
accountViewModel.account.addToGallery(postNostrid, videoUri)
|
||||
var n19 = Nip19Bech32.uriToRoute(postNostrUri)?.entity as? Nip19Bech32.NEvent
|
||||
if (n19 != null) {
|
||||
accountViewModel.addMediaToGallery(n19.hex, videoUri)
|
||||
accountViewModel.toast(R.string.image_saved_to_the_gallery, R.string.image_saved_to_the_gallery)
|
||||
}
|
||||
}
|
||||
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
|
||||
+52
@@ -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
|
||||
fun NoteQuickActionMenu(
|
||||
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
|
||||
fun NoteQuickActionItem(
|
||||
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
|
||||
fun DeleteAlertDialog(
|
||||
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,
|
||||
accountViewModel = accountViewModel,
|
||||
nostrUriCallback = note.toNostrUri(),
|
||||
nostrIdCallback = note.idHex,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,7 +160,6 @@ fun RenderLiveActivityEventInner(
|
||||
isFiniteHeight = false,
|
||||
accountViewModel = accountViewModel,
|
||||
nostrUriCallback = "nostr:${baseNote.toNEvent()}",
|
||||
nostrIdCallback = baseNote.idHex,
|
||||
)
|
||||
}
|
||||
} 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.UserProfileBookmarksFeedFilter
|
||||
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.UserProfileReportsFeedFilter
|
||||
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) :
|
||||
FeedViewModel(UserProfileBookmarksFeedFilter(user, account)) {
|
||||
class Factory(val user: User, val account: Account) : ViewModelProvider.Factory {
|
||||
|
||||
+14
@@ -669,6 +669,20 @@ class AccountViewModel(
|
||||
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) {
|
||||
viewModelScope.launch(Dispatchers.IO) { account.addBookmark(note, true) }
|
||||
}
|
||||
|
||||
+129
-64
@@ -20,86 +20,163 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.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.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment.Companion.BottomStart
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import coil.compose.AsyncImage
|
||||
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.note.CheckHiddenFeedWatchBlockAndReport
|
||||
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.WatchNoteEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.calculateBackgroundColor
|
||||
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.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||
import com.vitorpamplona.quartz.events.GalleryListEvent
|
||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||
|
||||
// TODO This is to large parts from the ChannelCardCompose
|
||||
// Why does it not be in a grid, like the marketplace
|
||||
@Composable
|
||||
fun ProfileGallery(
|
||||
baseNotes: List<GalleryThumb>,
|
||||
fun RenderGalleryFeed(
|
||||
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,
|
||||
parentBackgroundColor: MutableState<Color>? = null,
|
||||
forceEventKind: Int?,
|
||||
isHiddenFeed: Boolean = false,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
for (thumb in baseNotes) {
|
||||
thumb.baseNote?.let {
|
||||
WatchNoteEvent(baseNote = it, accountViewModel = accountViewModel) {
|
||||
if (thumb.baseNote.event?.kind() == GalleryListEvent.KIND) {
|
||||
CheckHiddenFeedWatchBlockAndReport(
|
||||
note = thumb.baseNote,
|
||||
modifier = modifier,
|
||||
ignoreAllBlocksAndReports = isHiddenFeed,
|
||||
showHiddenWarning = false,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
) { canPreview ->
|
||||
|
||||
thumb.image?.let { it1 ->
|
||||
GalleryCard(
|
||||
baseNote = thumb.baseNote,
|
||||
url = it1,
|
||||
modifier = modifier,
|
||||
parentBackgroundColor = parentBackgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel) {
|
||||
CheckHiddenFeedWatchBlockAndReport(
|
||||
note = baseNote,
|
||||
modifier = modifier,
|
||||
ignoreAllBlocksAndReports = isHiddenFeed,
|
||||
showHiddenWarning = false,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
) { canPreview ->
|
||||
GalleryCard(
|
||||
baseNote = baseNote,
|
||||
modifier = modifier,
|
||||
parentBackgroundColor = parentBackgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,18 +184,15 @@ fun ProfileGallery(
|
||||
@Composable
|
||||
fun GalleryCard(
|
||||
baseNote: Note,
|
||||
url: String,
|
||||
modifier: Modifier = Modifier,
|
||||
parentBackgroundColor: MutableState<Color>? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
// baseNote.event?.let { Text(text = it.pubKey()) }
|
||||
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
|
||||
|
||||
LongPressToQuickActionGallery(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
|
||||
CheckNewAndRenderChannelCard(
|
||||
baseNote,
|
||||
url,
|
||||
modifier,
|
||||
parentBackgroundColor,
|
||||
accountViewModel,
|
||||
@@ -131,7 +205,6 @@ fun GalleryCard(
|
||||
@Composable
|
||||
private fun CheckNewAndRenderChannelCard(
|
||||
baseNote: Note,
|
||||
url: String,
|
||||
modifier: Modifier = Modifier,
|
||||
parentBackgroundColor: MutableState<Color>? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
@@ -156,7 +229,6 @@ private fun CheckNewAndRenderChannelCard(
|
||||
// baseNote.event?.let { Text(text = it.pubKey()) }
|
||||
InnerGalleryCardWithReactions(
|
||||
baseNote = baseNote,
|
||||
url = url,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
@@ -166,17 +238,15 @@ private fun CheckNewAndRenderChannelCard(
|
||||
@Composable
|
||||
fun InnerGalleryCardWithReactions(
|
||||
baseNote: Note,
|
||||
url: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
InnerGalleryCardBox(baseNote, url, accountViewModel, nav)
|
||||
InnerGalleryCardBox(baseNote, accountViewModel, nav)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InnerGalleryCardBox(
|
||||
baseNote: Note,
|
||||
url: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
@@ -185,14 +255,13 @@ fun InnerGalleryCardBox(
|
||||
note = baseNote,
|
||||
accountViewModel = accountViewModel,
|
||||
) {
|
||||
RenderGalleryThumb(baseNote, url, accountViewModel, nav)
|
||||
RenderGalleryThumb(baseNote, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class GalleryThumb(
|
||||
val baseNote: Note?,
|
||||
val id: String?,
|
||||
val image: String?,
|
||||
val title: String?,
|
||||
@@ -202,34 +271,31 @@ data class GalleryThumb(
|
||||
@Composable
|
||||
fun RenderGalleryThumb(
|
||||
baseNote: Note,
|
||||
url: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val noteEvent = baseNote.event as? GalleryListEvent ?: return
|
||||
val noteEvent = baseNote.event as? TextNoteEvent ?: return
|
||||
|
||||
val card by
|
||||
baseNote
|
||||
.live()
|
||||
.metadata
|
||||
.map {
|
||||
val noteEvent = baseNote.event as GalleryListEvent
|
||||
val noteEvent = baseNote.event as TextNoteEvent
|
||||
|
||||
GalleryThumb(
|
||||
baseNote = baseNote,
|
||||
id = "",
|
||||
image = url,
|
||||
title = "Hello",
|
||||
image = noteEvent.firstTaggedUrl(),
|
||||
title = noteEvent.content(),
|
||||
// noteEvent?.title(),
|
||||
// price = noteEvent?.price(),
|
||||
)
|
||||
}.distinctUntilChanged()
|
||||
.observeAsState(
|
||||
GalleryThumb(
|
||||
baseNote = baseNote,
|
||||
id = "",
|
||||
image = "https://gokaygokay-aurasr.hf.space/file=/tmp/gradio/68292f324a38d7071453cf6912dfb1da9d1305c8/image3.png",
|
||||
title = "Hello",
|
||||
image = noteEvent.firstTaggedUrl(),
|
||||
title = noteEvent.content(),
|
||||
// image = noteEvent.image(),
|
||||
// title = noteEvent.title(),
|
||||
// price = noteEvent.price(),
|
||||
@@ -246,7 +312,6 @@ fun RenderGalleryThumbPreview() {
|
||||
InnerRenderGalleryThumb(
|
||||
card =
|
||||
GalleryThumb(
|
||||
baseNote = null,
|
||||
id = "",
|
||||
image = null,
|
||||
title = "Like New",
|
||||
@@ -277,7 +342,7 @@ fun InnerRenderGalleryThumb(
|
||||
)
|
||||
} ?: run { DisplayGalleryAuthorBanner(note) }
|
||||
|
||||
Row(
|
||||
/* Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color.Black.copy(0.6f))
|
||||
@@ -294,7 +359,7 @@ fun InnerRenderGalleryThumb(
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
/*
|
||||
|
||||
card.price?.let {
|
||||
val priceTag =
|
||||
remember(card) {
|
||||
@@ -315,8 +380,8 @@ fun InnerRenderGalleryThumb(
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = Color.White,
|
||||
)
|
||||
}*/
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+52
-2
@@ -42,6 +42,7 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.grid.LazyGridState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
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.NostrUserProfileFollowersUserFeedViewModel
|
||||
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.NostrUserProfileReportFeedViewModel
|
||||
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.RelayFeedView
|
||||
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.stringRes
|
||||
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 =
|
||||
viewModel(
|
||||
key = baseUser.pubkeyHex + "UserProfileReportFeedViewModel",
|
||||
@@ -312,6 +325,7 @@ fun PrepareViewModels(
|
||||
appRecommendations,
|
||||
zapFeedViewModel,
|
||||
bookmarksFeedViewModel,
|
||||
galleryFeedViewModel,
|
||||
reportsFeedViewModel,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
@@ -328,6 +342,7 @@ fun ProfileScreen(
|
||||
appRecommendations: NostrUserAppRecommendationsFeedViewModel,
|
||||
zapFeedViewModel: NostrUserProfileZapsFeedViewModel,
|
||||
bookmarksFeedViewModel: NostrUserProfileBookmarksFeedViewModel,
|
||||
galleryFeedViewModel: NostrUserProfileGalleryFeedViewModel,
|
||||
reportsFeedViewModel: NostrUserProfileReportFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
@@ -372,6 +387,7 @@ fun ProfileScreen(
|
||||
followersFeedViewModel,
|
||||
zapFeedViewModel,
|
||||
bookmarksFeedViewModel,
|
||||
galleryFeedViewModel,
|
||||
reportsFeedViewModel,
|
||||
accountViewModel,
|
||||
nav,
|
||||
@@ -388,6 +404,7 @@ private fun RenderSurface(
|
||||
followersFeedViewModel: NostrUserProfileFollowersUserFeedViewModel,
|
||||
zapFeedViewModel: NostrUserProfileZapsFeedViewModel,
|
||||
bookmarksFeedViewModel: NostrUserProfileBookmarksFeedViewModel,
|
||||
galleryFeedViewModel: NostrUserProfileGalleryFeedViewModel,
|
||||
reportsFeedViewModel: NostrUserProfileReportFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
@@ -448,6 +465,7 @@ private fun RenderSurface(
|
||||
followersFeedViewModel,
|
||||
zapFeedViewModel,
|
||||
bookmarksFeedViewModel,
|
||||
galleryFeedViewModel,
|
||||
reportsFeedViewModel,
|
||||
accountViewModel,
|
||||
nav,
|
||||
@@ -470,6 +488,7 @@ private fun RenderScreen(
|
||||
followersFeedViewModel: NostrUserProfileFollowersUserFeedViewModel,
|
||||
zapFeedViewModel: NostrUserProfileZapsFeedViewModel,
|
||||
bookmarksFeedViewModel: NostrUserProfileBookmarksFeedViewModel,
|
||||
galleryFeedViewModel: NostrUserProfileGalleryFeedViewModel,
|
||||
reportsFeedViewModel: NostrUserProfileReportFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
@@ -501,6 +520,7 @@ private fun RenderScreen(
|
||||
followersFeedViewModel,
|
||||
zapFeedViewModel,
|
||||
bookmarksFeedViewModel,
|
||||
galleryFeedViewModel,
|
||||
reportsFeedViewModel,
|
||||
accountViewModel,
|
||||
nav,
|
||||
@@ -519,6 +539,7 @@ private fun CreateAndRenderPages(
|
||||
followersFeedViewModel: NostrUserProfileFollowersUserFeedViewModel,
|
||||
zapFeedViewModel: NostrUserProfileZapsFeedViewModel,
|
||||
bookmarksFeedViewModel: NostrUserProfileBookmarksFeedViewModel,
|
||||
galleryFeedViewModel: NostrUserProfileGalleryFeedViewModel,
|
||||
reportsFeedViewModel: NostrUserProfileReportFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
@@ -533,7 +554,7 @@ private fun CreateAndRenderPages(
|
||||
when (page) {
|
||||
0 -> TabNotesNewThreads(threadsViewModel, accountViewModel, nav)
|
||||
1 -> TabNotesConversations(repliesViewModel, accountViewModel, nav)
|
||||
2 -> Gallery(baseUser, followsFeedViewModel, accountViewModel, nav)
|
||||
2 -> TabGallery(galleryFeedViewModel, accountViewModel, nav)
|
||||
3 -> TabFollows(baseUser, followsFeedViewModel, accountViewModel, nav)
|
||||
4 -> TabFollowers(baseUser, followersFeedViewModel, accountViewModel, nav)
|
||||
5 -> TabReceivedZaps(baseUser, zapFeedViewModel, accountViewModel, nav)
|
||||
@@ -1537,7 +1558,36 @@ fun TabNotesConversations(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@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(
|
||||
baseUser: User,
|
||||
feedViewModel: UserFeedViewModel,
|
||||
@@ -1579,7 +1629,7 @@ fun Gallery(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} */
|
||||
|
||||
@Composable
|
||||
fun TabFollowedTags(
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<string name="could_not_decrypt_the_message">Nie można odszyfrować wiadomości</string>
|
||||
<string name="group_picture">Zdjęcie grupy</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="impersonation">Podszywanie się</string>
|
||||
<string name="illegal_behavior">Antyspołeczne zachowanie</string>
|
||||
@@ -199,6 +200,8 @@
|
||||
<string name="never">nigdy</string>
|
||||
<string name="now">teraz</string>
|
||||
<string name="h">godz.</string>
|
||||
<string name="m">m</string>
|
||||
<string name="d">d</string>
|
||||
<string name="nudity">Nagość</string>
|
||||
<string name="profanity_hateful_speech">Wulgaryzmy / Mowa nienawiści</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_max">Maksymalny Zap</string>
|
||||
<string name="poll_consensus_threshold">Konsensus</string>
|
||||
<string name="poll_consensus_threshold_percent">(0–100)%</string>
|
||||
<string name="poll_closing_time">Zamknij po</string>
|
||||
<string name="poll_closing_time_days">dni</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="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="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_target_amount_in_sats">Docelowa kwota w Satach</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="auth">Autoryzacja</string>
|
||||
<string name="payment">Płatność</string>
|
||||
<string name="cashu">Cashu Token</string>
|
||||
<string name="cashu_redeem">Wykup</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>
|
||||
@@ -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="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_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_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>
|
||||
@@ -527,6 +534,7 @@
|
||||
<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="zap_split_weight">Procentowo</string>
|
||||
<string name="zap_split_weight_placeholder">25</string>
|
||||
<string name="splitting_zaps_with">Podziel zapsy z</string>
|
||||
<string name="forwarding_zaps_to">Przesyłanie zapasów do</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="classifieds">Sprzedaj przedmiot</string>
|
||||
<string name="classifieds_title">Tytuł</string>
|
||||
<string name="classifieds_title_placeholder">iPhone 13</string>
|
||||
<string name="classifieds_condition">Stan</string>
|
||||
<string name="classifieds_category">Kategoria</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_placeholder">Miasto, Województwo, Kraj</string>
|
||||
<string name="classifieds_condition_new">Nowość</string>
|
||||
@@ -614,6 +624,7 @@
|
||||
<string name="classifieds_category_books">Książki</string>
|
||||
<string name="classifieds_category_pets">Zwierzęta domowe</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_crafts">Rzemiosło</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="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="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="login_with_qr_code">Zaloguj się przy użyciu QR kodu</string>
|
||||
<string name="route">Ścieżka</string>
|
||||
@@ -647,6 +660,7 @@
|
||||
<string name="reply_description">Odpowiedz</string>
|
||||
<string name="boost_or_quote_description">Zrepostuj lub Zacytuj</string>
|
||||
<string name="like_description">Lubię</string>
|
||||
<string name="zap_description">Zap</string>
|
||||
<string name="change_reaction">Zmień odzew</string>
|
||||
<string name="profile_image_of_user">Zdjęcie profilowe %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="cancel_bitcoin_invoice">Anuluj fakturę Bitcoin</string>
|
||||
<string name="cancel_classifieds">Anuluj sprzedaż przedmiotu</string>
|
||||
<string name="add_zapraiser">Zapraiser</string>
|
||||
<string name="cancel_zapraiser">Anuluj Zapraiser</string>
|
||||
<string name="add_location">Lokalizacja</string>
|
||||
<string name="remove_location">Usuń lokalizację</string>
|
||||
@@ -710,6 +725,7 @@
|
||||
<string name="git_repository">Repozytorium Git: %1$s</string>
|
||||
<string name="git_web_address">Strona internetowa:</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_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>
|
||||
@@ -737,4 +753,27 @@
|
||||
<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="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>
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
<string name="add_a_relay">添加中继器</string>
|
||||
<string name="display_name">显示名称</string>
|
||||
<string name="my_display_name">我的显示名称</string>
|
||||
<string name="my_awesome_name">Nostr 太酷了</string>
|
||||
<string name="welcome">欢迎!</string>
|
||||
<string name="username">用户名</string>
|
||||
<string name="my_username">我的用户名</string>
|
||||
@@ -386,6 +387,7 @@
|
||||
<string name="warn_when_posts_have_reports_from_your_follows">当帖子有你的关注的报告时警告</string>
|
||||
<string name="new_reaction_symbol">新回应符号</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_target_amount_in_sats">目标聪金额</string>
|
||||
<string name="sats_to_complete">Zapraiser 位于 %1$s。距离目标%2$s聪</string>
|
||||
@@ -671,6 +673,7 @@
|
||||
<string name="add_bitcoin_invoice">比特币发票</string>
|
||||
<string name="cancel_bitcoin_invoice">取消比特币发票</string>
|
||||
<string name="cancel_classifieds">取消出售物品</string>
|
||||
<string name="add_zapraiser">Zapraiser</string>
|
||||
<string name="cancel_zapraiser">取消 Zapraiser</string>
|
||||
<string name="add_location">地点</string>
|
||||
<string name="remove_location">移除地点</string>
|
||||
|
||||
@@ -270,6 +270,8 @@
|
||||
<string name="quick_action_delete">Delete</string>
|
||||
<string name="quick_action_unfollow">Unfollow</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_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>
|
||||
@@ -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="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="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>
|
||||
|
||||
@@ -896,4 +900,35 @@
|
||||
<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="dvm_waiting_to_confirm_payment">Waiting for DVM to confirm payment or send results</string>
|
||||
|
||||
<string name="http_status_400">Bad Request - The server can’t or won’t process the request.</string>
|
||||
<string name="http_status_401">Unauthorized - The user doesn’t 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 doesn’t have access rights to make the request</string>
|
||||
<string name="http_status_404">Not Found - The server can’t 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 doesn’t find any content that satisfies the request.</string>
|
||||
<string name="http_status_407">Proxy Authentication Required - The user doesn’t 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 can’t fulfill the request because there’s 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 server’s 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 can’t fulfill the value indicated in the request’s Range header field. </string>
|
||||
<string name="http_status_417">Expectation Failed - The server can’t 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 can’t fulfill the request or doesn’t 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 doesn’t 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 doesn’t 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>
|
||||
|
||||
@@ -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 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 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 taggedGalleryEntries(): List<GalleryUrl>
|
||||
|
||||
fun taggedEmojis(): List<EmojiUrl>
|
||||
|
||||
fun matchTag1With(text: String): Boolean
|
||||
|
||||
@@ -60,7 +60,7 @@ class GalleryListEvent(
|
||||
) {
|
||||
add(
|
||||
earlierVersion,
|
||||
arrayOf(arrayOf(tagName, url, tagValue)),
|
||||
arrayOf(arrayOf(tagName, tagValue, url)),
|
||||
signer,
|
||||
createdAt,
|
||||
onReady,
|
||||
@@ -76,7 +76,7 @@ class GalleryListEvent(
|
||||
) {
|
||||
create(
|
||||
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,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
@@ -86,23 +86,26 @@ class GalleryListEvent(
|
||||
fun removeEvent(
|
||||
earlierVersion: GalleryListEvent,
|
||||
eventId: HexKey,
|
||||
url: String,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (GalleryListEvent) -> Unit,
|
||||
) = removeTag(earlierVersion, "e", eventId, signer, createdAt, onReady)
|
||||
) = removeTag(earlierVersion, "g", eventId, url, signer, createdAt, onReady)
|
||||
|
||||
fun removeReplaceable(
|
||||
earlierVersion: GalleryListEvent,
|
||||
aTag: ATag,
|
||||
url: String,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (GalleryListEvent) -> Unit,
|
||||
) = removeTag(earlierVersion, "a", aTag.toTag(), signer, createdAt, onReady)
|
||||
) = removeTag(earlierVersion, "g", aTag.toTag(), url, signer, createdAt, onReady)
|
||||
|
||||
private fun removeTag(
|
||||
earlierVersion: GalleryListEvent,
|
||||
tagName: String,
|
||||
tagValue: HexKey,
|
||||
url: String,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (GalleryListEvent) -> Unit,
|
||||
@@ -111,7 +114,7 @@ class GalleryListEvent(
|
||||
content = earlierVersion.content,
|
||||
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(),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
@@ -135,30 +138,5 @@ class GalleryListEvent(
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user