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 }}
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
+2 -2
View File
@@ -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,
),
)
@@ -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(
@@ -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)
}
}
}
@@ -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()
},
)
@@ -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 {
@@ -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) }
}
@@ -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,
)
}*/
}
}
}*/
}
}
@@ -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">(0100)%</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>
+35
View File
@@ -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 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>
@@ -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)
}
}
}
}