From a9cbbf66bdd8a6430e9d689984170814ece9b2e8 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 28 Jun 2024 10:40:55 -0400 Subject: [PATCH 01/11] Adds uploading error messages for common HTTP status codes. --- .../amethyst/service/HttpStatusMessages.kt | 63 +++++++++++++++++++ .../amethyst/service/Nip96Uploader.kt | 49 ++++++++++----- .../amethyst/ui/actions/EditPostViewModel.kt | 41 ++++++++---- .../amethyst/ui/actions/NewMediaModel.kt | 1 + .../amethyst/ui/actions/NewPostViewModel.kt | 1 + .../ui/actions/NewUserMetadataViewModel.kt | 1 + amethyst/src/main/res/values/strings.xml | 33 ++++++++++ 7 files changed, 161 insertions(+), 28 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/HttpStatusMessages.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/HttpStatusMessages.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/HttpStatusMessages.kt new file mode 100644 index 000000000..009038d54 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/HttpStatusMessages.kt @@ -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 + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip96Uploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip96Uploader.kt index 7d069d0f1..a2b915363 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip96Uploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip96Uploader.kt @@ -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 = ('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, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt index a951d4a7a..53aa0a0e8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt @@ -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" } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt index c9d3ccf79..d3cdcfa45 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt @@ -139,6 +139,7 @@ open class NewMediaModel : ViewModel() { onProgress = { percent: Float -> uploadingPercentage.value = 0.2f + (0.2f * percent) }, + context = context, ) createNIP94Record( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index 4dd0e23a5..55dc95a68 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -801,6 +801,7 @@ open class NewPostViewModel : ViewModel() { server = server.server, contentResolver = contentResolver, onProgress = {}, + context = context, ) createNIP94Record( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt index 02dc193f1..93fac1474 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt @@ -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) diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 6d8f93237..0a6fd89e1 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -760,6 +760,8 @@ Server did not provide a URL after uploading Could not download uploaded media from the server Could not prepare local file to upload: %1$s + Failed to upload: %1$s + Failed to delete: %1$s Edit draft @@ -895,4 +897,35 @@ Requesting Job from DVM Payment request sent, waiting for confirmation from your wallet Waiting for DVM to confirm payment or send results + + Bad Request - The server can’t or won’t process the request. + Unauthorized - The user doesn’t have valid authentication credentials + Payment Required - The server requires payment to complete the request + Forbidden - The user doesn’t have access rights to make the request + Not Found - The server can’t find the requested address + Method Not Allowed - The server supports the request method, but not the target resource + Not Acceptable - The server doesn’t find any content that satisfies the request. + Proxy Authentication Required - The user doesn’t have valid authentication credentials + Request Timeout - The server timed out waiting for somebody else + Conflict - The server can’t fulfill the request because there’s a conflict with the resource + Gone - The content requested has been permanently deleted from the server and will not be reinstated + Length Required - The server rejects the request because it requires a defined + Precondition Failed - The request\'s preconditions in the header fields that the server fails to meet + Payload Too Large - The request is larger than the server’s defined limits, and the server refuses to process it + URI Too Long - The url requested by the client is too long for the server to process. + Unsupported Media Type - The request uses a media format the server does not support + Range Not Satisfiable - The server can’t fulfill the value indicated in the request’s Range header field. + Expectation Failed - The server can’t meet the requirements indicated by the Expect request header field + Upgrade Required - The server refuses to process the request using the current protocol unless the client upgrades to a different protocol. + + Internal Server Error - The server has encountered an unexpected error and cannot complete the request + Not Implemented - The server can’t fulfill the request or doesn’t recognize the request method + Bad Gateway - The server acts as a gateway and got an invalid response from the host + Service Unavailable - This often occurs when a server is overloaded or down for maintenance + Gateway Timeout - The server was acting as a gateway or proxy and timed out, waiting for a response + HTTP Version Not Supported - The server doesn’t support the HTTP version in the request + Variant Also Negotiates - The server has an internal configuration error + Insufficient Storage - The server doesn’t have enough storage to process the request successfully + Loop Detected - The server detects an infinite loop while processing the request + Network Authentication Required - The client must be authenticated to access the network From a57ba2550ce2bae308cba1a4e50d862d495de22c Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 28 Jun 2024 14:43:45 +0000 Subject: [PATCH 02/11] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-zh-rCN/strings.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/amethyst/src/main/res/values-zh-rCN/strings.xml b/amethyst/src/main/res/values-zh-rCN/strings.xml index a83417094..0c0e685f1 100644 --- a/amethyst/src/main/res/values-zh-rCN/strings.xml +++ b/amethyst/src/main/res/values-zh-rCN/strings.xml @@ -103,6 +103,7 @@ 添加中继器 显示名称 我的显示名称 + Nostr 太酷了 欢迎! 用户名 我的用户名 @@ -386,6 +387,7 @@ 当帖子有你的关注的报告时警告 新回应符号 未选择回应类型。长按可更改 + Zapraiser 为这个帖子添加目标聪金额。支持的客户端可以将此显示为奖励捐赠的进度条 目标聪金额 Zapraiser 位于 %1$s。距离目标%2$s聪 @@ -671,6 +673,7 @@ 比特币发票 取消比特币发票 取消出售物品 + Zapraiser 取消 Zapraiser 地点 移除地点 From a42762de53b3e00abca6d81852e14981a785958f Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Fri, 28 Jun 2024 19:14:36 +0200 Subject: [PATCH 03/11] alternative attempt.. --- .../ui/dal/UserProfileGalleryFeedFilter.kt | 52 +++++++ .../amethyst/ui/screen/FeedViewModel.kt | 11 ++ .../ui/screen/loggedIn/ProfileGallery.kt | 134 +++++++++++++++++- .../ui/screen/loggedIn/ProfileScreen.kt | 47 +++++- 4 files changed, 239 insertions(+), 5 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileGalleryFeedFilter.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileGalleryFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileGalleryFeedFilter.kt new file mode 100644 index 000000000..6c5e5a86e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileGalleryFeedFilter.kt @@ -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() { + override fun feedKey(): String { + return account.userProfile().pubkeyHex + "-" + user.pubkeyHex + } + + override fun feed(): List { + val notes = + user.latestGalleryList + ?.taggedEvents() + ?.mapNotNull { LocalCache.checkGetOrCreateNote(it) } + ?.toSet() + ?: emptySet() + + val addresses = + user.latestGalleryList + ?.taggedAddresses() + ?.map { LocalCache.getOrCreateAddressableNote(it) } + ?.toSet() + ?: emptySet() + + return (notes + addresses) + .filter { account.isAcceptable(it) } + .sortedWith(DefaultFeedOrder) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index 422bd15a2..04169e499 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -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 create(modelClass: Class): 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 { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileGallery.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileGallery.kt index 08204061f..3f3529a04 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileGallery.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileGallery.kt @@ -20,6 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn +import androidx.compose.animation.core.tween +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -30,6 +32,10 @@ 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.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +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 @@ -37,6 +43,7 @@ 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 @@ -46,28 +53,149 @@ 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.NormalChannelCard 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 +@Composable +private fun RenderGalleryFeed( + viewModel: FeedViewModel, + routeForLastRead: String?, + forceEventKind: Int?, + listState: LazyListState, + 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: LazyListState, + forceEventKind: Int?, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + LazyColumn( + 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.fillMaxWidth(), + forceEventKind = forceEventKind, + accountViewModel = accountViewModel, + nav = nav, + ) + } + + HorizontalDivider( + thickness = DividerThickness, + ) + } + } +} + +@Composable +fun GalleryCardCompose( + baseNote: Note, + routeForLastRead: String? = null, + modifier: Modifier = Modifier, + parentBackgroundColor: MutableState? = null, + forceEventKind: Int?, + isHiddenFeed: Boolean = false, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel) { + if (forceEventKind == null || baseNote.event?.kind() == forceEventKind) { + CheckHiddenFeedWatchBlockAndReport( + note = baseNote, + modifier = modifier, + ignoreAllBlocksAndReports = isHiddenFeed, + showHiddenWarning = false, + accountViewModel = accountViewModel, + nav = nav, + ) { canPreview -> + NormalChannelCard( + baseNote = baseNote, + routeForLastRead = routeForLastRead, + modifier = modifier, + parentBackgroundColor = parentBackgroundColor, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } + } +} + // TODO This is to large parts from the ChannelCardCompose -// Why does it not be in a grid, like the marketplace + +/*@OptIn(ExperimentalFoundationApi::class) @Composable fun ProfileGallery( - baseNotes: List, + baseNote: List, modifier: Modifier = Modifier, parentBackgroundColor: MutableState? = null, isHiddenFeed: Boolean = false, @@ -103,7 +231,7 @@ fun ProfileGallery( } } } - +*/ @Composable fun GalleryCard( baseNote: Note, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index 735444d82..59e1c16b6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -148,6 +148,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 @@ -294,6 +295,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 +323,7 @@ fun PrepareViewModels( appRecommendations, zapFeedViewModel, bookmarksFeedViewModel, + galleryFeedViewModel, reportsFeedViewModel, accountViewModel = accountViewModel, nav = nav, @@ -328,6 +340,7 @@ fun ProfileScreen( appRecommendations: NostrUserAppRecommendationsFeedViewModel, zapFeedViewModel: NostrUserProfileZapsFeedViewModel, bookmarksFeedViewModel: NostrUserProfileBookmarksFeedViewModel, + galleryFeedViewModel: NostrUserProfileGalleryFeedViewModel, reportsFeedViewModel: NostrUserProfileReportFeedViewModel, accountViewModel: AccountViewModel, nav: (String) -> Unit, @@ -372,6 +385,7 @@ fun ProfileScreen( followersFeedViewModel, zapFeedViewModel, bookmarksFeedViewModel, + galleryFeedViewModel, reportsFeedViewModel, accountViewModel, nav, @@ -388,6 +402,7 @@ private fun RenderSurface( followersFeedViewModel: NostrUserProfileFollowersUserFeedViewModel, zapFeedViewModel: NostrUserProfileZapsFeedViewModel, bookmarksFeedViewModel: NostrUserProfileBookmarksFeedViewModel, + galleryFeedViewModel: NostrUserProfileGalleryFeedViewModel, reportsFeedViewModel: NostrUserProfileReportFeedViewModel, accountViewModel: AccountViewModel, nav: (String) -> Unit, @@ -448,6 +463,7 @@ private fun RenderSurface( followersFeedViewModel, zapFeedViewModel, bookmarksFeedViewModel, + galleryFeedViewModel, reportsFeedViewModel, accountViewModel, nav, @@ -470,6 +486,7 @@ private fun RenderScreen( followersFeedViewModel: NostrUserProfileFollowersUserFeedViewModel, zapFeedViewModel: NostrUserProfileZapsFeedViewModel, bookmarksFeedViewModel: NostrUserProfileBookmarksFeedViewModel, + galleryFeedViewModel: NostrUserProfileGalleryFeedViewModel, reportsFeedViewModel: NostrUserProfileReportFeedViewModel, accountViewModel: AccountViewModel, nav: (String) -> Unit, @@ -501,6 +518,7 @@ private fun RenderScreen( followersFeedViewModel, zapFeedViewModel, bookmarksFeedViewModel, + galleryFeedViewModel, reportsFeedViewModel, accountViewModel, nav, @@ -519,6 +537,7 @@ private fun CreateAndRenderPages( followersFeedViewModel: NostrUserProfileFollowersUserFeedViewModel, zapFeedViewModel: NostrUserProfileZapsFeedViewModel, bookmarksFeedViewModel: NostrUserProfileBookmarksFeedViewModel, + galleryFeedViewModel: NostrUserProfileGalleryFeedViewModel, reportsFeedViewModel: NostrUserProfileReportFeedViewModel, accountViewModel: AccountViewModel, nav: (String) -> Unit, @@ -533,7 +552,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 +1556,31 @@ 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), + ) { + RefresheableFeedView( + feedViewModel, + null, + enablePullRefresh = false, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } +} + +/*@Composable fun Gallery( baseUser: User, feedViewModel: UserFeedViewModel, @@ -1579,7 +1622,7 @@ fun Gallery( } } } -} +} */ @Composable fun TabFollowedTags( From 347b82ef7f5161294862d1480201688e67cd0a81 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 28 Jun 2024 13:28:05 -0400 Subject: [PATCH 04/11] Fixes when the logged in accounts only include the current account. --- .../vitorpamplona/amethyst/service/NostrAccountDataSource.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt index 0d91a22f8..4df69e1e4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt @@ -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, ), ) From f0044afeabed4bf7f06a368577a731deb7aa8a26 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 28 Jun 2024 14:06:08 -0400 Subject: [PATCH 05/11] Fixes bug on string resources. --- .../EventNotificationConsumer.kt | 25 ++++++++++++++++--- .../amethyst/ui/StringResourceCache.kt | 4 +-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt index 4ff65b474..7580bdc2e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt @@ -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, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/StringResourceCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/StringResourceCache.kt index 2caf1d52b..c193a35aa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/StringResourceCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/StringResourceCache.kt @@ -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) } + ) } From f331398316298d8e1892ec4b1794c4498d5076d3 Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Fri, 28 Jun 2024 21:13:52 +0200 Subject: [PATCH 06/11] gallery is visualised --- .../vitorpamplona/amethyst/model/Account.kt | 2 - .../ui/dal/UserProfileGalleryFeedFilter.kt | 14 ++-- .../ui/screen/loggedIn/ProfileGallery.kt | 83 +++++++++---------- .../ui/screen/loggedIn/ProfileScreen.kt | 7 +- .../quartz/events/EmojiPackEvent.kt | 18 ++++ .../com/vitorpamplona/quartz/events/Event.kt | 2 + .../quartz/events/EventInterface.kt | 2 + 7 files changed, 73 insertions(+), 55 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 2331f3c37..903579a7c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -2226,7 +2226,6 @@ class Account( GalleryListEvent.removeReplaceable( galleryentries, note.address, - false, signer, ) { Client.send(it) @@ -2236,7 +2235,6 @@ class Account( GalleryListEvent.removeEvent( galleryentries, note.idHex, - false, signer, ) { Client.send(it) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileGalleryFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileGalleryFeedFilter.kt index 6c5e5a86e..8b81e5bde 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileGalleryFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileGalleryFeedFilter.kt @@ -27,26 +27,26 @@ import com.vitorpamplona.amethyst.model.User class UserProfileGalleryFeedFilter(val user: User, val account: Account) : FeedFilter() { override fun feedKey(): String { - return account.userProfile().pubkeyHex + "-" + user.pubkeyHex + return account.userProfile().pubkeyHex + "-Gallery-" + user.pubkeyHex } override fun feed(): List { val notes = user.latestGalleryList - ?.taggedEvents() - ?.mapNotNull { LocalCache.checkGetOrCreateNote(it) } + ?.taggedGalleryEntries() + ?.mapNotNull { LocalCache.checkGetOrCreateNote(it.id) } ?.toSet() ?: emptySet() - val addresses = + /*val addresses = user.latestGalleryList ?.taggedAddresses() ?.map { LocalCache.getOrCreateAddressableNote(it) } ?.toSet() - ?: emptySet() + ?: emptySet() */ - return (notes + addresses) + // .sortedWith(DefaultFeedOrder) + return (notes) .filter { account.isAcceptable(it) } - .sortedWith(DefaultFeedOrder) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileGallery.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileGallery.kt index 3f3529a04..df02459ae 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileGallery.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileGallery.kt @@ -32,8 +32,10 @@ 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.LazyColumn -import androidx.compose.foundation.lazy.LazyListState +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 @@ -63,7 +65,6 @@ 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.NormalChannelCard import com.vitorpamplona.amethyst.ui.note.WatchAuthor import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent import com.vitorpamplona.amethyst.ui.note.calculateBackgroundColor @@ -78,19 +79,18 @@ 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 @Composable -private fun RenderGalleryFeed( +fun RenderGalleryFeed( viewModel: FeedViewModel, routeForLastRead: String?, forceEventKind: Int?, - listState: LazyListState, + listState: LazyGridState, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { val feedState by viewModel.feedContent.collectAsStateWithLifecycle() - CrossfadeIfEnabled( targetState = feedState, animationSpec = tween(durationMillis = 100), @@ -126,12 +126,13 @@ private fun RenderGalleryFeed( private fun GalleryFeedLoaded( state: FeedState.Loaded, routeForLastRead: String?, - listState: LazyListState, + listState: LazyGridState, forceEventKind: Int?, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - LazyColumn( + LazyVerticalGrid( + columns = GridCells.Fixed(3), contentPadding = FeedPadding, state = listState, ) { @@ -142,7 +143,7 @@ private fun GalleryFeedLoaded( GalleryCardCompose( baseNote = item, routeForLastRead = routeForLastRead, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier, forceEventKind = forceEventKind, accountViewModel = accountViewModel, nav = nav, @@ -167,25 +168,30 @@ fun GalleryCardCompose( accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { + /* baseNote.event?.let { + Text(text = it.id() + " " + it.firstTaggedUrl() + " ") // TODO why does it.taggedGalleryEntries() not return something? whats different? + } baseNote.event?.let { + for (entry in it.taggedGalleryEntries()) { + Text(text = entry.url + " " + entry.id) + } + }*/ + WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel) { - if (forceEventKind == null || baseNote.event?.kind() == forceEventKind) { - CheckHiddenFeedWatchBlockAndReport( - note = baseNote, + CheckHiddenFeedWatchBlockAndReport( + note = baseNote, + modifier = modifier, + ignoreAllBlocksAndReports = isHiddenFeed, + showHiddenWarning = false, + accountViewModel = accountViewModel, + nav = nav, + ) { canPreview -> + GalleryCard( + baseNote = baseNote, modifier = modifier, - ignoreAllBlocksAndReports = isHiddenFeed, - showHiddenWarning = false, + parentBackgroundColor = parentBackgroundColor, accountViewModel = accountViewModel, nav = nav, - ) { canPreview -> - NormalChannelCard( - baseNote = baseNote, - routeForLastRead = routeForLastRead, - modifier = modifier, - parentBackgroundColor = parentBackgroundColor, - accountViewModel = accountViewModel, - nav = nav, - ) - } + ) } } } @@ -235,7 +241,6 @@ fun ProfileGallery( @Composable fun GalleryCard( baseNote: Note, - url: String, modifier: Modifier = Modifier, parentBackgroundColor: MutableState? = null, accountViewModel: AccountViewModel, @@ -246,7 +251,6 @@ fun GalleryCard( CheckNewAndRenderChannelCard( baseNote, - url, modifier, parentBackgroundColor, accountViewModel, @@ -259,7 +263,6 @@ fun GalleryCard( @Composable private fun CheckNewAndRenderChannelCard( baseNote: Note, - url: String, modifier: Modifier = Modifier, parentBackgroundColor: MutableState? = null, accountViewModel: AccountViewModel, @@ -284,7 +287,6 @@ private fun CheckNewAndRenderChannelCard( // baseNote.event?.let { Text(text = it.pubKey()) } InnerGalleryCardWithReactions( baseNote = baseNote, - url = url, accountViewModel = accountViewModel, nav = nav, ) @@ -294,17 +296,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, ) { @@ -313,14 +313,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?, @@ -330,34 +329,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(), @@ -374,7 +370,6 @@ fun RenderGalleryThumbPreview() { InnerRenderGalleryThumb( card = GalleryThumb( - baseNote = null, id = "", image = null, title = "Like New", diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index 59e1c16b6..e15700961 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -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 @@ -1569,10 +1570,12 @@ fun TabGallery( Column( modifier = Modifier.padding(vertical = 0.dp), ) { - RefresheableFeedView( + var state = LazyGridState() + RenderGalleryFeed( feedViewModel, null, - enablePullRefresh = false, + 0, + state, accountViewModel = accountViewModel, nav = nav, ) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/EmojiPackEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/EmojiPackEvent.kt index 73a09f013..356e8ce47 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EmojiPackEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EmojiPackEvent.kt @@ -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 + } + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt index 19521379e..a921dfb44 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt @@ -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] } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt index db11a3ea4..d444f4195 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt @@ -145,6 +145,8 @@ interface EventInterface { fun firstTaggedK(): Int? + fun taggedGalleryEntries(): List + fun taggedEmojis(): List fun matchTag1With(text: String): Boolean From 7c7f704173afa330444146440b3a6332df22ee9d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 28 Jun 2024 16:25:44 -0400 Subject: [PATCH 07/11] v0.88.5 --- amethyst/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/amethyst/build.gradle b/amethyst/build.gradle index b39f077c5..869259684 100644 --- a/amethyst/build.gradle +++ b/amethyst/build.gradle @@ -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" From 6eb7b4599083dcf8c7f29b9c9aa49948c4abd5d5 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 28 Jun 2024 20:27:40 +0000 Subject: [PATCH 08/11] New Crowdin translations by GitHub Action --- .../src/main/res/values-pl-rPL/strings.xml | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index 95481cddd..3a4ad34ef 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -14,6 +14,7 @@ Nie można odszyfrować wiadomości Zdjęcie grupy Niedozwolona zawartość + Spam Liczba spamu z tego transmitera Podszywanie się Antyspołeczne zachowanie @@ -199,6 +200,8 @@ nigdy teraz godz. + m + d Nagość Wulgaryzmy / Mowa nienawiści Zgłoś nienawistną mowę @@ -301,6 +304,7 @@ Minimalny Zap Maksymalny Zap Konsensus + (0–100)% Zamknij po dni Nie można głosować @@ -383,6 +387,7 @@ Ostrzegaj, gdy posty zostały zgłoszone przez osoby które obserwujesz Nowy Symbol Odzewu Brak wstępnie wybranych typów reakcji dla tego użytkownika. Przytrzymaj przycisk serce, aby zmienić + Zapraiser 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 Docelowa kwota w Satach Zapraiser przy: %1$s. %2$s satach do celu @@ -414,6 +419,7 @@ Minimalny poW Autoryzacja Płatność + Cashu Token Wykup Wyślij do Zap Wallet Otwórz w Portfelu Cashu @@ -507,6 +513,7 @@ 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ę. Nie można wysłać zapa Wyślij wiadomość użytkownikowi + OK Nieudane połączenie z %1$s: %2$s Nie udało się skompilować adresu URL NIP-11 dla %1$s: %2$s Nieudane połączenie z %1$s: %2$s @@ -527,6 +534,7 @@ Brakująca konfiguracja LN Użytkownik %1$s nie ma skonfigurowanego adresu LN, aby odbierać saty Procentowo + 25 Podziel zapsy z Przesyłanie zapasów do Nie znaleziono portfeli Lightning @@ -593,9 +601,11 @@ Cześć, czy to jest nadal dostępne? Sprzedaj przedmiot Tytuł + iPhone 13 Stan Kategoria Cena (w Satach) + 1000 Lokalizacja Miasto, Województwo, Kraj Nowość @@ -614,6 +624,7 @@ Książki Zwierzęta domowe Sporty + Fitness Sztuka Rzemiosło AGD @@ -628,6 +639,8 @@ Serwer nie podał adresu URL po załadowaniu Nie można pobrać przesłanych plików z serwera Nie można przygotować pliku lokalnego do przesłania: %1$s + Nie udało się przesłać: %1$s + Nie udało się usunąć: %1$s Edytuj wersję roboczą Zaloguj się przy użyciu QR kodu Ścieżka @@ -647,6 +660,7 @@ Odpowiedz Zrepostuj lub Zacytuj Lubię + Zap Zmień odzew Zdjęcie profilowe %1$s Transmiter %1$s @@ -658,6 +672,7 @@ Faktura Bitcoin Anuluj fakturę Bitcoin Anuluj sprzedaż przedmiotu + Zapraiser Anuluj Zapraiser Lokalizacja Usuń lokalizację @@ -710,6 +725,7 @@ Repozytorium Git: %1$s Strona internetowa: Klonuj: + OTS: %1$s Potwierdzenie znacznika czasu 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. Edytuj wpis @@ -737,4 +753,27 @@ Zapytanie o pracę z DVM Prośba o płatność została wysłana, oczekiwanie na potwierdzenie z Twojego portfela Oczekiwanie na potwierdzenie płatności lub wysłanie wyników DVM + Błędne żądanie-Serwer nie może lub nie chce przetworzyć żądania. + Nieautoryzowany - użytkownik nie ma prawidłowych danych uwierzytelniających + Wymagana płatność - serwer wymaga płatności, aby wypełnić żądanie + Zabronione - użytkownik nie ma praw dostępu, aby złożyć żądanie + Nie znaleziono - serwer nie może odnaleźć żądanego adresu + Metoda niedozwolona - serwer obsługuje metodę żądania, ale nie docelowy zasób + Niedopuszczalne - Serwer nie znalazł żadnej zawartości spełniającej żądanie. + Wymagane uwierzytelnianie proxy - użytkownik nie ma prawidłowych danych uwierzytelniania + Limit czasu żądania - serwer przekroczył limit czasu, oczekując na kogoś innego + Konflikt - serwer nie może spełnić żądania, ponieważ istnieje konflikt z zasobem + Przepadło - Żądana zawartość została trwale usunięta z serwera i nie zostanie przywrócona + Wymagana długość - serwer odrzuca żądanie, ponieważ wymaga zdefiniowanej + URI Zbyt długi - URL klienta jest zbyt długi, aby serwer mógł go przetworzyć. + Nieobsługiwany format plików - żądanie używa formatu multimediów, którego serwer nie obsługuje + Wymagana aktualizacja - serwer odmawia przetwarzania żądania przy użyciu bieżącego protokołu, chyba że klient uaktualni go do innego protokołu. + Wewnętrzny błąd serwera - serwer napotkał nieoczekiwany błąd i nie może zakończyć żądania + Nie zaimplementowano - serwer nie może spełnić żądania lub nie rozpoznaje metody żądania + Usługa niedostępna - Często zdarza się, gdy serwer jest przeciążony lub wyłączony w celu konserwacji + Wersja HTTP nie jest obsługiwana - serwer nie obsługuje wersji HTTP w zapytaniu + Wariant również Negocjacje - serwer ma błąd konfiguracji wewnętrznej + Niewystarczająca pojemność - serwer nie ma wystarczającej pojemność, aby pomyślnie przetworzyć żądanie + Wykryta pętla - serwer wykrywa nieskończoną pętlę podczas przetwarzania żądania + Wymagane uwierzytelnienie sieciowe - Klient musi być uwierzytelniony aby uzyskać dostęp do sieci From 1fafb6a503f564b383f09439e2921cff722ace54 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 28 Jun 2024 16:58:45 -0400 Subject: [PATCH 09/11] updates create release script to match new directories --- .github/workflows/create-release.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 24e124faf..a1579b7de 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -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 From 45df7713ce48db889198c11b5ef3c1bacc868e14 Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Fri, 28 Jun 2024 23:38:41 +0200 Subject: [PATCH 10/11] remove text preview from images --- .../vitorpamplona/amethyst/model/Account.kt | 3 + .../amethyst/ui/components/VideoView.kt | 11 +- .../ui/components/ZoomableContentView.kt | 16 +- .../amethyst/ui/note/NoteQuickActionMenu.kt | 306 ++++++++++++++++++ .../amethyst/ui/note/types/AudioTrack.kt | 1 - .../amethyst/ui/note/types/LiveActivity.kt | 1 - .../ui/screen/loggedIn/AccountViewModel.kt | 14 + .../ui/screen/loggedIn/ProfileGallery.kt | 20 +- .../ui/screen/loggedIn/ProfileScreen.kt | 20 +- amethyst/src/main/res/values/strings.xml | 2 + .../quartz/events/GalleryListEvent.kt | 38 +-- 11 files changed, 358 insertions(+), 74 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 903579a7c..59f8501e9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -2203,6 +2203,7 @@ class Account( url: String, ) { if (!isWriteable()) return + GalleryListEvent.addEvent( userProfile().latestGalleryList, idHex, @@ -2226,6 +2227,7 @@ class Account( GalleryListEvent.removeReplaceable( galleryentries, note.address, + url, signer, ) { Client.send(it) @@ -2235,6 +2237,7 @@ class Account( GalleryListEvent.removeEvent( galleryentries, note.idHex, + url, signer, ) { Client.send(it) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt index 8618ae622..239d88b0d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt @@ -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, 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? = null, keepPlaying: MutableState, automaticallyStartPlayback: State, @@ -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) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt index 1f1ed554a..5fed68693 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt @@ -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, 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() }, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt index 2cc7a5c62..957a0e77e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt @@ -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, + 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) + } + } + } + }, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt index f1879ea25..d7875628e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt @@ -212,7 +212,6 @@ fun AudioHeader( isFiniteHeight = isFiniteHeight, accountViewModel = accountViewModel, nostrUriCallback = note.toNostrUri(), - nostrIdCallback = note.idHex, ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt index 766aeea97..6d0a16f35 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt @@ -160,7 +160,6 @@ fun RenderLiveActivityEventInner( isFiniteHeight = false, accountViewModel = accountViewModel, nostrUriCallback = "nostr:${baseNote.toNEvent()}", - nostrIdCallback = baseNote.idHex, ) } } else { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index b1e5c4eef..5c6504503 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -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) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileGallery.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileGallery.kt index df02459ae..53271adfb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileGallery.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileGallery.kt @@ -22,15 +22,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn import androidx.compose.animation.core.tween import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement 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 @@ -39,7 +36,6 @@ 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 @@ -51,8 +47,6 @@ 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 @@ -64,7 +58,7 @@ 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 @@ -78,7 +72,6 @@ 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.TextNoteEvent @Composable @@ -247,8 +240,7 @@ fun GalleryCard( nav: (String) -> Unit, ) { // baseNote.event?.let { Text(text = it.pubKey()) } - LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup -> - + LongPressToQuickActionGallery(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup -> CheckNewAndRenderChannelCard( baseNote, modifier, @@ -400,7 +392,7 @@ fun InnerRenderGalleryThumb( ) } ?: run { DisplayGalleryAuthorBanner(note) } - Row( + /* Row( Modifier .fillMaxWidth() .background(Color.Black.copy(0.6f)) @@ -417,7 +409,7 @@ fun InnerRenderGalleryThumb( modifier = Modifier.weight(1f), ) } - /* + card.price?.let { val priceTag = remember(card) { @@ -438,8 +430,8 @@ fun InnerRenderGalleryThumb( overflow = TextOverflow.Ellipsis, color = Color.White, ) - }*/ - } + } + }*/ } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index e15700961..0a0ec78a9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -157,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 @@ -1571,14 +1572,17 @@ fun TabGallery( modifier = Modifier.padding(vertical = 0.dp), ) { var state = LazyGridState() - RenderGalleryFeed( - feedViewModel, - null, - 0, - state, - accountViewModel = accountViewModel, - nav = nav, - ) + + SaveableGridFeedState(feedViewModel, scrollStateKey = "gallery") { listState -> + RenderGalleryFeed( + feedViewModel, + null, + 0, + state, + accountViewModel = accountViewModel, + nav = nav, + ) + } } } } diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 37aa8a5e0..4803fe72d 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -270,6 +270,8 @@ Delete Unfollow Follow + Delete from Gallery + Remove this media from your Gallery, you can readd it later Request Deletion 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. Block diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/GalleryListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/GalleryListEvent.kt index 4311407c8..1b65359c0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/GalleryListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GalleryListEvent.kt @@ -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? = null, - videos: List? = null, - audios: List? = null, - privEvents: List? = null, - privUsers: List? = null, - privAddresses: List? = null, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (GalleryListEvent) -> Unit, - ) { - val tags = mutableListOf>() - 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) - } - } } } From 0d18a89177df2aa5b37b128210049cb33cd9a4af Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Fri, 28 Jun 2024 23:53:40 +0200 Subject: [PATCH 11/11] cleanup --- .../ui/screen/loggedIn/ProfileGallery.kt | 50 ------------------- 1 file changed, 50 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileGallery.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileGallery.kt index 53271adfb..41850616d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileGallery.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileGallery.kt @@ -161,14 +161,6 @@ fun GalleryCardCompose( accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - /* baseNote.event?.let { - Text(text = it.id() + " " + it.firstTaggedUrl() + " ") // TODO why does it.taggedGalleryEntries() not return something? whats different? - } baseNote.event?.let { - for (entry in it.taggedGalleryEntries()) { - Text(text = entry.url + " " + entry.id) - } - }*/ - WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel) { CheckHiddenFeedWatchBlockAndReport( note = baseNote, @@ -189,48 +181,6 @@ fun GalleryCardCompose( } } -// TODO This is to large parts from the ChannelCardCompose - -/*@OptIn(ExperimentalFoundationApi::class) -@Composable -fun ProfileGallery( - baseNote: List, - modifier: Modifier = Modifier, - parentBackgroundColor: MutableState? = null, - 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, - ) - } - } - } - } - } - } -} -*/ @Composable fun GalleryCard( baseNote: Note,