From 91caacd36dce6d3613e02eb44a056f61e306d361 Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Fri, 28 Jun 2024 16:01:56 +0200 Subject: [PATCH 01/39] added first attempt to add user galleries. can read 10011 events and shows profile tab What works: - can load existing 10011 lists and show images in profile tab What doesn't work: - gallery view is broken - can't load notes to click images to get to original note - adding media to gallery crashes amethyst atm - no functionality to delete media from gallery yet. --- .../vitorpamplona/amethyst/model/Account.kt | 48 +++ .../amethyst/model/LocalCache.kt | 15 + .../com/vitorpamplona/amethyst/model/User.kt | 14 +- .../service/NostrUserProfileDataSource.kt | 3 +- .../amethyst/ui/components/VideoView.kt | 11 +- .../ui/components/ZoomableContentDialog.kt | 2 +- .../ui/components/ZoomableContentView.kt | 26 ++ .../amethyst/ui/note/types/AudioTrack.kt | 1 + .../amethyst/ui/note/types/LiveActivity.kt | 1 + .../amethyst/ui/note/types/Video.kt | 1 + .../ui/screen/loggedIn/DiscoverScreen.kt | 14 +- .../ui/screen/loggedIn/ProfileGallery.kt | 333 ++++++++++++++++++ .../ui/screen/loggedIn/ProfileScreen.kt | 69 +++- amethyst/src/main/res/values/strings.xml | 1 + .../commons/richtext/MediaContentModels.kt | 4 + .../quartz/events/EventFactory.kt | 1 + .../quartz/events/GalleryListEvent.kt | 190 ++++++++++ 17 files changed, 714 insertions(+), 20 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileGallery.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/GalleryListEvent.kt 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 60fe2689f..2331f3c37 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -71,6 +71,7 @@ import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.FileServersEvent import com.vitorpamplona.quartz.events.FileStorageEvent import com.vitorpamplona.quartz.events.FileStorageHeaderEvent +import com.vitorpamplona.quartz.events.GalleryListEvent import com.vitorpamplona.quartz.events.GeneralListEvent import com.vitorpamplona.quartz.events.GenericRepostEvent import com.vitorpamplona.quartz.events.GiftWrapEvent @@ -2197,6 +2198,53 @@ class Account( } } + fun addToGallery( + idHex: String, + url: String, + ) { + if (!isWriteable()) return + GalleryListEvent.addEvent( + userProfile().latestGalleryList, + idHex, + url, + signer, + ) { + Client.send(it) + LocalCache.consume(it) + } + } + + fun removeFromGallery( + note: Note, + url: String, + ) { + if (!isWriteable()) return + + val galleryentries = userProfile().latestGalleryList ?: return + + if (note is AddressableNote) { + GalleryListEvent.removeReplaceable( + galleryentries, + note.address, + false, + signer, + ) { + Client.send(it) + LocalCache.consume(it) + } + } else { + GalleryListEvent.removeEvent( + galleryentries, + note.idHex, + false, + signer, + ) { + Client.send(it) + LocalCache.consume(it) + } + } + } + fun addBookmark( note: Note, isPrivate: Boolean, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 5e605af24..1034d6065 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -75,6 +75,7 @@ import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.FileServersEvent import com.vitorpamplona.quartz.events.FileStorageEvent import com.vitorpamplona.quartz.events.FileStorageHeaderEvent +import com.vitorpamplona.quartz.events.GalleryListEvent import com.vitorpamplona.quartz.events.GenericRepostEvent import com.vitorpamplona.quartz.events.GiftWrapEvent import com.vitorpamplona.quartz.events.GitIssueEvent @@ -423,6 +424,19 @@ object LocalCache { } } + fun consume(event: GalleryListEvent) { + val user = getOrCreateUser(event.pubKey) + if (user.latestGalleryList == null || event.createdAt > user.latestGalleryList!!.createdAt) { + if (event.dTag() == "gallery") { + user.updateGallery(event) + } + // Log.d("MT", "New User Metadata ${oldUser.pubkeyDisplayHex} ${oldUser.toBestDisplayName()}") + } else { + // Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()} + // ${formattedDateTime(event.createdAt)} > ${formattedDateTime(oldUser.updatedAt)}") + } + } + fun formattedDateTime(timestamp: Long): String = Instant .ofEpochSecond(timestamp) @@ -2523,6 +2537,7 @@ object LocalCache { is DraftEvent -> consume(event, relay) is EmojiPackEvent -> consume(event, relay) is EmojiPackSelectionEvent -> consume(event, relay) + is GalleryListEvent -> consume(event) is GenericRepostEvent -> { event.containedPost()?.let { verifyAndConsume(it, relay) } consume(event) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt index 01abf0ad7..2e09dbe9e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -40,6 +40,7 @@ import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent import com.vitorpamplona.quartz.events.BookmarkListEvent import com.vitorpamplona.quartz.events.ChatroomKey import com.vitorpamplona.quartz.events.ContactListEvent +import com.vitorpamplona.quartz.events.GalleryListEvent import com.vitorpamplona.quartz.events.LnZapEvent import com.vitorpamplona.quartz.events.MetadataEvent import com.vitorpamplona.quartz.events.ReportEvent @@ -60,6 +61,7 @@ class User( var latestMetadataRelay: String? = null var latestContactList: ContactListEvent? = null var latestBookmarkList: BookmarkListEvent? = null + var latestGalleryList: GalleryListEvent? = null var reports = mapOf>() private set @@ -123,6 +125,13 @@ class User( liveSet?.innerBookmarks?.invalidateData() } + fun updateGallery(event: GalleryListEvent) { + if (event.id == latestGalleryList?.id) return + print("GALLERY " + event.id()) + latestGalleryList = event + liveSet?.innerGallery?.invalidateData() + } + fun clearEOSE() { latestEOSEs = emptyMap() } @@ -488,6 +497,7 @@ class UserLiveSet( val innerRelayInfo = UserBundledRefresherLiveData(u) val innerZaps = UserBundledRefresherLiveData(u) val innerBookmarks = UserBundledRefresherLiveData(u) + val innerGallery = UserBundledRefresherLiveData(u) val innerStatuses = UserBundledRefresherLiveData(u) // UI Observers line up here. @@ -500,6 +510,7 @@ class UserLiveSet( val relayInfo = innerRelayInfo.map { it } val zaps = innerZaps.map { it } val bookmarks = innerBookmarks.map { it } + val gallery = innerGallery.map { it } val statuses = innerStatuses.map { it } val profilePictureChanges = innerMetadata.map { it.user.profilePicture() }.distinctUntilChanged() @@ -517,7 +528,7 @@ class UserLiveSet( relays.hasObservers() || relayInfo.hasObservers() || zaps.hasObservers() || - bookmarks.hasObservers() || + bookmarks.hasObservers() || gallery.hasObservers() || statuses.hasObservers() || profilePictureChanges.hasObservers() || nip05Changes.hasObservers() || @@ -533,6 +544,7 @@ class UserLiveSet( innerRelayInfo.destroy() innerZaps.destroy() innerBookmarks.destroy() + innerGallery.destroy() innerStatuses.destroy() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt index f05299d2c..fc0144b2f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt @@ -31,6 +31,7 @@ import com.vitorpamplona.quartz.events.BadgeAwardEvent import com.vitorpamplona.quartz.events.BadgeProfilesEvent import com.vitorpamplona.quartz.events.BookmarkListEvent import com.vitorpamplona.quartz.events.ContactListEvent +import com.vitorpamplona.quartz.events.GalleryListEvent import com.vitorpamplona.quartz.events.GenericRepostEvent import com.vitorpamplona.quartz.events.HighlightEvent import com.vitorpamplona.quartz.events.LnZapEvent @@ -146,7 +147,7 @@ object NostrUserProfileDataSource : AmethystNostrDataSource("UserProfileFeed") { filter = Filter( kinds = - listOf(BookmarkListEvent.KIND, PeopleListEvent.KIND, AppRecommendationEvent.KIND), + listOf(BookmarkListEvent.KIND, PeopleListEvent.KIND, AppRecommendationEvent.KIND, GalleryListEvent.KIND), authors = listOf(it.pubkeyHex), limit = 100, ), 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 463690343..8618ae622 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,6 +142,7 @@ fun LoadThumbAndThenVideoView( roundedCorner: Boolean, isFiniteHeight: Boolean, nostrUriCallback: String? = null, + nostrIdCallback: String? = null, accountViewModel: AccountViewModel, onDialog: ((Boolean) -> Unit)? = null, ) { @@ -176,6 +177,7 @@ fun LoadThumbAndThenVideoView( artworkUri = thumbUri, authorName = authorName, nostrUriCallback = nostrUriCallback, + nostrIdCallback = nostrIdCallback, accountViewModel = accountViewModel, onDialog = onDialog, ) @@ -190,6 +192,7 @@ fun LoadThumbAndThenVideoView( artworkUri = thumbUri, authorName = authorName, nostrUriCallback = nostrUriCallback, + nostrIdCallback = nostrIdCallback, accountViewModel = accountViewModel, onDialog = onDialog, ) @@ -211,6 +214,7 @@ fun VideoView( dimensions: String? = null, blurhash: String? = null, nostrUriCallback: String? = null, + nostrIdCallback: String? = null, onDialog: ((Boolean) -> Unit)? = null, onControllerVisibilityChanged: ((Boolean) -> Unit)? = null, accountViewModel: AccountViewModel, @@ -252,6 +256,7 @@ fun VideoView( artworkUri = artworkUri, authorName = authorName, nostrUriCallback = nostrUriCallback, + nostrIDCallback = nostrIdCallback, automaticallyStartPlayback = automaticallyStartPlayback, onControllerVisibilityChanged = onControllerVisibilityChanged, onDialog = onDialog, @@ -305,6 +310,7 @@ fun VideoView( artworkUri = artworkUri, authorName = authorName, nostrUriCallback = nostrUriCallback, + nostrIDCallback = nostrIdCallback, automaticallyStartPlayback = automaticallyStartPlayback, onControllerVisibilityChanged = onControllerVisibilityChanged, onDialog = onDialog, @@ -329,6 +335,7 @@ fun VideoViewInner( artworkUri: String? = null, authorName: String? = null, nostrUriCallback: String? = null, + nostrIDCallback: String? = null, automaticallyStartPlayback: State, onControllerVisibilityChanged: ((Boolean) -> Unit)? = null, onDialog: ((Boolean) -> Unit)? = null, @@ -350,6 +357,7 @@ fun VideoViewInner( roundedCorner = roundedCorner, isFiniteHeight = isFiniteHeight, nostrUriCallback = nostrUriCallback, + nostrIDCallback = nostrIDCallback, waveform = waveform, keepPlaying = keepPlaying, automaticallyStartPlayback = automaticallyStartPlayback, @@ -697,6 +705,7 @@ private fun RenderVideoPlayer( roundedCorner: Boolean, isFiniteHeight: Boolean, nostrUriCallback: String?, + nostrIDCallback: String?, waveform: ImmutableList? = null, keepPlaying: MutableState, automaticallyStartPlayback: State, @@ -810,7 +819,7 @@ private fun RenderVideoPlayer( } AnimatedShareButton(controllerVisible, Modifier.align(Alignment.TopEnd).padding(end = Size165dp)) { popupExpanded, toggle -> - ShareImageAction(popupExpanded, videoUri, nostrUriCallback, toggle) + ShareImageAction(accountViewModel = accountViewModel, popupExpanded, videoUri, nostrUriCallback, nostrIDCallback, toggle) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt index d028c9ce8..445045dd1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt @@ -269,7 +269,7 @@ private fun DialogContent( contentDescription = stringRes(R.string.quick_action_share), ) - ShareImageAction(popupExpanded = popupExpanded, myContent, onDismiss = { popupExpanded.value = false }) + ShareImageAction(accountViewModel = accountViewModel, popupExpanded = popupExpanded, myContent, onDismiss = { popupExpanded.value = false }) } val localContext = LocalContext.current 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 195cdac5f..1f1ed554a 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 @@ -138,6 +138,7 @@ fun ZoomableContentView( roundedCorner = roundedCorner, isFiniteHeight = isFiniteHeight, nostrUriCallback = content.uri, + nostrIdCallback = content.id, onDialog = { dialogOpen = true }, accountViewModel = accountViewModel, ) @@ -162,6 +163,7 @@ fun ZoomableContentView( roundedCorner = roundedCorner, isFiniteHeight = isFiniteHeight, nostrUriCallback = content.uri, + nostrIdCallback = content.id, onDialog = { dialogOpen = true }, accountViewModel = accountViewModel, ) @@ -596,22 +598,27 @@ fun DisplayBlurHash( @Composable fun ShareImageAction( + accountViewModel: AccountViewModel, popupExpanded: MutableState, content: BaseMediaContent, onDismiss: () -> Unit, ) { if (content is MediaUrlContent) { ShareImageAction( + accountViewModel = accountViewModel, popupExpanded = popupExpanded, videoUri = content.url, postNostrUri = content.uri, + postNostrid = content.id, onDismiss = onDismiss, ) } else if (content is MediaPreloadedContent) { ShareImageAction( + accountViewModel = accountViewModel, popupExpanded = popupExpanded, videoUri = content.localFile?.toUri().toString(), postNostrUri = content.uri, + postNostrid = content.id, onDismiss = onDismiss, ) } @@ -620,9 +627,11 @@ fun ShareImageAction( @OptIn(ExperimentalPermissionsApi::class) @Composable fun ShareImageAction( + accountViewModel: AccountViewModel, popupExpanded: MutableState, videoUri: String?, postNostrUri: String?, + postNostrid: String?, onDismiss: () -> Unit, ) { DropdownMenu( @@ -650,6 +659,23 @@ fun ShareImageAction( }, ) } + + postNostrUri?.let { + DropdownMenuItem( + 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) + } + } + onDismiss() + }, + ) + } } } 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 d7875628e..f1879ea25 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,6 +212,7 @@ 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 6d0a16f35..766aeea97 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,6 +160,7 @@ 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/note/types/Video.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt index 080aca811..0f43d93c2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt @@ -86,6 +86,7 @@ fun VideoDisplay( val description = event.content.ifBlank { null } ?: event.alt() val isImage = event.mimeType()?.startsWith("image/") == true || RichTextParser.isImageUrl(fullUrl) val uri = note.toNostrUri() + val id = note.id() val mimeType = event.mimeType() mutableStateOf( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt index 250eca54f..2847381aa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt @@ -118,13 +118,6 @@ fun DiscoverScreen( ScrollStateKeys.DISCOVER_CONTENT, AppDefinitionEvent.KIND, ), - TabItem( - R.string.discover_marketplace, - discoveryMarketplaceFeedViewModel, - Route.Discover.base + "Marketplace", - ScrollStateKeys.DISCOVER_MARKETPLACE, - ClassifiedsEvent.KIND, - ), TabItem( R.string.discover_live, discoveryLiveFeedViewModel, @@ -139,6 +132,13 @@ fun DiscoverScreen( ScrollStateKeys.DISCOVER_COMMUNITY, CommunityDefinitionEvent.KIND, ), + TabItem( + R.string.discover_marketplace, + discoveryMarketplaceFeedViewModel, + Route.Discover.base + "Marketplace", + ScrollStateKeys.DISCOVER_MARKETPLACE, + ClassifiedsEvent.KIND, + ), TabItem( R.string.discover_chat, discoveryChatFeedViewModel, 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 new file mode 100644 index 000000000..08204061f --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileGallery.kt @@ -0,0 +1,333 @@ +/** + * 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.screen.loggedIn + +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.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Alignment.Companion.BottomStart +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.distinctUntilChanged +import androidx.lifecycle.map +import coil.compose.AsyncImage +import com.vitorpamplona.amethyst.model.Note +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.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.theme.HalfPadding +import com.vitorpamplona.amethyst.ui.theme.QuoteBorder +import com.vitorpamplona.amethyst.ui.theme.Size5dp +import com.vitorpamplona.quartz.events.GalleryListEvent + +// TODO This is to large parts from the ChannelCardCompose +// Why does it not be in a grid, like the marketplace +@Composable +fun ProfileGallery( + baseNotes: 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, + url: String, + modifier: Modifier = Modifier, + parentBackgroundColor: MutableState? = null, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + // baseNote.event?.let { Text(text = it.pubKey()) } + LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup -> + + CheckNewAndRenderChannelCard( + baseNote, + url, + modifier, + parentBackgroundColor, + accountViewModel, + showPopup, + nav, + ) + } +} + +@Composable +private fun CheckNewAndRenderChannelCard( + baseNote: Note, + url: String, + modifier: Modifier = Modifier, + parentBackgroundColor: MutableState? = null, + accountViewModel: AccountViewModel, + showPopup: () -> Unit, + nav: (String) -> Unit, +) { + val backgroundColor = + calculateBackgroundColor( + createdAt = baseNote.createdAt(), + parentBackgroundColor = parentBackgroundColor, + accountViewModel = accountViewModel, + ) + + ClickableNote( + baseNote = baseNote, + backgroundColor = backgroundColor, + modifier = modifier, + accountViewModel = accountViewModel, + showPopup = showPopup, + nav = nav, + ) { + // baseNote.event?.let { Text(text = it.pubKey()) } + InnerGalleryCardWithReactions( + baseNote = baseNote, + url = url, + accountViewModel = accountViewModel, + nav = nav, + ) + } +} + +@Composable +fun InnerGalleryCardWithReactions( + baseNote: Note, + url: String, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + InnerGalleryCardBox(baseNote, url, accountViewModel, nav) +} + +@Composable +fun InnerGalleryCardBox( + baseNote: Note, + url: String, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + Column(HalfPadding) { + SensitivityWarning( + note = baseNote, + accountViewModel = accountViewModel, + ) { + RenderGalleryThumb(baseNote, url, accountViewModel, nav) + } + } +} + +@Immutable +data class GalleryThumb( + val baseNote: Note?, + val id: String?, + val image: String?, + val title: String?, + // val price: Price?, +) + +@Composable +fun RenderGalleryThumb( + baseNote: Note, + url: String, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val noteEvent = baseNote.event as? GalleryListEvent ?: return + + val card by + baseNote + .live() + .metadata + .map { + val noteEvent = baseNote.event as GalleryListEvent + + GalleryThumb( + baseNote = baseNote, + id = "", + image = url, + title = "Hello", + // 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.image(), + // title = noteEvent.title(), + // price = noteEvent.price(), + ), + ) + + InnerRenderGalleryThumb(card as GalleryThumb, baseNote) +} + +@Preview +@Composable +fun RenderGalleryThumbPreview() { + Surface(Modifier.size(200.dp)) { + InnerRenderGalleryThumb( + card = + GalleryThumb( + baseNote = null, + id = "", + image = null, + title = "Like New", + // price = Price("800000", "SATS", null), + ), + note = Note("hex"), + ) + } +} + +@Composable +fun InnerRenderGalleryThumb( + card: GalleryThumb, + note: Note, +) { + Box( + Modifier + .fillMaxWidth() + .aspectRatio(1f), + contentAlignment = BottomStart, + ) { + card.image?.let { + AsyncImage( + model = it, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize(), + ) + } ?: run { DisplayGalleryAuthorBanner(note) } + + Row( + Modifier + .fillMaxWidth() + .background(Color.Black.copy(0.6f)) + .padding(Size5dp), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + card.title?.let { + Text( + text = it, + fontWeight = FontWeight.Medium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = Color.White, + modifier = Modifier.weight(1f), + ) + } + /* + card.price?.let { + val priceTag = + remember(card) { + val newAmount = it.amount.toBigDecimalOrNull()?.let { showAmountAxis(it) } ?: it.amount + + if (it.frequency != null && it.currency != null) { + "$newAmount ${it.currency}/${it.frequency}" + } else if (it.currency != null) { + "$newAmount ${it.currency}" + } else { + newAmount + } + } + + Text( + text = priceTag, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = Color.White, + ) + }*/ + } + } +} + +@Composable +fun DisplayGalleryAuthorBanner(note: Note) { + WatchAuthor(note) { + BannerImage( + it, + Modifier + .fillMaxSize() + .clip(QuoteBorder), + ) + } +} 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 a7d3e1aca..735444d82 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 @@ -433,7 +433,8 @@ private fun RenderSurface( } } }, - ).fillMaxHeight() + ) + .fillMaxHeight() }, ) { RenderScreen( @@ -532,13 +533,14 @@ private fun CreateAndRenderPages( when (page) { 0 -> TabNotesNewThreads(threadsViewModel, accountViewModel, nav) 1 -> TabNotesConversations(repliesViewModel, accountViewModel, nav) - 2 -> TabFollows(baseUser, followsFeedViewModel, accountViewModel, nav) - 3 -> TabFollowers(baseUser, followersFeedViewModel, accountViewModel, nav) - 4 -> TabReceivedZaps(baseUser, zapFeedViewModel, accountViewModel, nav) - 5 -> TabBookmarks(bookmarksFeedViewModel, accountViewModel, nav) - 6 -> TabFollowedTags(baseUser, accountViewModel, nav) - 7 -> TabReports(baseUser, reportsFeedViewModel, accountViewModel, nav) - 8 -> TabRelays(baseUser, accountViewModel, nav) + 2 -> Gallery(baseUser, followsFeedViewModel, accountViewModel, nav) + 3 -> TabFollows(baseUser, followsFeedViewModel, accountViewModel, nav) + 4 -> TabFollowers(baseUser, followersFeedViewModel, accountViewModel, nav) + 5 -> TabReceivedZaps(baseUser, zapFeedViewModel, accountViewModel, nav) + 6 -> TabBookmarks(bookmarksFeedViewModel, accountViewModel, nav) + 7 -> TabFollowedTags(baseUser, accountViewModel, nav) + 8 -> TabReports(baseUser, reportsFeedViewModel, accountViewModel, nav) + 9 -> TabRelays(baseUser, accountViewModel, nav) } } @@ -573,6 +575,7 @@ private fun CreateAndRenderTabs( listOf<@Composable (() -> Unit)?>( { Text(text = stringRes(R.string.notes)) }, { Text(text = stringRes(R.string.replies)) }, + { Text(text = "Gallery") }, { FollowTabHeader(baseUser) }, { FollowersTabHeader(baseUser) }, { ZapTabHeader(baseUser) }, @@ -1534,6 +1537,50 @@ fun TabNotesConversations( } } +@Composable +fun Gallery( + baseUser: User, + feedViewModel: UserFeedViewModel, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + WatchFollowChanges(baseUser, feedViewModel) + + Column(Modifier.fillMaxHeight()) { + Column { + baseUser.latestGalleryList?.let { + // val note2 = getOrCreateAddressableNoteInternal(aTag) + val note = LocalCache.getOrCreateAddressableNote(it.address()) + note.event = it + var notes = listOf() + for (tag in note.event?.tags()!!) { + if (tag.size > 2) { + if (tag[0] == "g") { + // TODO get the node by id on main thread. LoadNote does nothing. + val thumb = + GalleryThumb( + baseNote = note, + id = tag[2], + // TODO use the original note once it's loaded baseNote = basenote, + image = tag[1], + title = null, + ) + notes = notes + thumb + // } + } + } + ProfileGallery( + baseNotes = notes, + modifier = Modifier, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } + } + } +} + @Composable fun TabFollowedTags( baseUser: User, @@ -1545,7 +1592,11 @@ fun TabFollowedTags( baseUser.latestContactList?.unverifiedFollowTagSet() } - Column(Modifier.fillMaxHeight().padding(vertical = 0.dp)) { + Column( + Modifier + .fillMaxHeight() + .padding(vertical = 0.dp), + ) { items?.let { LazyColumn { itemsIndexed(items) { index, hashtag -> diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 6d8f93237..37aa8a5e0 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -586,6 +586,7 @@ Share or Save Copy URL to clipboard Copy Note ID to clipboard + Add Media to Gallery Created at Rules diff --git a/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/MediaContentModels.kt b/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/MediaContentModels.kt index 2db6e28db..28ee73ee7 100644 --- a/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/MediaContentModels.kt +++ b/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/MediaContentModels.kt @@ -38,6 +38,7 @@ abstract class MediaUrlContent( dim: String? = null, blurhash: String? = null, val uri: String? = null, + val id: String? = null, val mimeType: String? = null, ) : BaseMediaContent(description, dim, blurhash) @@ -49,6 +50,7 @@ class MediaUrlImage( blurhash: String? = null, dim: String? = null, uri: String? = null, + id: String? = null, val contentWarning: String? = null, mimeType: String? = null, ) : MediaUrlContent(url, description, hash, dim, blurhash, uri, mimeType) @@ -60,6 +62,7 @@ class MediaUrlVideo( hash: String? = null, dim: String? = null, uri: String? = null, + id: String? = null, val artworkUri: String? = null, val authorName: String? = null, blurhash: String? = null, @@ -76,6 +79,7 @@ abstract class MediaPreloadedContent( dim: String? = null, blurhash: String? = null, val uri: String, + val id: String? = null, ) : BaseMediaContent(description, dim, blurhash) { fun localFileExists() = localFile != null && localFile.exists() } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt index d8e071057..759a0bd17 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt @@ -97,6 +97,7 @@ class EventFactory { GitPatchEvent.KIND -> GitPatchEvent(id, pubKey, createdAt, tags, content, sig) GitRepositoryEvent.KIND -> GitRepositoryEvent(id, pubKey, createdAt, tags, content, sig) GoalEvent.KIND -> GoalEvent(id, pubKey, createdAt, tags, content, sig) + GalleryListEvent.KIND -> GalleryListEvent(id, pubKey, createdAt, tags, content, sig) HighlightEvent.KIND -> HighlightEvent(id, pubKey, createdAt, tags, content, sig) HTTPAuthorizationEvent.KIND -> HTTPAuthorizationEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/GalleryListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/GalleryListEvent.kt new file mode 100644 index 000000000..d43cd5523 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GalleryListEvent.kt @@ -0,0 +1,190 @@ +/** + * 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.quartz.events + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.encoders.ATag +import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class GalleryListEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : GeneralListEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + companion object { + const val KIND = 10011 + const val ALT = "Gallery List" + const val DEFAULT_D_TAG_GALLERY = "gallery" + + fun addEvent( + earlierVersion: GalleryListEvent?, + eventId: HexKey, + url: String, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (GalleryListEvent) -> Unit, + ) = addTag(earlierVersion, "g", eventId, url, signer, createdAt, onReady) + + fun addTag( + earlierVersion: GalleryListEvent?, + tagName: String, + tagValue: HexKey, + url: String, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (GalleryListEvent) -> Unit, + ) { + add( + earlierVersion, + arrayOf(arrayOf(tagName, url, tagValue)), + signer, + createdAt, + onReady, + ) + } + + fun add( + earlierVersion: GalleryListEvent?, + listNewTags: Array>, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (GalleryListEvent) -> Unit, + ) { + create( + content = earlierVersion?.content ?: "", + tags = (earlierVersion?.tags ?: arrayOf(arrayOf("d", DEFAULT_D_TAG_GALLERY))).plus(listNewTags), + signer = signer, + createdAt = createdAt, + onReady = onReady, + ) + } + + fun removeEvent( + earlierVersion: GalleryListEvent, + eventId: HexKey, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (GalleryListEvent) -> Unit, + ) = removeTag(earlierVersion, "e", eventId, isPrivate, signer, createdAt, onReady) + + fun removeReplaceable( + earlierVersion: GalleryListEvent, + aTag: ATag, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (GalleryListEvent) -> Unit, + ) = removeTag(earlierVersion, "a", aTag.toTag(), isPrivate, signer, createdAt, onReady) + + private fun removeTag( + earlierVersion: GalleryListEvent, + tagName: String, + tagValue: HexKey, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (GalleryListEvent) -> Unit, + ) { + if (isPrivate) { + earlierVersion.privateTagsOrEmpty(signer) { privateTags -> + encryptTags( + privateTags = + privateTags + .filter { it.size <= 1 || !(it[0] == tagName && it[1] == tagValue) } + .toTypedArray(), + signer = signer, + ) { encryptedTags -> + create( + content = encryptedTags, + tags = + earlierVersion.tags + .filter { it.size <= 1 || !(it[0] == tagName && it[1] == tagValue) } + .toTypedArray(), + signer = signer, + createdAt = createdAt, + onReady = onReady, + ) + } + } + } else { + create( + content = earlierVersion.content, + tags = + earlierVersion.tags + .filter { it.size <= 1 || !(it[0] == tagName && it[1] == tagValue) } + .toTypedArray(), + signer = signer, + createdAt = createdAt, + onReady = onReady, + ) + } + } + + fun create( + content: String, + tags: Array>, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (GalleryListEvent) -> Unit, + ) { + val newTags = + if (tags.any { it.size > 1 && it[0] == "alt" }) { + tags + } else { + tags + arrayOf("alt", ALT) + } + + 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("image", it)) } + videos?.forEach { tags.add(arrayOf("video", it)) } + audios?.forEach { tags.add(arrayOf("audio", it)) } + tags.add(arrayOf("alt", ALT)) + + createPrivateTags(privEvents, privUsers, privAddresses, signer) { content -> + signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) + } + } + } +} From e073e58241ba46727ec3ba2b057a3722e76700e3 Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Fri, 28 Jun 2024 16:51:09 +0200 Subject: [PATCH 02/39] Update GalleryListEvent.kt --- .../quartz/events/GalleryListEvent.kt | 56 +++++-------------- 1 file changed, 15 insertions(+), 41 deletions(-) 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 d43cd5523..4311407c8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/GalleryListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GalleryListEvent.kt @@ -86,63 +86,37 @@ class GalleryListEvent( fun removeEvent( earlierVersion: GalleryListEvent, eventId: HexKey, - isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (GalleryListEvent) -> Unit, - ) = removeTag(earlierVersion, "e", eventId, isPrivate, signer, createdAt, onReady) + ) = removeTag(earlierVersion, "e", eventId, signer, createdAt, onReady) fun removeReplaceable( earlierVersion: GalleryListEvent, aTag: ATag, - isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (GalleryListEvent) -> Unit, - ) = removeTag(earlierVersion, "a", aTag.toTag(), isPrivate, signer, createdAt, onReady) + ) = removeTag(earlierVersion, "a", aTag.toTag(), signer, createdAt, onReady) private fun removeTag( earlierVersion: GalleryListEvent, tagName: String, tagValue: HexKey, - isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (GalleryListEvent) -> Unit, ) { - if (isPrivate) { - earlierVersion.privateTagsOrEmpty(signer) { privateTags -> - encryptTags( - privateTags = - privateTags - .filter { it.size <= 1 || !(it[0] == tagName && it[1] == tagValue) } - .toTypedArray(), - signer = signer, - ) { encryptedTags -> - create( - content = encryptedTags, - tags = - earlierVersion.tags - .filter { it.size <= 1 || !(it[0] == tagName && it[1] == tagValue) } - .toTypedArray(), - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - } - } else { - create( - content = earlierVersion.content, - tags = - earlierVersion.tags - .filter { it.size <= 1 || !(it[0] == tagName && it[1] == tagValue) } - .toTypedArray(), - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } + create( + content = earlierVersion.content, + tags = + earlierVersion.tags + .filter { it.size <= 1 || !(it[0] == tagName && it[1] == tagValue) } + .toTypedArray(), + signer = signer, + createdAt = createdAt, + onReady = onReady, + ) } fun create( @@ -177,9 +151,9 @@ class GalleryListEvent( val tags = mutableListOf>() tags.add(arrayOf("d", name)) - images?.forEach { tags.add(arrayOf("image", it)) } - videos?.forEach { tags.add(arrayOf("video", it)) } - audios?.forEach { tags.add(arrayOf("audio", it)) } + 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 -> 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/39] 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 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 04/39] 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 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 05/39] 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 06/39] 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, From 16eb6097cef73e573e1ec2b1cdab1c6ea52a405f Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Sat, 29 Jun 2024 15:08:58 +0200 Subject: [PATCH 07/39] actually use the image from the 10011 event --- .../java/com/vitorpamplona/amethyst/model/Note.kt | 2 ++ .../amethyst/ui/dal/UserProfileGalleryFeedFilter.kt | 12 ++++++++++-- .../amethyst/ui/note/NoteQuickActionMenu.kt | 6 +++++- .../amethyst/ui/screen/loggedIn/ProfileGallery.kt | 7 ++++--- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 055476c11..acc2ae0ad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -139,6 +139,8 @@ open class Note( var relays = listOf() private set + var headerImage: String? = null + var lastReactionsDownloadTime: Map = emptyMap() fun id() = Hex.decode(idHex) 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 8b81e5bde..59faa9376 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 @@ -34,7 +34,7 @@ class UserProfileGalleryFeedFilter(val user: User, val account: Account) : FeedF val notes = user.latestGalleryList ?.taggedGalleryEntries() - ?.mapNotNull { LocalCache.checkGetOrCreateNote(it.id) } + ?.map { Pair(LocalCache.getOrCreateNote(it.id), it.url) } ?.toSet() ?: emptySet() @@ -46,7 +46,15 @@ class UserProfileGalleryFeedFilter(val user: User, val account: Account) : FeedF ?: emptySet() */ // .sortedWith(DefaultFeedOrder) - return (notes) + + var finalnotes = setOf() + for (pair in notes) { + pair.first.headerImage = pair.second + finalnotes = finalnotes + pair.first + } + println(finalnotes) + + return (finalnotes) .filter { account.isAcceptable(it) } } } 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 957a0e77e..aa442f935 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 @@ -669,7 +669,11 @@ fun DeleteFromGalleryDialog( buttonIcon = Icons.Default.Delete, buttonText = stringRes(R.string.quick_action_delete_dialog_btn), onClickDoOnce = { - note.event?.firstTaggedUrl()?.let { accountViewModel.removefromMediaGallery(note, it) } + note.headerImage.let { + if (it != null) { + accountViewModel.removefromMediaGallery(note, it) + } + } onDismiss() }, onDismiss = onDismiss, 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 41850616d..606916fb7 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 @@ -282,10 +282,9 @@ fun RenderGalleryThumb( .metadata .map { val noteEvent = baseNote.event as TextNoteEvent - GalleryThumb( id = "", - image = noteEvent.firstTaggedUrl(), + image = baseNote.headerImage, title = noteEvent.content(), // noteEvent?.title(), // price = noteEvent?.price(), @@ -294,7 +293,7 @@ fun RenderGalleryThumb( .observeAsState( GalleryThumb( id = "", - image = noteEvent.firstTaggedUrl(), + image = baseNote.headerImage, title = noteEvent.content(), // image = noteEvent.image(), // title = noteEvent.title(), @@ -342,6 +341,8 @@ fun InnerRenderGalleryThumb( ) } ?: run { DisplayGalleryAuthorBanner(note) } + // TODO what if video? + /* Row( Modifier .fillMaxWidth() From 9fe08f6c458559d0b91bcfb6739c5ea14d7df963 Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Sun, 30 Jun 2024 22:29:11 +0200 Subject: [PATCH 08/39] allow videos in the gallery --- .../amethyst/ui/components/VideoView.kt | 25 +++++++---- .../ui/dal/UserProfileGalleryFeedFilter.kt | 22 ++++----- .../ui/screen/loggedIn/ProfileGallery.kt | 45 +++++++++++++------ .../vitorpamplona/amethyst/ui/theme/Shape.kt | 1 + .../vitorpamplona/amethyst/ui/theme/Theme.kt | 30 +++++++++++++ 5 files changed, 92 insertions(+), 31 deletions(-) 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 239d88b0d..41e6fe1cb 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 @@ -109,15 +109,13 @@ import com.vitorpamplona.amethyst.ui.note.MutedIcon import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.PinBottomIconSize -import com.vitorpamplona.amethyst.ui.theme.Size110dp -import com.vitorpamplona.amethyst.ui.theme.Size165dp import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.Size22Modifier import com.vitorpamplona.amethyst.ui.theme.Size50Modifier -import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.amethyst.ui.theme.Size75dp import com.vitorpamplona.amethyst.ui.theme.VolumeBottomIconSize import com.vitorpamplona.amethyst.ui.theme.imageModifier +import com.vitorpamplona.amethyst.ui.theme.videoGalleryModifier import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers @@ -204,6 +202,7 @@ fun VideoView( title: String? = null, thumb: VideoThumb? = null, roundedCorner: Boolean, + gallery: Boolean = false, isFiniteHeight: Boolean, waveform: ImmutableList? = null, artworkUri: String? = null, @@ -247,6 +246,7 @@ fun VideoView( title = title, thumb = thumb, roundedCorner = roundedCorner, + gallery = gallery, isFiniteHeight = isFiniteHeight, waveform = waveform, artworkUri = artworkUri, @@ -324,6 +324,7 @@ fun VideoViewInner( title: String? = null, thumb: VideoThumb? = null, roundedCorner: Boolean, + gallery: Boolean = false, isFiniteHeight: Boolean, waveform: ImmutableList? = null, artworkUri: String? = null, @@ -348,6 +349,7 @@ fun VideoViewInner( controller = controller, thumbData = thumb, roundedCorner = roundedCorner, + gallery = gallery, isFiniteHeight = isFiniteHeight, nostrUriCallback = nostrUriCallback, waveform = waveform, @@ -695,6 +697,7 @@ private fun RenderVideoPlayer( controller: MediaController, thumbData: VideoThumb?, roundedCorner: Boolean, + gallery: Boolean = false, isFiniteHeight: Boolean, nostrUriCallback: String?, waveform: ImmutableList? = null, @@ -712,13 +715,18 @@ private fun RenderVideoPlayer( Box { val borders = MaterialTheme.colorScheme.imageModifier - + val bordersSquare = MaterialTheme.colorScheme.videoGalleryModifier val myModifier = remember(controller) { if (roundedCorner) { modifier.then( borders.defaultMinSize(minHeight = 75.dp).align(Alignment.Center), ) + } else if (gallery) { + Modifier + modifier.then( + bordersSquare.defaultMinSize(minHeight = 75.dp).align(Alignment.Center), + ) } else { modifier.fillMaxWidth().defaultMinSize(minHeight = 75.dp).align(Alignment.Center) } @@ -737,6 +745,7 @@ private fun RenderVideoPlayer( setBackgroundColor(Color.Transparent.toArgb()) setShutterBackgroundColor(Color.Transparent.toArgb()) controllerAutoShow = false + useController = !gallery thumbData?.thumb?.let { defaultArtwork = it } hideController() resizeMode = @@ -745,7 +754,7 @@ private fun RenderVideoPlayer( } else { AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH } - onDialog?.let { innerOnDialog -> + /*onDialog?.let { innerOnDialog -> setFullscreenButtonClickListener { controller.pause() innerOnDialog(it) @@ -756,11 +765,11 @@ private fun RenderVideoPlayer( controllerVisible.value = visible == View.VISIBLE onControllerVisibilityChanged?.let { callback -> callback(visible == View.VISIBLE) } }, - ) + ) */ } }, ) - +/* waveform?.let { Waveform(it, controller, remember { Modifier.align(Alignment.Center) }) } val startingMuteState = remember(controller) { controller.volume < 0.001 } @@ -811,7 +820,7 @@ private fun RenderVideoPlayer( AnimatedShareButton(controllerVisible, Modifier.align(Alignment.TopEnd).padding(end = Size165dp)) { popupExpanded, toggle -> ShareImageAction(accountViewModel = accountViewModel, popupExpanded, videoUri, nostrUriCallback, toggle) - } + } */ } } 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 59faa9376..8d726b89c 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 @@ -34,19 +34,21 @@ class UserProfileGalleryFeedFilter(val user: User, val account: Account) : FeedF val notes = user.latestGalleryList ?.taggedGalleryEntries() - ?.map { Pair(LocalCache.getOrCreateNote(it.id), it.url) } + ?.map { + Pair( + // ( + // if (ATag.isATag(it.id)) { + // ATag.parse(it.id, null)?.let { it1 -> LocalCache.getOrCreateAddressableNote(it1) } + // } else { + LocalCache.getOrCreateNote(it.id), + // } + // )!! + it.url, + ) + } ?.toSet() ?: emptySet() - /*val addresses = - user.latestGalleryList - ?.taggedAddresses() - ?.map { LocalCache.getOrCreateAddressableNote(it) } - ?.toSet() - ?: emptySet() */ - - // .sortedWith(DefaultFeedOrder) - var finalnotes = setOf() for (pair in notes) { pair.first.headerImage = pair.second 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 606916fb7..0bde747d0 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 @@ -56,6 +56,7 @@ 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.components.VideoView import com.vitorpamplona.amethyst.ui.note.CheckHiddenFeedWatchBlockAndReport import com.vitorpamplona.amethyst.ui.note.ClickableNote import com.vitorpamplona.amethyst.ui.note.LongPressToQuickActionGallery @@ -301,12 +302,12 @@ fun RenderGalleryThumb( ), ) - InnerRenderGalleryThumb(card as GalleryThumb, baseNote) + InnerRenderGalleryThumb(card as GalleryThumb, baseNote, accountViewModel) } @Preview @Composable -fun RenderGalleryThumbPreview() { +fun RenderGalleryThumbPreview(accountViewModel: AccountViewModel) { Surface(Modifier.size(200.dp)) { InnerRenderGalleryThumb( card = @@ -317,6 +318,7 @@ fun RenderGalleryThumbPreview() { // price = Price("800000", "SATS", null), ), note = Note("hex"), + accountViewModel = accountViewModel, ) } } @@ -325,6 +327,7 @@ fun RenderGalleryThumbPreview() { fun InnerRenderGalleryThumb( card: GalleryThumb, note: Note, + accountViewModel: AccountViewModel, ) { Box( Modifier @@ -333,15 +336,31 @@ fun InnerRenderGalleryThumb( contentAlignment = BottomStart, ) { card.image?.let { - AsyncImage( - model = it, - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize(), - ) - } ?: run { DisplayGalleryAuthorBanner(note) } - - // TODO what if video? + if (it.endsWith("mp4") || it.endsWith("mov") || it.endsWith("mpeg")) { + // TODO how to long press? + VideoView( + videoUri = it, + mimeType = null, + title = "", + dimensions = "1x1", + authorName = note.author?.toBestDisplayName(), + roundedCorner = false, + gallery = true, + isFiniteHeight = false, + alwaysShowVideo = true, + accountViewModel = accountViewModel, + ) + } else { + AsyncImage( + model = it, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize(), + ) + } + } + ?: run { DisplayGalleryAuthorBanner(note) } + } /* Row( Modifier @@ -382,8 +401,8 @@ fun InnerRenderGalleryThumb( color = Color.White, ) } - }*/ - } + } + }*/ } @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt index 2af199445..954c525c8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt @@ -56,6 +56,7 @@ val TabRowHeight = Modifier val SmallBorder = RoundedCornerShape(7.dp) val SmallishBorder = RoundedCornerShape(9.dp) val QuoteBorder = RoundedCornerShape(15.dp) + val ButtonBorder = RoundedCornerShape(20.dp) val EditFieldBorder = RoundedCornerShape(25.dp) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt index ef220986b..4fedca007 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt @@ -42,6 +42,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalView @@ -124,6 +125,18 @@ val LightImageModifier = .clip(shape = QuoteBorder) .border(1.dp, LightSubtleBorder, QuoteBorder) +val DarkVideoModifier = + Modifier + .fillMaxWidth() + .clip(shape = RectangleShape) + .border(1.dp, DarkSubtleBorder, RectangleShape) + +val LightVideoModifier = + Modifier + .fillMaxWidth() + .clip(shape = RectangleShape) + .border(1.dp, LightSubtleBorder, RectangleShape) + val DarkProfile35dpModifier = Modifier .size(Size35dp) @@ -148,6 +161,20 @@ val LightReplyBorderModifier = .clip(shape = QuoteBorder) .border(1.dp, LightSubtleBorder, QuoteBorder) +val DarkVideoBorderModifier = + Modifier + .padding(top = 5.dp) + .fillMaxWidth() + .clip(shape = RectangleShape) + .border(1.dp, DarkSubtleBorder, RectangleShape) + +val LightVideoBorderModifier = + Modifier + .padding(top = 2.dp, bottom = 0.dp, start = 0.dp, end = 0.dp) + .fillMaxWidth() + .clip(shape = RectangleShape) + .border(1.dp, LightSubtleBorder, RectangleShape) + val DarkInnerPostBorderModifier = Modifier .padding(vertical = 5.dp) @@ -356,6 +383,9 @@ val ColorScheme.markdownStyle: RichTextStyle val ColorScheme.imageModifier: Modifier get() = if (isLight) LightImageModifier else DarkImageModifier +val ColorScheme.videoGalleryModifier: Modifier + get() = if (isLight) LightVideoModifier else DarkVideoModifier + val ColorScheme.profile35dpModifier: Modifier get() = if (isLight) LightProfile35dpModifier else DarkProfile35dpModifier From ec065a65955fbeb1a367daa889189201e0aec4a4 Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Sun, 30 Jun 2024 23:14:37 +0200 Subject: [PATCH 09/39] readd regular video elements for regular playing, use isVideo funtion to check --- .../amethyst/ui/components/VideoView.kt | 110 ++++++++++-------- .../ui/screen/loggedIn/ProfileGallery.kt | 4 +- 2 files changed, 61 insertions(+), 53 deletions(-) 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 41e6fe1cb..363868d52 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 @@ -109,9 +109,12 @@ import com.vitorpamplona.amethyst.ui.note.MutedIcon import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.PinBottomIconSize +import com.vitorpamplona.amethyst.ui.theme.Size110dp +import com.vitorpamplona.amethyst.ui.theme.Size165dp import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.Size22Modifier import com.vitorpamplona.amethyst.ui.theme.Size50Modifier +import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.amethyst.ui.theme.Size75dp import com.vitorpamplona.amethyst.ui.theme.VolumeBottomIconSize import com.vitorpamplona.amethyst.ui.theme.imageModifier @@ -754,73 +757,78 @@ private fun RenderVideoPlayer( } else { AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH } - /*onDialog?.let { innerOnDialog -> - setFullscreenButtonClickListener { - controller.pause() - innerOnDialog(it) + if (!gallery) { + onDialog?.let { innerOnDialog -> + setFullscreenButtonClickListener { + controller.pause() + innerOnDialog(it) + } } + setControllerVisibilityListener( + PlayerView.ControllerVisibilityListener { visible -> + controllerVisible.value = visible == View.VISIBLE + onControllerVisibilityChanged?.let { callback -> callback(visible == View.VISIBLE) } + }, + ) } - setControllerVisibilityListener( - PlayerView.ControllerVisibilityListener { visible -> - controllerVisible.value = visible == View.VISIBLE - onControllerVisibilityChanged?.let { callback -> callback(visible == View.VISIBLE) } - }, - ) */ } }, ) -/* + waveform?.let { Waveform(it, controller, remember { Modifier.align(Alignment.Center) }) } + if (!gallery) { + val startingMuteState = remember(controller) { controller.volume < 0.001 } - val startingMuteState = remember(controller) { controller.volume < 0.001 } + MuteButton( + controllerVisible, + startingMuteState, + Modifier.align(Alignment.TopEnd), + ) { mute: Boolean -> + // makes the new setting the default for new creations. + DEFAULT_MUTED_SETTING.value = mute - MuteButton( - controllerVisible, - startingMuteState, - Modifier.align(Alignment.TopEnd), - ) { mute: Boolean -> - // makes the new setting the default for new creations. - DEFAULT_MUTED_SETTING.value = mute - - // if the user unmutes a video and it's not the current playing, switches to that one. - if (!mute && keepPlayingMutex != null && keepPlayingMutex != controller) { - keepPlayingMutex?.stop() - keepPlayingMutex?.release() - keepPlayingMutex = null - } - - controller.volume = if (mute) 0f else 1f - } - - KeepPlayingButton( - keepPlaying, - controllerVisible, - Modifier.align(Alignment.TopEnd).padding(end = Size55dp), - ) { newKeepPlaying: Boolean -> - // If something else is playing and the user marks this video to keep playing, stops the other - // one. - if (newKeepPlaying) { - if (keepPlayingMutex != null && keepPlayingMutex != controller) { + // if the user unmutes a video and it's not the current playing, switches to that one. + if (!mute && keepPlayingMutex != null && keepPlayingMutex != controller) { keepPlayingMutex?.stop() keepPlayingMutex?.release() - } - keepPlayingMutex = controller - } else { - if (keepPlayingMutex == controller) { keepPlayingMutex = null } + + controller.volume = if (mute) 0f else 1f } - keepPlaying.value = newKeepPlaying - } + KeepPlayingButton( + keepPlaying, + controllerVisible, + Modifier.align(Alignment.TopEnd).padding(end = Size55dp), + ) { newKeepPlaying: Boolean -> + // If something else is playing and the user marks this video to keep playing, stops the other + // one. + if (newKeepPlaying) { + if (keepPlayingMutex != null && keepPlayingMutex != controller) { + keepPlayingMutex?.stop() + keepPlayingMutex?.release() + } + keepPlayingMutex = controller + } else { + if (keepPlayingMutex == controller) { + keepPlayingMutex = null + } + } - AnimatedSaveButton(controllerVisible, Modifier.align(Alignment.TopEnd).padding(end = Size110dp)) { context -> - saveImage(videoUri, mimeType, context, accountViewModel) - } + keepPlaying.value = newKeepPlaying + } - AnimatedShareButton(controllerVisible, Modifier.align(Alignment.TopEnd).padding(end = Size165dp)) { popupExpanded, toggle -> - ShareImageAction(accountViewModel = accountViewModel, popupExpanded, videoUri, nostrUriCallback, toggle) - } */ + AnimatedSaveButton(controllerVisible, Modifier.align(Alignment.TopEnd).padding(end = Size110dp)) { context -> + saveImage(videoUri, mimeType, context, accountViewModel) + } + + AnimatedShareButton(controllerVisible, Modifier.align(Alignment.TopEnd).padding(end = Size165dp)) { popupExpanded, toggle -> + ShareImageAction(accountViewModel = accountViewModel, popupExpanded, videoUri, nostrUriCallback, toggle) + } + } else { + controller.volume = 0f + } } } 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 0bde747d0..9dfe05e10 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 @@ -53,6 +53,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.map import coil.compose.AsyncImage +import com.vitorpamplona.amethyst.commons.richtext.RichTextParser.Companion.isVideoUrl import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.components.SensitivityWarning @@ -336,8 +337,7 @@ fun InnerRenderGalleryThumb( contentAlignment = BottomStart, ) { card.image?.let { - if (it.endsWith("mp4") || it.endsWith("mov") || it.endsWith("mpeg")) { - // TODO how to long press? + if (isVideoUrl(it)) { VideoView( videoUri = it, mimeType = null, From 1d42440e269f3ad42f2f0086f63c3e5c3275d92c Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Mon, 1 Jul 2024 08:21:00 +0200 Subject: [PATCH 10/39] add gallery entry to dictionary --- .../vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt | 2 +- amethyst/src/main/res/values/strings.xml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) 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 0a0ec78a9..638f0d492 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 @@ -596,7 +596,7 @@ private fun CreateAndRenderTabs( listOf<@Composable (() -> Unit)?>( { Text(text = stringRes(R.string.notes)) }, { Text(text = stringRes(R.string.replies)) }, - { Text(text = "Gallery") }, + { Text(text = stringRes(R.string.gallery)) }, { FollowTabHeader(baseUser) }, { FollowersTabHeader(baseUser) }, { ZapTabHeader(baseUser) }, diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index e24567d9a..75a83cc4c 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -134,6 +134,7 @@ Conversations Notes Replies + Gallery "Follows" "Reports" More Options From b12d14d6f5e9a84c73939b8d2473b3baf54633ce Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Mon, 1 Jul 2024 09:19:16 +0200 Subject: [PATCH 11/39] add a shortpreview to not mess up grid layout on load --- .../amethyst/ui/note/BlankNote.kt | 8 +++- .../amethyst/ui/note/WatchNoteEvent.kt | 2 + .../ui/screen/RememberForeverStates.kt | 1 + .../ui/screen/loggedIn/ProfileGallery.kt | 46 +------------------ .../ui/screen/loggedIn/ProfileScreen.kt | 31 ++++++------- amethyst/src/main/res/values/strings.xml | 1 + 6 files changed, 27 insertions(+), 62 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt index 75af3272e..4f8e2c6d1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt @@ -59,6 +59,7 @@ fun BlankNotePreview() { fun BlankNote( modifier: Modifier = Modifier, idHex: String? = null, + shortPreview: Boolean = false, ) { Column(modifier = modifier) { Row { @@ -75,7 +76,12 @@ fun BlankNote( horizontalArrangement = Arrangement.Center, ) { Text( - text = stringRes(R.string.post_not_found) + if (idHex != null) ": $idHex" else "", + text = + if (shortPreview) { + stringRes(R.string.post_not_found_short) + } else { + stringRes(R.string.post_not_found) + if (idHex != null) ": $idHex" else "" + }, modifier = Modifier.padding(30.dp), color = Color.Gray, textAlign = TextAlign.Center, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/WatchNoteEvent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/WatchNoteEvent.kt index 33db44e8b..1983a4ba3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/WatchNoteEvent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/WatchNoteEvent.kt @@ -37,6 +37,7 @@ fun WatchNoteEvent( baseNote: Note, accountViewModel: AccountViewModel, modifier: Modifier = Modifier, + shortPreview: Boolean = false, onNoteEventFound: @Composable () -> Unit, ) { WatchNoteEvent( @@ -54,6 +55,7 @@ fun WatchNoteEvent( onLongClick = showPopup, ) }, + shortPreview = shortPreview, ) } }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/RememberForeverStates.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/RememberForeverStates.kt index b47a2fec0..c44bafda9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/RememberForeverStates.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/RememberForeverStates.kt @@ -45,6 +45,7 @@ object ScrollStateKeys { const val DISCOVER_SCREEN = "Discover" val HOME_FOLLOWS = Route.Home.base + "Follows" val HOME_REPLIES = Route.Home.base + "FollowsReplies" + val PROFILE_GALLERY = Route.Home.base + "ProfileGallery" val DRAFTS = Route.Home.base + "Drafts" 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 9dfe05e10..2520a8dd1 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 @@ -163,7 +163,7 @@ fun GalleryCardCompose( accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel) { + WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel, shortPreview = true) { CheckHiddenFeedWatchBlockAndReport( note = baseNote, modifier = modifier, @@ -228,7 +228,6 @@ private fun CheckNewAndRenderChannelCard( showPopup = showPopup, nav = nav, ) { - // baseNote.event?.let { Text(text = it.pubKey()) } InnerGalleryCardWithReactions( baseNote = baseNote, accountViewModel = accountViewModel, @@ -342,7 +341,6 @@ fun InnerRenderGalleryThumb( videoUri = it, mimeType = null, title = "", - dimensions = "1x1", authorName = note.author?.toBestDisplayName(), roundedCorner = false, gallery = true, @@ -361,48 +359,6 @@ fun InnerRenderGalleryThumb( } ?: run { DisplayGalleryAuthorBanner(note) } } - - /* Row( - Modifier - .fillMaxWidth() - .background(Color.Black.copy(0.6f)) - .padding(Size5dp), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - card.title?.let { - Text( - text = it, - fontWeight = FontWeight.Medium, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = Color.White, - modifier = Modifier.weight(1f), - ) - } - - card.price?.let { - val priceTag = - remember(card) { - val newAmount = it.amount.toBigDecimalOrNull()?.let { showAmountAxis(it) } ?: it.amount - - if (it.frequency != null && it.currency != null) { - "$newAmount ${it.currency}/${it.frequency}" - } else if (it.currency != null) { - "$newAmount ${it.currency}" - } else { - newAmount - } - } - - Text( - text = priceTag, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = Color.White, - ) - } - } - }*/ } @Composable 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 638f0d492..2aedc388e 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,7 +42,6 @@ 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 @@ -153,11 +152,13 @@ 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 +import com.vitorpamplona.amethyst.ui.screen.RefresheableBox 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.ScrollStateKeys import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange @@ -1567,24 +1568,22 @@ fun TabGallery( ) { LaunchedEffect(Unit) { feedViewModel.invalidateData() } - Column(Modifier.fillMaxHeight()) { - Column( - modifier = Modifier.padding(vertical = 0.dp), - ) { - var state = LazyGridState() + // Column(Modifier.fillMaxHeight()) { - SaveableGridFeedState(feedViewModel, scrollStateKey = "gallery") { listState -> - RenderGalleryFeed( - feedViewModel, - null, - 0, - state, - accountViewModel = accountViewModel, - nav = nav, - ) - } + RefresheableBox(feedViewModel, true) { + SaveableGridFeedState(feedViewModel, scrollStateKey = ScrollStateKeys.PROFILE_GALLERY) { listState -> + RenderGalleryFeed( + feedViewModel, + null, + 0, + listState, + accountViewModel = accountViewModel, + nav = nav, + ) } } + + // } } /*@Composable diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 75a83cc4c..ebf7df349 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -10,6 +10,7 @@ This post was hidden because it mentions your hidden users or words Post was muted or reported by Event is loading or can\'t be found in your relay list + 👀 Channel Image Referenced event not found Could not decrypt the message From 8421dbfa4cacef25db820d86c3c1d9e274b6307a Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Mon, 1 Jul 2024 22:06:40 +0200 Subject: [PATCH 12/39] use r tag instead of g (reserved for geotag) --- .../src/main/java/com/vitorpamplona/quartz/events/Event.kt | 2 +- .../com/vitorpamplona/quartz/events/GalleryListEvent.kt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) 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 a921dfb44..6d9cb1b81 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt @@ -119,7 +119,7 @@ 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 taggedGalleryEntries() = tags.filter { it.size > 2 && it[0] == "r" }.map { GalleryUrl(it[1], it[2]) } override fun taggedUrls() = tags.filter { it.size > 1 && it[0] == "r" }.map { it[1] } 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 1b65359c0..91aba31b5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/GalleryListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GalleryListEvent.kt @@ -47,7 +47,7 @@ class GalleryListEvent( signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (GalleryListEvent) -> Unit, - ) = addTag(earlierVersion, "g", eventId, url, signer, createdAt, onReady) + ) = addTag(earlierVersion, "r", eventId, url, signer, createdAt, onReady) fun addTag( earlierVersion: GalleryListEvent?, @@ -90,7 +90,7 @@ class GalleryListEvent( signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (GalleryListEvent) -> Unit, - ) = removeTag(earlierVersion, "g", eventId, url, signer, createdAt, onReady) + ) = removeTag(earlierVersion, "r", eventId, url, signer, createdAt, onReady) fun removeReplaceable( earlierVersion: GalleryListEvent, @@ -99,7 +99,7 @@ class GalleryListEvent( signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (GalleryListEvent) -> Unit, - ) = removeTag(earlierVersion, "g", aTag.toTag(), url, signer, createdAt, onReady) + ) = removeTag(earlierVersion, "r", aTag.toTag(), url, signer, createdAt, onReady) private fun removeTag( earlierVersion: GalleryListEvent, From 0ed87db3b3be60672f382441025654be7a7c47bc Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Mon, 1 Jul 2024 22:45:50 +0200 Subject: [PATCH 13/39] moved to gallery tag --- .../src/main/java/com/vitorpamplona/quartz/events/Event.kt | 2 +- .../com/vitorpamplona/quartz/events/GalleryListEvent.kt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) 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 6d9cb1b81..eda93d632 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt @@ -119,7 +119,7 @@ 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] == "r" }.map { GalleryUrl(it[1], it[2]) } + override fun taggedGalleryEntries() = tags.filter { it.size > 2 && it[0] == "gallery" }.map { GalleryUrl(it[1], it[2]) } override fun taggedUrls() = tags.filter { it.size > 1 && it[0] == "r" }.map { it[1] } 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 91aba31b5..048ee1050 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/GalleryListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GalleryListEvent.kt @@ -47,7 +47,7 @@ class GalleryListEvent( signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (GalleryListEvent) -> Unit, - ) = addTag(earlierVersion, "r", eventId, url, signer, createdAt, onReady) + ) = addTag(earlierVersion, "gallery", eventId, url, signer, createdAt, onReady) fun addTag( earlierVersion: GalleryListEvent?, @@ -90,7 +90,7 @@ class GalleryListEvent( signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (GalleryListEvent) -> Unit, - ) = removeTag(earlierVersion, "r", eventId, url, signer, createdAt, onReady) + ) = removeTag(earlierVersion, "gallery", eventId, url, signer, createdAt, onReady) fun removeReplaceable( earlierVersion: GalleryListEvent, @@ -99,7 +99,7 @@ class GalleryListEvent( signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (GalleryListEvent) -> Unit, - ) = removeTag(earlierVersion, "r", aTag.toTag(), url, signer, createdAt, onReady) + ) = removeTag(earlierVersion, "gallery", aTag.toTag(), url, signer, createdAt, onReady) private fun removeTag( earlierVersion: GalleryListEvent, From 6efb970794cd1e772165489a7c1c85beb7b3b190 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Tue, 2 Jul 2024 09:51:34 +0100 Subject: [PATCH 14/39] Use keys of map entries, as they are more reliable(from testing). Extract pollViewModel.pollOptions into a separate variable. --- .../vitorpamplona/amethyst/ui/actions/NewPostView.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index b8f4019b9..de0529f4a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -629,17 +629,21 @@ private fun BottomRowActions(postViewModel: NewPostViewModel) { @Composable private fun PollField(postViewModel: NewPostViewModel) { + val optionsList = postViewModel.pollOptions Column( modifier = Modifier.fillMaxWidth(), ) { - postViewModel.pollOptions.values.forEachIndexed { index, _ -> - NewPollOption(postViewModel, index) + optionsList.forEach { value -> + NewPollOption(postViewModel, value.key) } NewPollVoteValueRange(postViewModel) Button( - onClick = { postViewModel.pollOptions[postViewModel.pollOptions.size] = "" }, + onClick = { + // postViewModel.pollOptions[postViewModel.pollOptions.size] = "" + optionsList[optionsList.size] = "" + }, border = BorderStroke( 1.dp, From 7a76bf2ae52843c6cf74a5220f92ccdb590f5704 Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Tue, 2 Jul 2024 11:31:06 +0200 Subject: [PATCH 15/39] remove unneeded dtag from 10011 --- .../vitorpamplona/quartz/events/GalleryListEvent.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 048ee1050..4ec728427 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/GalleryListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GalleryListEvent.kt @@ -37,8 +37,8 @@ class GalleryListEvent( ) : GeneralListEvent(id, pubKey, createdAt, KIND, tags, content, sig) { companion object { const val KIND = 10011 - const val ALT = "Gallery List" - const val DEFAULT_D_TAG_GALLERY = "gallery" + const val ALT = "Profile Gallery" + const val GALLERYTAGNAME = "gallery" fun addEvent( earlierVersion: GalleryListEvent?, @@ -47,7 +47,7 @@ class GalleryListEvent( signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (GalleryListEvent) -> Unit, - ) = addTag(earlierVersion, "gallery", eventId, url, signer, createdAt, onReady) + ) = addTag(earlierVersion, GALLERYTAGNAME, eventId, url, signer, createdAt, onReady) fun addTag( earlierVersion: GalleryListEvent?, @@ -76,7 +76,7 @@ class GalleryListEvent( ) { create( content = earlierVersion?.content ?: "", - tags = listNewTags.plus(earlierVersion?.tags ?: arrayOf(arrayOf("d", DEFAULT_D_TAG_GALLERY))), + tags = listNewTags.plus(earlierVersion?.tags ?: arrayOf()), signer = signer, createdAt = createdAt, onReady = onReady, @@ -90,7 +90,7 @@ class GalleryListEvent( signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (GalleryListEvent) -> Unit, - ) = removeTag(earlierVersion, "gallery", eventId, url, signer, createdAt, onReady) + ) = removeTag(earlierVersion, GALLERYTAGNAME, eventId, url, signer, createdAt, onReady) fun removeReplaceable( earlierVersion: GalleryListEvent, @@ -99,7 +99,7 @@ class GalleryListEvent( signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (GalleryListEvent) -> Unit, - ) = removeTag(earlierVersion, "gallery", aTag.toTag(), url, signer, createdAt, onReady) + ) = removeTag(earlierVersion, GALLERYTAGNAME, aTag.toTag(), url, signer, createdAt, onReady) private fun removeTag( earlierVersion: GalleryListEvent, From d1ed73f78b54cea300b1e3295c3ea64a350c9fc2 Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Tue, 2 Jul 2024 11:36:26 +0200 Subject: [PATCH 16/39] Gallery: get rid of dtag, --- .../main/java/com/vitorpamplona/amethyst/model/LocalCache.kt | 4 +--- quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 1034d6065..2d7f5133a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -427,9 +427,7 @@ object LocalCache { fun consume(event: GalleryListEvent) { val user = getOrCreateUser(event.pubKey) if (user.latestGalleryList == null || event.createdAt > user.latestGalleryList!!.createdAt) { - if (event.dTag() == "gallery") { - user.updateGallery(event) - } + user.updateGallery(event) // Log.d("MT", "New User Metadata ${oldUser.pubkeyDisplayHex} ${oldUser.toBestDisplayName()}") } else { // Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()} 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 eda93d632..7410fc477 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt @@ -119,7 +119,7 @@ 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] == "gallery" }.map { GalleryUrl(it[1], it[2]) } + override fun taggedGalleryEntries() = tags.filter { it.size > 2 && it[0] == GalleryListEvent.GALLERYTAGNAME }.map { GalleryUrl(it[1], it[2]) } override fun taggedUrls() = tags.filter { it.size > 1 && it[0] == "r" }.map { it[1] } From cc8e432336109805f4e357015a275e37e3193005 Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Tue, 2 Jul 2024 11:42:10 +0200 Subject: [PATCH 17/39] make sure only owner of images get delete dialog in Gallery --- .../amethyst/ui/note/NoteQuickActionMenu.kt | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) 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 aa442f935..27e7b18d9 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 @@ -165,12 +165,14 @@ fun LongPressToQuickActionGallery( content { popupExpanded.value = true } if (popupExpanded.value) { - NoteQuickActionMenuGallery( - note = baseNote, - onDismiss = { popupExpanded.value = false }, - accountViewModel = accountViewModel, - nav = {}, - ) + if (baseNote.author == accountViewModel.account.userProfile()) { + NoteQuickActionMenuGallery( + note = baseNote, + onDismiss = { popupExpanded.value = false }, + accountViewModel = accountViewModel, + nav = {}, + ) + } } } From 9b26bdac5c5a7e4981b8c3baf3a794e81c69c591 Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Wed, 3 Jul 2024 23:11:59 +0200 Subject: [PATCH 18/39] move GalleryUrl to GalleryListEvent, merge recent changes --- .../ui/dal/UserProfileGalleryFeedFilter.kt | 12 ++++++------ .../amethyst/ui/screen/FeedViewModel.kt | 15 +++++++++++++++ .../ui/screen/loggedIn/ProfileScreen.kt | 3 +-- .../quartz/events/EmojiPackEvent.kt | 18 ------------------ .../com/vitorpamplona/quartz/events/Event.kt | 2 +- .../quartz/events/EventInterface.kt | 2 +- .../quartz/events/GalleryListEvent.kt | 19 +++++++++++++++++++ 7 files changed, 43 insertions(+), 28 deletions(-) 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 8d726b89c..f27abe3a6 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 @@ -25,10 +25,11 @@ 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 + "-Gallery-" + user.pubkeyHex - } +class UserProfileGalleryFeedFilter( + val user: User, + val account: Account, +) : FeedFilter() { + override fun feedKey(): String = account.userProfile().pubkeyHex + "-Gallery-" + user.pubkeyHex override fun feed(): List { val notes = @@ -45,8 +46,7 @@ class UserProfileGalleryFeedFilter(val user: User, val account: Account) : FeedF // )!! it.url, ) - } - ?.toSet() + }?.toSet() ?: emptySet() var finalnotes = setOf() 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 94e80f342..3343d89d7 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 @@ -248,6 +249,20 @@ class NostrUserProfileReportFeedViewModel( } } +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 = + NostrUserProfileGalleryFeedViewModel(user, account) + as NostrUserProfileGalleryFeedViewModel + } +} + class NostrUserProfileBookmarksFeedViewModel( val user: User, val account: Account, 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 be815f1f9..056371ea2 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 @@ -451,8 +451,7 @@ private fun RenderSurface( } } }, - ) - .fillMaxHeight() + ).fillMaxHeight() }, ) { RenderScreen( 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 9d7291dd1..0599fc9b8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EmojiPackEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EmojiPackEvent.kt @@ -73,21 +73,3 @@ data class EmojiUrl( } } } - -@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 7410fc477..e534dde0b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt @@ -119,7 +119,7 @@ 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] == GalleryListEvent.GALLERYTAGNAME }.map { GalleryUrl(it[1], it[2]) } + override fun taggedGalleryEntries() = tags.filter { it.size > 2 && it[0] == GalleryListEvent.GALLERYTAGNAME }.map { GalleryListEvent.GalleryUrl(it[1], it[2]) } override fun taggedUrls() = tags.filter { it.size > 1 && it[0] == "r" }.map { 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 d444f4195..c7c162493 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt @@ -145,7 +145,7 @@ interface EventInterface { fun firstTaggedK(): Int? - fun taggedGalleryEntries(): List + fun taggedGalleryEntries(): List fun taggedEmojis(): List 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 4ec728427..73620d6e5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/GalleryListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GalleryListEvent.kt @@ -139,4 +139,23 @@ class GalleryListEvent( signer.sign(createdAt, KIND, newTags, content, onReady) } } + + @Immutable + data class GalleryUrl( + val id: String, + val url: String, + ) { + fun encode(): String = ":$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 + } + } + } + } } From a55da5f62e4071dbde5003df5527d67dd481d211 Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Wed, 3 Jul 2024 23:28:15 +0200 Subject: [PATCH 19/39] linting --- .../src/main/java/com/vitorpamplona/amethyst/model/User.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt index 2e09dbe9e..43d0fe6fa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -528,7 +528,8 @@ class UserLiveSet( relays.hasObservers() || relayInfo.hasObservers() || zaps.hasObservers() || - bookmarks.hasObservers() || gallery.hasObservers() || + bookmarks.hasObservers() || + gallery.hasObservers() || statuses.hasObservers() || profilePictureChanges.hasObservers() || nip05Changes.hasObservers() || From c81324843ca46191788f07bf785bdb727faab09e Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Thu, 4 Jul 2024 20:41:14 +0200 Subject: [PATCH 20/39] move to G Tag for Gallery entries --- .../java/com/vitorpamplona/quartz/events/GalleryListEvent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 73620d6e5..64c8fc9b1 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/GalleryListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GalleryListEvent.kt @@ -38,7 +38,7 @@ class GalleryListEvent( companion object { const val KIND = 10011 const val ALT = "Profile Gallery" - const val GALLERYTAGNAME = "gallery" + const val GALLERYTAGNAME = "G" fun addEvent( earlierVersion: GalleryListEvent?, From ad6dcfc0306f944997d5f0850ff2dc264405106e Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Fri, 5 Jul 2024 08:29:49 +0200 Subject: [PATCH 21/39] use url tag, add relay hint --- .../vitorpamplona/amethyst/model/Account.kt | 2 ++ .../ui/components/ZoomableContentView.kt | 2 +- .../ui/screen/loggedIn/AccountViewModel.kt | 3 +- .../com/vitorpamplona/quartz/events/Event.kt | 5 ++- .../quartz/events/GalleryListEvent.kt | 32 ++++++++++++------- 5 files changed, 29 insertions(+), 15 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 8c832c4f2..20b750ebe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -2201,6 +2201,7 @@ class Account( fun addToGallery( idHex: String, url: String, + relay: String?, ) { if (!isWriteable()) return @@ -2208,6 +2209,7 @@ class Account( userProfile().latestGalleryList, idHex, url, + relay, signer, ) { Client.send(it) 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 5fed68693..3544d639e 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 @@ -663,7 +663,7 @@ fun ShareImageAction( if (videoUri != null) { var n19 = Nip19Bech32.uriToRoute(postNostrUri)?.entity as? Nip19Bech32.NEvent if (n19 != null) { - accountViewModel.addMediaToGallery(n19.hex, videoUri) + accountViewModel.addMediaToGallery(n19.hex, videoUri, n19.relay[0]) // TODO Whole list or first? accountViewModel.toast(R.string.image_saved_to_the_gallery, R.string.image_saved_to_the_gallery) } } 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 5c6504503..1b26192a7 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 @@ -672,8 +672,9 @@ class AccountViewModel( fun addMediaToGallery( hex: String, url: String, + relay: String?, ) { - viewModelScope.launch(Dispatchers.IO) { account.addToGallery(hex, url) } + viewModelScope.launch(Dispatchers.IO) { account.addToGallery(hex, url, relay) } } fun removefromMediaGallery( 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 e534dde0b..6d4cbd8f4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt @@ -119,7 +119,10 @@ 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] == GalleryListEvent.GALLERYTAGNAME }.map { GalleryListEvent.GalleryUrl(it[1], it[2]) } + override fun taggedGalleryEntries() = + tags.filter { it.size > 2 && it[0] == GalleryListEvent.GALLERYTAGNAME }.map { + GalleryListEvent.GalleryUrl(it[1], it[2], it.getOrNull(3)) + } override fun taggedUrls() = tags.filter { it.size > 1 && it[0] == "r" }.map { it[1] } 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 64c8fc9b1..808155184 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/GalleryListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GalleryListEvent.kt @@ -38,29 +38,36 @@ class GalleryListEvent( companion object { const val KIND = 10011 const val ALT = "Profile Gallery" - const val GALLERYTAGNAME = "G" + const val GALLERYTAGNAME = "url" fun addEvent( earlierVersion: GalleryListEvent?, eventId: HexKey, url: String, + relay: String?, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (GalleryListEvent) -> Unit, - ) = addTag(earlierVersion, GALLERYTAGNAME, eventId, url, signer, createdAt, onReady) + ) = addTag(earlierVersion, GALLERYTAGNAME, eventId, url, relay, signer, createdAt, onReady) fun addTag( earlierVersion: GalleryListEvent?, tagName: String, - tagValue: HexKey, + eventid: HexKey, url: String, + relay: String?, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (GalleryListEvent) -> Unit, ) { + var tags = arrayOf(tagName, url, eventid) + if (relay != null) { + tags + relay + } + add( earlierVersion, - arrayOf(arrayOf(tagName, tagValue, url)), + arrayOf(tags), signer, createdAt, onReady, @@ -104,7 +111,7 @@ class GalleryListEvent( private fun removeTag( earlierVersion: GalleryListEvent, tagName: String, - tagValue: HexKey, + eventid: HexKey, url: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), @@ -114,7 +121,7 @@ class GalleryListEvent( content = earlierVersion.content, tags = earlierVersion.tags - .filter { it.size <= 1 || !(it[0] == tagName && it[1] == tagValue && it[2] == url) } + .filter { it.size <= 1 || !(it[0] == tagName && it[1] == url && it[2] == eventid) } .toTypedArray(), signer = signer, createdAt = createdAt, @@ -142,16 +149,17 @@ class GalleryListEvent( @Immutable data class GalleryUrl( - val id: String, val url: String, + val id: String, + val relay: String?, ) { - fun encode(): String = ":$id:$url" + fun encode(): String = ":$url:$id:$relay" companion object { - fun decode(encodedGallerySetup: String): EmojiUrl? { - val emojiParts = encodedGallerySetup.split(":", limit = 3) - return if (emojiParts.size > 2) { - EmojiUrl(emojiParts[1], emojiParts[2]) + fun decode(encodedGallerySetup: String): GalleryUrl? { + val galleryParts = encodedGallerySetup.split(":", limit = 3) + return if (galleryParts.size > 3) { + GalleryUrl(galleryParts[1], galleryParts[2], galleryParts[3]) } else { null } From 06a1862d17b405e8fea7d9bef8d300b23dac8793 Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Fri, 5 Jul 2024 13:35:36 +0200 Subject: [PATCH 22/39] add relay info --- .../amethyst/ui/dal/UserProfileGalleryFeedFilter.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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 f27abe3a6..b1ec6e5a1 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 @@ -24,6 +24,8 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.ammolite.relays.FeedType +import com.vitorpamplona.ammolite.relays.Relay class UserProfileGalleryFeedFilter( val user: User, @@ -36,7 +38,7 @@ class UserProfileGalleryFeedFilter( user.latestGalleryList ?.taggedGalleryEntries() ?.map { - Pair( + Triple( // ( // if (ATag.isATag(it.id)) { // ATag.parse(it.id, null)?.let { it1 -> LocalCache.getOrCreateAddressableNote(it1) } @@ -45,6 +47,7 @@ class UserProfileGalleryFeedFilter( // } // )!! it.url, + it.relay, ) }?.toSet() ?: emptySet() @@ -52,6 +55,11 @@ class UserProfileGalleryFeedFilter( var finalnotes = setOf() for (pair in notes) { pair.first.headerImage = pair.second + if (pair.third != null) { + val relay = Relay(pair.third!!, true, false, setOf(FeedType.GLOBAL)) + pair.first.createdAt()?.let { user.addRelayBeingUsed(relay, it) } + pair.first.addRelay(relay) + } finalnotes = finalnotes + pair.first } println(finalnotes) From 511a7030b82c472fbc92ceed7120aedea2b800ae Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Sat, 6 Jul 2024 00:11:35 +0200 Subject: [PATCH 23/39] use alternative gallery using 1163 events instead of 10011 lists. (This might still need some tuning but works in general) --- .../vitorpamplona/amethyst/model/Account.kt | 59 +++----- .../amethyst/model/LocalCache.kt | 35 +++-- .../com/vitorpamplona/amethyst/model/Note.kt | 2 +- .../com/vitorpamplona/amethyst/model/User.kt | 9 -- .../service/NostrUserProfileDataSource.kt | 21 ++- .../ui/dal/UserProfileGalleryFeedFilter.kt | 80 ++++++----- .../amethyst/ui/note/NoteQuickActionMenu.kt | 6 +- .../ui/screen/loggedIn/AccountViewModel.kt | 7 +- .../ui/screen/loggedIn/ProfileGallery.kt | 89 ++++++------ .../com/vitorpamplona/quartz/events/Event.kt | 5 - .../quartz/events/EventFactory.kt | 2 +- .../quartz/events/EventInterface.kt | 2 - .../quartz/events/ProfileGalleryEntryEvent.kt | 130 ++++++++++++++++++ 13 files changed, 291 insertions(+), 156 deletions(-) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/ProfileGalleryEntryEvent.kt 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 20b750ebe..1995916c5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -71,7 +71,6 @@ import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.FileServersEvent import com.vitorpamplona.quartz.events.FileStorageEvent import com.vitorpamplona.quartz.events.FileStorageHeaderEvent -import com.vitorpamplona.quartz.events.GalleryListEvent import com.vitorpamplona.quartz.events.GeneralListEvent import com.vitorpamplona.quartz.events.GenericRepostEvent import com.vitorpamplona.quartz.events.GiftWrapEvent @@ -92,6 +91,7 @@ import com.vitorpamplona.quartz.events.PollNoteEvent import com.vitorpamplona.quartz.events.Price import com.vitorpamplona.quartz.events.PrivateDmEvent import com.vitorpamplona.quartz.events.PrivateOutboxRelayListEvent +import com.vitorpamplona.quartz.events.ProfileGalleryEntryEvent import com.vitorpamplona.quartz.events.ReactionEvent import com.vitorpamplona.quartz.events.RelayAuthEvent import com.vitorpamplona.quartz.events.ReportEvent @@ -2204,48 +2204,27 @@ class Account( relay: String?, ) { if (!isWriteable()) return - - GalleryListEvent.addEvent( - userProfile().latestGalleryList, - idHex, - url, - relay, - signer, - ) { - Client.send(it) - LocalCache.consume(it) + ProfileGalleryEntryEvent.create( + url = url, + eventid = idHex, + /*magnetUri = magnetUri, + mimeType = headerInfo.mimeType, + hash = headerInfo.hash, + size = headerInfo.size.toString(), + dimensions = headerInfo.dim, + blurhash = headerInfo.blurHash, + alt = alt, + originalHash = originalHash, + sensitiveContent = sensitiveContent, */ + signer = signer, + ) { event -> + Client.send(event) + LocalCache.consume(event, null) } } - fun removeFromGallery( - note: Note, - url: String, - ) { - if (!isWriteable()) return - - val galleryentries = userProfile().latestGalleryList ?: return - - if (note is AddressableNote) { - GalleryListEvent.removeReplaceable( - galleryentries, - note.address, - url, - signer, - ) { - Client.send(it) - LocalCache.consume(it) - } - } else { - GalleryListEvent.removeEvent( - galleryentries, - note.idHex, - url, - signer, - ) { - Client.send(it) - LocalCache.consume(it) - } - } + fun removeFromGallery(note: Note) { + delete(note) } fun addBookmark( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 2d7f5133a..c74002477 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -75,7 +75,6 @@ import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.FileServersEvent import com.vitorpamplona.quartz.events.FileStorageEvent import com.vitorpamplona.quartz.events.FileStorageHeaderEvent -import com.vitorpamplona.quartz.events.GalleryListEvent import com.vitorpamplona.quartz.events.GenericRepostEvent import com.vitorpamplona.quartz.events.GiftWrapEvent import com.vitorpamplona.quartz.events.GitIssueEvent @@ -104,6 +103,7 @@ import com.vitorpamplona.quartz.events.PinListEvent import com.vitorpamplona.quartz.events.PollNoteEvent import com.vitorpamplona.quartz.events.PrivateDmEvent import com.vitorpamplona.quartz.events.PrivateOutboxRelayListEvent +import com.vitorpamplona.quartz.events.ProfileGalleryEntryEvent import com.vitorpamplona.quartz.events.ReactionEvent import com.vitorpamplona.quartz.events.RecommendRelayEvent import com.vitorpamplona.quartz.events.RelaySetEvent @@ -424,17 +424,6 @@ object LocalCache { } } - fun consume(event: GalleryListEvent) { - val user = getOrCreateUser(event.pubKey) - if (user.latestGalleryList == null || event.createdAt > user.latestGalleryList!!.createdAt) { - user.updateGallery(event) - // Log.d("MT", "New User Metadata ${oldUser.pubkeyDisplayHex} ${oldUser.toBestDisplayName()}") - } else { - // Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()} - // ${formattedDateTime(event.createdAt)} > ${formattedDateTime(oldUser.updatedAt)}") - } - } - fun formattedDateTime(timestamp: Long): String = Instant .ofEpochSecond(timestamp) @@ -1680,6 +1669,26 @@ object LocalCache { refreshObservers(note) } + fun consume( + event: ProfileGalleryEntryEvent, + relay: Relay?, + ) { + val note = getOrCreateNote(event.id) + val author = getOrCreateUser(event.pubKey) + + if (relay != null) { + author.addRelayBeingUsed(relay, event.createdAt) + note.addRelay(relay) + } + + // Already processed this event. + if (note.event != null) return + + note.loadEvent(event, author, emptyList()) + + refreshObservers(note) + } + fun consume( event: FileStorageHeaderEvent, relay: Relay?, @@ -2535,13 +2544,13 @@ object LocalCache { is DraftEvent -> consume(event, relay) is EmojiPackEvent -> consume(event, relay) is EmojiPackSelectionEvent -> consume(event, relay) - is GalleryListEvent -> consume(event) is GenericRepostEvent -> { event.containedPost()?.let { verifyAndConsume(it, relay) } consume(event) } is FhirResourceEvent -> consume(event, relay) is FileHeaderEvent -> consume(event, relay) + is ProfileGalleryEntryEvent -> consume(event, relay) is FileServersEvent -> consume(event, relay) is FileStorageEvent -> consume(event, relay) is FileStorageHeaderEvent -> consume(event, relay) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index acc2ae0ad..a5118c559 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -139,7 +139,7 @@ open class Note( var relays = listOf() private set - var headerImage: String? = null + var associatedNote: Note? = null var lastReactionsDownloadTime: Map = emptyMap() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt index 43d0fe6fa..50964cd07 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -40,7 +40,6 @@ import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent import com.vitorpamplona.quartz.events.BookmarkListEvent import com.vitorpamplona.quartz.events.ChatroomKey import com.vitorpamplona.quartz.events.ContactListEvent -import com.vitorpamplona.quartz.events.GalleryListEvent import com.vitorpamplona.quartz.events.LnZapEvent import com.vitorpamplona.quartz.events.MetadataEvent import com.vitorpamplona.quartz.events.ReportEvent @@ -61,7 +60,6 @@ class User( var latestMetadataRelay: String? = null var latestContactList: ContactListEvent? = null var latestBookmarkList: BookmarkListEvent? = null - var latestGalleryList: GalleryListEvent? = null var reports = mapOf>() private set @@ -125,13 +123,6 @@ class User( liveSet?.innerBookmarks?.invalidateData() } - fun updateGallery(event: GalleryListEvent) { - if (event.id == latestGalleryList?.id) return - print("GALLERY " + event.id()) - latestGalleryList = event - liveSet?.innerGallery?.invalidateData() - } - fun clearEOSE() { latestEOSEs = emptyMap() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt index fc0144b2f..3f7139549 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt @@ -31,7 +31,6 @@ import com.vitorpamplona.quartz.events.BadgeAwardEvent import com.vitorpamplona.quartz.events.BadgeProfilesEvent import com.vitorpamplona.quartz.events.BookmarkListEvent import com.vitorpamplona.quartz.events.ContactListEvent -import com.vitorpamplona.quartz.events.GalleryListEvent import com.vitorpamplona.quartz.events.GenericRepostEvent import com.vitorpamplona.quartz.events.HighlightEvent import com.vitorpamplona.quartz.events.LnZapEvent @@ -40,6 +39,7 @@ import com.vitorpamplona.quartz.events.MetadataEvent import com.vitorpamplona.quartz.events.PeopleListEvent import com.vitorpamplona.quartz.events.PinListEvent import com.vitorpamplona.quartz.events.PollNoteEvent +import com.vitorpamplona.quartz.events.ProfileGalleryEntryEvent import com.vitorpamplona.quartz.events.RepostEvent import com.vitorpamplona.quartz.events.TextNoteEvent import com.vitorpamplona.quartz.events.WikiNoteEvent @@ -84,7 +84,7 @@ object NostrUserProfileDataSource : AmethystNostrDataSource("UserProfileFeed") { WikiNoteEvent.KIND, ), authors = listOf(it.pubkeyHex), - limit = 200, + limit = 500, ), ) } @@ -147,13 +147,27 @@ object NostrUserProfileDataSource : AmethystNostrDataSource("UserProfileFeed") { filter = Filter( kinds = - listOf(BookmarkListEvent.KIND, PeopleListEvent.KIND, AppRecommendationEvent.KIND, GalleryListEvent.KIND), + listOf(BookmarkListEvent.KIND, PeopleListEvent.KIND, AppRecommendationEvent.KIND), authors = listOf(it.pubkeyHex), limit = 100, ), ) } + fun createProfileGalleryFilter() = + user?.let { + TypedFilter( + types = COMMON_FEED_TYPES, + filter = + Filter( + kinds = + listOf(ProfileGalleryEntryEvent.KIND), + authors = listOf(it.pubkeyHex), + limit = 1000, + ), + ) + } + fun createReceivedAwardsFilter() = user?.let { TypedFilter( @@ -174,6 +188,7 @@ object NostrUserProfileDataSource : AmethystNostrDataSource("UserProfileFeed") { listOfNotNull( createUserInfoFilter(), createUserPostsFilter(), + createProfileGalleryFilter(), createFollowFilter(), createFollowersFilter(), createUserReceivedZapsFilter(), 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 b1ec6e5a1..a4fc5eba2 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 @@ -24,47 +24,65 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.Relay +import com.vitorpamplona.quartz.events.MuteListEvent +import com.vitorpamplona.quartz.events.PeopleListEvent +import com.vitorpamplona.quartz.events.ProfileGalleryEntryEvent class UserProfileGalleryFeedFilter( val user: User, val account: Account, -) : FeedFilter() { - override fun feedKey(): String = account.userProfile().pubkeyHex + "-Gallery-" + user.pubkeyHex +) : AdditiveFeedFilter() { + override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + account.defaultStoriesFollowList.value + + override fun showHiddenKey(): Boolean = + account.defaultStoriesFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) || + account.defaultStoriesFollowList.value == MuteListEvent.blockListFor(account.userProfile().pubkeyHex) override fun feed(): List { + val params = buildFilterParams(account) + val notes = - user.latestGalleryList - ?.taggedGalleryEntries() - ?.map { - Triple( - // ( - // if (ATag.isATag(it.id)) { - // ATag.parse(it.id, null)?.let { it1 -> LocalCache.getOrCreateAddressableNote(it1) } - // } else { - LocalCache.getOrCreateNote(it.id), - // } - // )!! - it.url, - it.relay, - ) - }?.toSet() - ?: emptySet() + LocalCache.notes.filterIntoSet { _, it -> + acceptableEvent(it, params, user) + } var finalnotes = setOf() - for (pair in notes) { - pair.first.headerImage = pair.second - if (pair.third != null) { - val relay = Relay(pair.third!!, true, false, setOf(FeedType.GLOBAL)) - pair.first.createdAt()?.let { user.addRelayBeingUsed(relay, it) } - pair.first.addRelay(relay) - } - finalnotes = finalnotes + pair.first + for (item in notes) { + item.associatedNote = (item.event as ProfileGalleryEntryEvent).event()?.let { LocalCache.getOrCreateNote(it) } + finalnotes = finalnotes + item } - println(finalnotes) - return (finalnotes) - .filter { account.isAcceptable(it) } + return sort(finalnotes) } + + override fun applyFilter(collection: Set): Set = innerApplyFilter(collection) + + private fun innerApplyFilter(collection: Collection): Set { + val params = buildFilterParams(account) + + return collection.filterTo(HashSet()) { acceptableEvent(it, params, user) } + } + + fun acceptableEvent( + it: Note, + params: FilterByListParams, + user: User, + ): Boolean { + val noteEvent = it.event + return ( + (it.event?.pubKey() == user.pubkeyHex && noteEvent is ProfileGalleryEntryEvent) && noteEvent.hasUrl() && noteEvent.hasEvent() // && noteEvent.isOneOf(SUPPORTED_VIDEO_FEED_MIME_TYPES_SET)) + ) && + params.match(noteEvent) && + account.isAcceptable(it) + } + + fun buildFilterParams(account: Account): FilterByListParams = + FilterByListParams.create( + userHex = account.userProfile().pubkeyHex, + selectedListName = account.defaultStoriesFollowList.value, + followLists = account.liveStoriesFollowLists.value, + hiddenUsers = account.flowHiddenUsers.value, + ) + + override fun sort(collection: Set): List = collection.sortedWith(DefaultFeedOrder) } 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 27e7b18d9..ccff7de2a 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 @@ -671,11 +671,7 @@ fun DeleteFromGalleryDialog( buttonIcon = Icons.Default.Delete, buttonText = stringRes(R.string.quick_action_delete_dialog_btn), onClickDoOnce = { - note.headerImage.let { - if (it != null) { - accountViewModel.removefromMediaGallery(note, it) - } - } + accountViewModel.removefromMediaGallery(note) onDismiss() }, onDismiss = onDismiss, 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 1b26192a7..52b00c91c 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 @@ -677,11 +677,8 @@ class AccountViewModel( viewModelScope.launch(Dispatchers.IO) { account.addToGallery(hex, url, relay) } } - fun removefromMediaGallery( - note: Note, - url: String, - ) { - viewModelScope.launch(Dispatchers.IO) { account.removeFromGallery(note, url) } + fun removefromMediaGallery(note: Note) { + viewModelScope.launch(Dispatchers.IO) { account.removeFromGallery(note) } } fun addPrivateBookmark(note: Note) { 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 2520a8dd1..730db72dc 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 @@ -33,7 +33,6 @@ import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyGridState import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.itemsIndexed -import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Surface import androidx.compose.runtime.Composable @@ -74,7 +73,7 @@ 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.quartz.events.TextNoteEvent +import com.vitorpamplona.quartz.events.ProfileGalleryEntryEvent @Composable fun RenderGalleryFeed( @@ -134,27 +133,45 @@ private fun GalleryFeedLoaded( itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item -> val defaultModifier = remember { Modifier.fillMaxWidth().animateItemPlacement() } - Row(defaultModifier) { - GalleryCardCompose( - baseNote = item, - routeForLastRead = routeForLastRead, - modifier = Modifier, - forceEventKind = forceEventKind, - accountViewModel = accountViewModel, - nav = nav, - ) - } + if (item.associatedNote != null) { + if (item.associatedNote!!.event != null) { + if ((item.event as ProfileGalleryEntryEvent).hasUrl() && + (item.event as ProfileGalleryEntryEvent).hasEvent() + ) { + val image = (item.event as ProfileGalleryEntryEvent).url() - HorizontalDivider( - thickness = DividerThickness, - ) + Row(defaultModifier) { + if (image != null) { + GalleryCardCompose( + galleryNote = item, + image = image, + baseNote = item.associatedNote!!, + routeForLastRead = routeForLastRead, + modifier = Modifier, + forceEventKind = forceEventKind, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } + + HorizontalDivider( + thickness = DividerThickness, + ) + } else { + accountViewModel.delete(item) + } + } + } } } } @Composable fun GalleryCardCompose( + galleryNote: Note, baseNote: Note, + image: String, routeForLastRead: String? = null, modifier: Modifier = Modifier, parentBackgroundColor: MutableState? = null, @@ -172,8 +189,11 @@ fun GalleryCardCompose( accountViewModel = accountViewModel, nav = nav, ) { canPreview -> + GalleryCard( + galleryNote = galleryNote, baseNote = baseNote, + image = image, modifier = modifier, parentBackgroundColor = parentBackgroundColor, accountViewModel = accountViewModel, @@ -185,16 +205,19 @@ fun GalleryCardCompose( @Composable fun GalleryCard( + galleryNote: Note, baseNote: Note, + image: String, modifier: Modifier = Modifier, parentBackgroundColor: MutableState? = null, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { // baseNote.event?.let { Text(text = it.pubKey()) } - LongPressToQuickActionGallery(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup -> + LongPressToQuickActionGallery(baseNote = galleryNote, accountViewModel = accountViewModel) { showPopup -> CheckNewAndRenderChannelCard( baseNote, + image, modifier, parentBackgroundColor, accountViewModel, @@ -207,6 +230,7 @@ fun GalleryCard( @Composable private fun CheckNewAndRenderChannelCard( baseNote: Note, + image: String, modifier: Modifier = Modifier, parentBackgroundColor: MutableState? = null, accountViewModel: AccountViewModel, @@ -228,26 +252,14 @@ private fun CheckNewAndRenderChannelCard( showPopup = showPopup, nav = nav, ) { - InnerGalleryCardWithReactions( - baseNote = baseNote, - accountViewModel = accountViewModel, - nav = nav, - ) + InnerGalleryCardBox(baseNote, image, accountViewModel, nav) } } -@Composable -fun InnerGalleryCardWithReactions( - baseNote: Note, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - InnerGalleryCardBox(baseNote, accountViewModel, nav) -} - @Composable fun InnerGalleryCardBox( baseNote: Note, + image: String, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { @@ -256,7 +268,7 @@ fun InnerGalleryCardBox( note = baseNote, accountViewModel = accountViewModel, ) { - RenderGalleryThumb(baseNote, accountViewModel, nav) + RenderGalleryThumb(baseNote, image, accountViewModel, nav) } } } @@ -272,21 +284,19 @@ data class GalleryThumb( @Composable fun RenderGalleryThumb( baseNote: Note, + image: String, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - val noteEvent = baseNote.event as? TextNoteEvent ?: return - val card by baseNote .live() .metadata .map { - val noteEvent = baseNote.event as TextNoteEvent GalleryThumb( id = "", - image = baseNote.headerImage, - title = noteEvent.content(), + image = image, + title = "", // noteEvent?.title(), // price = noteEvent?.price(), ) @@ -294,11 +304,8 @@ fun RenderGalleryThumb( .observeAsState( GalleryThumb( id = "", - image = baseNote.headerImage, - title = noteEvent.content(), - // image = noteEvent.image(), - // title = noteEvent.title(), - // price = noteEvent.price(), + image = image, + title = "", ), ) 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 6d4cbd8f4..19521379e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt @@ -119,11 +119,6 @@ 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] == GalleryListEvent.GALLERYTAGNAME }.map { - GalleryListEvent.GalleryUrl(it[1], it[2], it.getOrNull(3)) - } - 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/EventFactory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt index 759a0bd17..288ae12e1 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt @@ -85,6 +85,7 @@ class EventFactory { EmojiPackSelectionEvent.KIND -> EmojiPackSelectionEvent(id, pubKey, createdAt, tags, content, sig) FileHeaderEvent.KIND -> FileHeaderEvent(id, pubKey, createdAt, tags, content, sig) + ProfileGalleryEntryEvent.KIND -> ProfileGalleryEntryEvent(id, pubKey, createdAt, tags, content, sig) FileServersEvent.KIND -> FileServersEvent(id, pubKey, createdAt, tags, content, sig) FileStorageEvent.KIND -> FileStorageEvent(id, pubKey, createdAt, tags, content, sig) FileStorageHeaderEvent.KIND -> @@ -97,7 +98,6 @@ class EventFactory { GitPatchEvent.KIND -> GitPatchEvent(id, pubKey, createdAt, tags, content, sig) GitRepositoryEvent.KIND -> GitRepositoryEvent(id, pubKey, createdAt, tags, content, sig) GoalEvent.KIND -> GoalEvent(id, pubKey, createdAt, tags, content, sig) - GalleryListEvent.KIND -> GalleryListEvent(id, pubKey, createdAt, tags, content, sig) HighlightEvent.KIND -> HighlightEvent(id, pubKey, createdAt, tags, content, sig) HTTPAuthorizationEvent.KIND -> HTTPAuthorizationEvent(id, pubKey, createdAt, tags, content, sig) 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 c7c162493..db11a3ea4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt @@ -145,8 +145,6 @@ interface EventInterface { fun firstTaggedK(): Int? - fun taggedGalleryEntries(): List - fun taggedEmojis(): List fun matchTag1With(text: String): Boolean diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ProfileGalleryEntryEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ProfileGalleryEntryEvent.kt new file mode 100644 index 000000000..2a04bfe38 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ProfileGalleryEntryEvent.kt @@ -0,0 +1,130 @@ +/** + * 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.quartz.events + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class ProfileGalleryEntryEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { + fun url() = tags.firstOrNull { it.size > 1 && it[0] == URL }?.get(1) + + fun urls() = tags.filter { it.size > 1 && it[0] == URL }.map { it[1] } + + fun encryptionKey() = tags.firstOrNull { it.size > 2 && it[0] == ENCRYPTION_KEY }?.let { AESGCM(it[1], it[2]) } + + fun mimeType() = tags.firstOrNull { it.size > 1 && it[0] == MIME_TYPE }?.get(1) + + fun hash() = tags.firstOrNull { it.size > 1 && it[0] == HASH }?.get(1) + + fun size() = tags.firstOrNull { it.size > 1 && it[0] == FILE_SIZE }?.get(1) + + fun alt() = tags.firstOrNull { it.size > 1 && it[0] == ALT }?.get(1) + + fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1) + + fun magnetURI() = tags.firstOrNull { it.size > 1 && it[0] == MAGNET_URI }?.get(1) + + fun torrentInfoHash() = tags.firstOrNull { it.size > 1 && it[0] == TORRENT_INFOHASH }?.get(1) + + fun blurhash() = tags.firstOrNull { it.size > 1 && it[0] == BLUR_HASH }?.get(1) + + fun hasUrl() = tags.any { it.size > 1 && it[0] == URL } + + fun event() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1) + + fun hasEvent() = tags.any { it.size > 1 && it[0] == "e" } + + fun isOneOf(mimeTypes: Set) = tags.any { it.size > 1 && it[0] == MIME_TYPE && mimeTypes.contains(it[1]) } + + companion object { + const val KIND = 1163 + const val ALT_DESCRIPTION = "Profile Gallery Entry" + + const val URL = "url" + const val ENCRYPTION_KEY = "aes-256-gcm" + const val MIME_TYPE = "m" + const val FILE_SIZE = "size" + const val DIMENSION = "dim" + const val HASH = "x" + const val MAGNET_URI = "magnet" + const val TORRENT_INFOHASH = "i" + const val BLUR_HASH = "blurhash" + const val ORIGINAL_HASH = "ox" + const val ALT = "alt" + + fun create( + url: String, + eventid: String? = null, + magnetUri: String? = null, + mimeType: String? = null, + alt: String? = null, + hash: String? = null, + size: String? = null, + dimensions: String? = null, + blurhash: String? = null, + originalHash: String? = null, + magnetURI: String? = null, + torrentInfoHash: String? = null, + encryptionKey: AESGCM? = null, + sensitiveContent: Boolean? = null, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (ProfileGalleryEntryEvent) -> Unit, + ) { + val tags = + listOfNotNull( + arrayOf(URL, url), + eventid?.let { arrayOf("e", it) }, + magnetUri?.let { arrayOf(MAGNET_URI, it) }, + mimeType?.let { arrayOf(MIME_TYPE, it) }, + alt?.ifBlank { null }?.let { arrayOf(ALT, it) } ?: arrayOf("alt", ALT_DESCRIPTION), + hash?.let { arrayOf(HASH, it) }, + size?.let { arrayOf(FILE_SIZE, it) }, + dimensions?.let { arrayOf(DIMENSION, it) }, + blurhash?.let { arrayOf(BLUR_HASH, it) }, + originalHash?.let { arrayOf(ORIGINAL_HASH, it) }, + magnetURI?.let { arrayOf(MAGNET_URI, it) }, + torrentInfoHash?.let { arrayOf(TORRENT_INFOHASH, it) }, + encryptionKey?.let { arrayOf(ENCRYPTION_KEY, it.key, it.nonce) }, + sensitiveContent?.let { + if (it) { + arrayOf("content-warning", "") + } else { + null + } + }, + ) + + val content = alt ?: "" + signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) + } + } +} From f489deb808523a6b587e7e0f96f1eec80fd63166 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Sat, 6 Jul 2024 20:57:06 +0100 Subject: [PATCH 24/39] Implement custom element removal with reordering. Works nicely, from observation. --- .../amethyst/ui/actions/NewPostViewModel.kt | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) 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 55dc95a68..9142519bb 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 @@ -1278,10 +1278,29 @@ open class NewPostViewModel : ViewModel() { } fun removePollOption(optionIndex: Int) { - pollOptions.remove(optionIndex) + pollOptions.removeOrdered(optionIndex) saveDraft() } + private fun MutableMap.removeOrdered(index: Int) { + val keyList = keys + val elementList = values.toMutableList() + run stop@{ + for (i in index until elementList.size) { + val nextIndex = i + 1 + if (nextIndex == elementList.size) return@stop + elementList[i] = elementList[nextIndex].also { elementList[nextIndex] = "null" } + } + } + elementList.removeLast() + val newEntries = keyList.zip(elementList) { key, content -> Pair(key, content) } + this.clear() + this.putAll(newEntries) + + println("Keys collection size(after deletion) :${keys.size}") + println("Values collection size(after deletion) :${values.size}") + } + fun updatePollOption( optionIndex: Int, text: String, From 08e84b056e29d3c7ca72363334e3e003f2f1bff9 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Sun, 7 Jul 2024 00:06:44 +0100 Subject: [PATCH 25/39] Some cleanup. --- .../com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt | 3 --- 1 file changed, 3 deletions(-) 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 9142519bb..eedf5298e 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 @@ -1296,9 +1296,6 @@ open class NewPostViewModel : ViewModel() { val newEntries = keyList.zip(elementList) { key, content -> Pair(key, content) } this.clear() this.putAll(newEntries) - - println("Keys collection size(after deletion) :${keys.size}") - println("Values collection size(after deletion) :${values.size}") } fun updatePollOption( From dcad12f8360a6799852ece874dd9c57939a3bad2 Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Sun, 7 Jul 2024 22:01:16 +0200 Subject: [PATCH 26/39] load older notes in feed --- .../service/NostrUserProfileDataSource.kt | 2 +- .../ui/dal/UserProfileGalleryFeedFilter.kt | 14 ++-- .../ui/screen/loggedIn/ProfileGallery.kt | 69 ++++++++----------- 3 files changed, 39 insertions(+), 46 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt index 3f7139549..cdd60db09 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt @@ -84,7 +84,7 @@ object NostrUserProfileDataSource : AmethystNostrDataSource("UserProfileFeed") { WikiNoteEvent.KIND, ), authors = listOf(it.pubkeyHex), - limit = 500, + limit = 200, ), ) } 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 a4fc5eba2..5b84eeacf 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 @@ -32,7 +32,7 @@ class UserProfileGalleryFeedFilter( val user: User, val account: Account, ) : AdditiveFeedFilter() { - override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + account.defaultStoriesFollowList.value + override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + "ProfileGallery" override fun showHiddenKey(): Boolean = account.defaultStoriesFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) || @@ -46,13 +46,17 @@ class UserProfileGalleryFeedFilter( acceptableEvent(it, params, user) } + var sorted = sort(notes) var finalnotes = setOf() - for (item in notes) { - item.associatedNote = (item.event as ProfileGalleryEntryEvent).event()?.let { LocalCache.getOrCreateNote(it) } - finalnotes = finalnotes + item + for (item in sorted) { + val note = (item.event as ProfileGalleryEntryEvent).event()?.let { LocalCache.checkGetOrCreateNote(it) } + if (note != null) { + note.associatedNote = item + finalnotes = finalnotes + note + } } - return sort(finalnotes) + return finalnotes.toList() } override fun applyFilter(collection: Set): Set = innerApplyFilter(collection) 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 730db72dc..fbf2af6a8 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 @@ -133,45 +133,27 @@ private fun GalleryFeedLoaded( itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item -> val defaultModifier = remember { Modifier.fillMaxWidth().animateItemPlacement() } - if (item.associatedNote != null) { - if (item.associatedNote!!.event != null) { - if ((item.event as ProfileGalleryEntryEvent).hasUrl() && - (item.event as ProfileGalleryEntryEvent).hasEvent() - ) { - val image = (item.event as ProfileGalleryEntryEvent).url() - - Row(defaultModifier) { - if (image != null) { - GalleryCardCompose( - galleryNote = item, - image = image, - baseNote = item.associatedNote!!, - routeForLastRead = routeForLastRead, - modifier = Modifier, - forceEventKind = forceEventKind, - accountViewModel = accountViewModel, - nav = nav, - ) - } - } - - HorizontalDivider( - thickness = DividerThickness, - ) - } else { - accountViewModel.delete(item) - } - } + Row(defaultModifier) { + GalleryCardCompose( + baseNote = item, + routeForLastRead = routeForLastRead, + modifier = Modifier, + forceEventKind = forceEventKind, + accountViewModel = accountViewModel, + nav = nav, + ) } + + HorizontalDivider( + thickness = DividerThickness, + ) } } } @Composable fun GalleryCardCompose( - galleryNote: Note, baseNote: Note, - image: String, routeForLastRead: String? = null, modifier: Modifier = Modifier, parentBackgroundColor: MutableState? = null, @@ -190,15 +172,22 @@ fun GalleryCardCompose( nav = nav, ) { canPreview -> - GalleryCard( - galleryNote = galleryNote, - baseNote = baseNote, - image = image, - modifier = modifier, - parentBackgroundColor = parentBackgroundColor, - accountViewModel = accountViewModel, - nav = nav, - ) + if (baseNote.associatedNote != null) { + if (baseNote.associatedNote!!.event != null) { + val image = (baseNote.associatedNote!!.event as ProfileGalleryEntryEvent).url() + if (image != null) { + GalleryCard( + galleryNote = baseNote.associatedNote!!, + baseNote = baseNote, + image = image, + modifier = modifier, + parentBackgroundColor = parentBackgroundColor, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } + } } } } From 63c509855817ed88657250e58dbdfb93cd7351db Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 8 Jul 2024 13:33:01 -0400 Subject: [PATCH 27/39] Pausing discovery top nav list watcher. --- .../amethyst/ui/screen/loggedIn/DiscoverScreen.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt index 250eca54f..a70369032 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt @@ -168,6 +168,10 @@ fun DiscoverScreen( println("Discovery Start") NostrDiscoveryDataSource.start() } + if (event == Lifecycle.Event.ON_PAUSE) { + println("Discovery Stop") + NostrDiscoveryDataSource.stop() + } } lifeCycleOwner.lifecycle.addObserver(observer) From af4dfbf965f0cba68f00bb5f9b50c3f09ef263de Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 8 Jul 2024 13:33:16 -0400 Subject: [PATCH 28/39] Making sure posts have content to appear in the feed. --- .../vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt index 14eab0a69..f27b166ac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt @@ -94,8 +94,8 @@ class HomeNewThreadFeedFilter( noteEvent is ClassifiedsEvent || noteEvent is RepostEvent || noteEvent is GenericRepostEvent || - noteEvent is LongTextNoteEvent || - noteEvent is WikiNoteEvent || + (noteEvent is LongTextNoteEvent && noteEvent.content.isNotEmpty()) || + (noteEvent is WikiNoteEvent && noteEvent.content.isNotEmpty()) || noteEvent is PollNoteEvent || noteEvent is HighlightEvent || noteEvent is AudioTrackEvent || From fc057f2b29ba0126d18337614f3f62a9178beca0 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Mon, 8 Jul 2024 17:34:57 +0000 Subject: [PATCH 29/39] New Crowdin translations by GitHub Action --- .../src/main/res/values-es-rES/strings.xml | 74 +++++++++++++++++++ .../src/main/res/values-es-rMX/strings.xml | 73 ++++++++++++++++++ .../src/main/res/values-es-rUS/strings.xml | 73 ++++++++++++++++++ amethyst/src/main/res/values-fr/strings.xml | 13 ++++ .../src/main/res/values-hi-rIN/strings.xml | 11 +++ .../src/main/res/values-pl-rPL/strings.xml | 13 ++++ 6 files changed, 257 insertions(+) diff --git a/amethyst/src/main/res/values-es-rES/strings.xml b/amethyst/src/main/res/values-es-rES/strings.xml index cda4a8344..c10bf9dc2 100644 --- a/amethyst/src/main/res/values-es-rES/strings.xml +++ b/amethyst/src/main/res/values-es-rES/strings.xml @@ -14,6 +14,7 @@ No se pudo desencriptar el mensaje Imagen de grupo Contenido explícito + Spam El número de eventos de spam procedentes de este relé Suplantación de identidad Comportamiento ilegal @@ -35,6 +36,7 @@ Reportar suplantación de identidad Reportar contenido explícito Reportar comportamiento ilegal + Reportar malware Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder responder Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder impulsar publicaciones. Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para indicar que te gustan las publicaciones. @@ -44,11 +46,13 @@ Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder dejar de seguir a otros usuarios. Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder ocultar una palabra o frase. Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder mostrar una palabra o frase. + Zaps Total de visualizaciones Impulsar Impulsada editada edición #%1$s + original Citar Bifurcar Proponer una edición @@ -89,6 +93,7 @@ No se pudo cargar la imagen Dirección del relé Publicaciones + Bytes Errores El número de errores de conexión en esta sesión Tus noticias @@ -195,6 +200,9 @@ Dirección de Nostr nunca ahora + h + m + d Desnudos Blasfemias / Discurso de odio Denuncia discurso de odio @@ -220,6 +228,7 @@ Autenticación fallida El sistema biométrico no pudo autenticar al propietario de este teléfono El sistema biométrico no pudo autenticar al propietario de este teléfono. Error: %1$s + Error "Credo por %1$s" "Imagen del Badge para %1$s" Has recibido un nuevo Badge @@ -259,6 +268,7 @@ Suplantación de identidad malintencionada Desnudos o contenido gráfico Comportamiento ilegal + Malware Si bloqueas a un usuario, se ocultará su contenido en tu app. Tus notas todavía son visibles públicamente, incluso para las personas que bloquees. Los usuarios bloqueados aparecen en la pantalla \"Filtros de seguridad\". Reportar abuso @@ -296,6 +306,7 @@ Zap mínimo Zap máximo Consenso + (0–100)% Cerrar después de días No se puede votar @@ -326,9 +337,18 @@ El remitente y el destinatario pueden verse entre sí y leer el mensaje Anónimo El receptor y el público no saben quién envió el pago + No zap No hay rastro en Nostr, solo en Lightning Servidor de archivos Dirección o @usuario de Lightning + Servidores multimedia + Configura tus servidores preferidos para subir contenido multimedia. + No tienes ningún servidor multimedia personalizado. Puedes utilizar la lista de Amethyst, o agregar uno de la lista a continuación ↓ + Servidores multimedia integrados + Lista predeterminada de Amethyst. Puedes agregarlos individualmente o añadir la lista. + Usar lista predeterminada + Agregar servidor multimedia + Eliminar servidor multimedia Tus relés (NIP-95) Los archivos están alojados en tus relés. Nuevo NIP: comprueba si son compatibles. Configuración de Tor/Orbot @@ -336,8 +356,10 @@ ¿Desconectarse de tu Orbot/Tor? Los datos se transferirán de inmediato en la red normal + No Lista de seguidos Todos los seguidos + Global Lista de silenciados ## Conéctate a través de Tor con Orbot \n\n1. Instala [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android). @@ -355,6 +377,7 @@ Te notifica cuando llega un mensaje privado Zaps recibidos Te notifica cuando alguien te zapea + %1$s sats De %1$s por %1$s Notificar: @@ -374,6 +397,7 @@ Avisar cuando las publicaciones tengan reportes de tus seguidos Nuevo símbolo de reacción No hay tipos de reacción preseleccionados para este usuario. Deja presionado el botón de corazón para cambiarlos. + Recaudación de zaps Añade una meta de sats para recaudar por esta publicación. Los clientes compatibles pueden mostrarla como una barra de progreso para incentivar las donaciones. Cantidad objetivo en sats Zapraiser en %1$s. %2$s sats hasta la meta @@ -384,6 +408,7 @@ Se ha producido un error al intentar obtener información sobre el relé de %1$s Propietario Versión + Software Contacto NIP compatibles Tasas de admisión @@ -402,6 +427,7 @@ Etiquetas de evento máximas Longitud del contenido PoW mínima + Autenticación Pago Token de Cashu Canjear @@ -423,6 +449,7 @@ Mercado En vivo Comunidad + Chats Publicaciones aprobadas Este grupo no tiene descripción ni reglas. Habla con el propietario para agregar una. Esta comunidad no tiene descripción. Habla con el propietario para agregar una. @@ -431,6 +458,7 @@ Configuración Siempre Solo Wi-Fi + WiFi sin medición Nunca Completo Simplificado @@ -449,6 +477,7 @@ Modo de interfaz Elegir el estilo de publicación Cargar imagen + Spammers Silenciado. Hacer clic para reactivar el sonido. Sonido activado. Hacer clic para silenciar. Buscar grabaciones locales y remotas @@ -496,6 +525,7 @@ Los votos se calculan según la cantidad de zaps. Puedes establecer una cantidad mínima para evitar spammers y una máxima para que los zappers grandes no puedan dominar la encuesta. Usa la misma cantidad en ambos campos para asegurarte de que cada voto tenga el mismo valor. Déjalos vacíos para aceptar cualquier cantidad. No se pudo enviar el zap Mensaje para el usuario + OK No se pudo llegar a %1$s: %2$s Error al ensamblar la url NIP-11 para %1$s: %2$s No se pudo contactar %1$s: %2$s @@ -507,6 +537,8 @@ Activo para: Inicio Mensajes directos + Chats + Global Búsqueda Dividir y reenviar zaps Los clientes compatibles dividirán y reenviarán los zaps a los usuarios que se agreguen aquí en lugar de enviártelos a ti. @@ -515,6 +547,7 @@ Lightning no está configurado El usuario %1$s no tiene una dirección de Lightning configurada para recibir sats. Porcentaje + 25 Dividir zaps con Reenviar zaps a No se encontraron billeteras de Lightning @@ -567,6 +600,7 @@ Seleccionar una aplicación UnifiedPush Notificación push De aplicaciones UnifiedPush instaladas + Ninguno Desactiva las notificaciones push Usa la app %1$s Configuración de notificaciones push @@ -580,9 +614,11 @@ Hola, ¿esto todavía está disponible? Vender un artículo Título + iPhone 13 Estado Categoría Precio (en sats) + 1000 Ubicación Ciudad, estado, país. Nuevo @@ -616,6 +652,8 @@ El servidor no proporcionó una URL después de la carga No se pudo descargar el contenido cargado desde el servidor No se pudo preparar el archivo local para cargar: %1$s + Error al subir: %1$s + Error al eliminar: %1$s Editar borrador Iniciar sesión con código QR Ruta @@ -624,6 +662,7 @@ Descubrir Mensajes Notificaciones + Global Cortos Filtros de seguridad Nueva publicación @@ -634,6 +673,8 @@ Responder Impulsar o citar Me gusta + Zap + Cambiar reacciones rápidas Imagen de perfil de %1$s Relé %1$s Ampliar lista de relés @@ -644,6 +685,7 @@ Factura de Bitcoin Cancelar factura de Bitcoin Cancelar la venta de un artículo + Recaudación de zaps Cancelar zapraiser Ubicación Eliminar ubicación @@ -694,7 +736,9 @@ Bifurcación de BIFURCACIÓN Repositorio Git: %1$s + Web: Clon: + OTS: %1$s Prueba de marca de tiempo Hay prueba de que esta publicación se firmó en algún momento antes de %1$s. La prueba se marcó en la cadena de bloques de Bitcoin en esa fecha y hora. Editar publicación @@ -722,4 +766,34 @@ Solicitando trabajo a DVM Solicitud de pago enviada, esperando confirmación de tu monedero Esperando a que DVM confirme el pago o envíe resultados + Solicitud incorrecta: el servidor no puede procesar o no procesará la solicitud. + No autorizado - El usuario no tiene credenciales de autenticación válidas + Pago requerido: el servidor requiere pago para completar la solicitud. + Prohibido: el usuario no tiene derechos de acceso para realizar la solicitud. + No se encontró: el servidor no puede encontrar la dirección solicitada. + Método no permitido: el servidor admite el método de solicitud, pero no el recurso de destino. + No aceptable: el servidor no encuentra ningún contenido que satisfaga la solicitud. + Autenticación de proxy requerida: el usuario no tiene credenciales de autenticación válidas. + Tiempo de espera de la solicitud: se agotó el tiempo de espera del servidor. + Conflicto: el servidor no puede satisfacer la solicitud porque hay un conflicto con el recurso. + Borrado: el contenido solicitado se eliminó permanentemente del servidor y no se restablecerá. + Longitud requerida: el servidor rechaza la solicitud porque requiere una longitud definida. + Precondición fallida: las precondiciones de la solicitud en los campos de cabecera que el servidor no cumple. + Carga demasiado grande: la solicitud supera los límites definidos por el servidor, por lo que este se niega a procesarla. + URI demasiado largo: la URL solicitada por el cliente es demasiado larga para que el servidor pueda procesarla. + Tipo de contenido multimedia no admitido: la solicitud usa un formato multimedia que el servidor no admite. + Intervalo no satisfactorio: el servidor no puede satisfacer el valor indicado en el campo de cabecera del intervalo de la solicitud. + Expectativa fallida: el servidor no puede cumplir los requisitos indicados por el campo de cabecera de la solicitud Expect. + Actualización requerida: el servidor se niega a procesar la solicitud utilizando el protocolo actual a menos que el cliente se actualice a un protocolo diferente. + Error interno del servidor: el servidor encontró un error inesperado y no puede completar la solicitud. + No implementado: el servidor no puede completar la solicitud o no reconoce el método de solicitud. + Gateway incorrecto: el servidor actúa como gateway y obtuvo una respuesta no válida del host. + Servicio no disponible: esto ocurre a menudo cuando un servidor está sobrecargado o caído por mantenimiento. + Tiempo de espera del gateway: el servidor estaba actuando como gateway o proxy y se agotó el tiempo de espera de una respuesta. + Versión HTTP no admitida: el servidor no admite la versión HTTP de la solicitud. + La variante también negocia: el servidor tiene un error de configuración interno. + Almacenamiento insuficiente: el servidor no dispone de almacenamiento suficiente para procesar correctamente la solicitud. + Bucle detectado: el servidor detecta un bucle infinito al procesar la solicitud. + Autenticación de red requerida: el cliente debe estar autenticado para acceder a la red. + Agregar servidor NIP-96 diff --git a/amethyst/src/main/res/values-es-rMX/strings.xml b/amethyst/src/main/res/values-es-rMX/strings.xml index b5c3aee43..68e44875f 100644 --- a/amethyst/src/main/res/values-es-rMX/strings.xml +++ b/amethyst/src/main/res/values-es-rMX/strings.xml @@ -14,6 +14,7 @@ No se pudo desencriptar el mensaje Foto de grupo Contenido explícito + Spam El número de eventos de spam procedentes de este relé Suplantación de identidad Comportamiento ilegal @@ -35,6 +36,7 @@ Reportar suplantación de identidad Reportar contenido explícito Reportar comportamiento ilegal + Reportar malware Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder responder Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder impulsar publicaciones Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para indicar que te gustan las publicaciones. @@ -44,11 +46,13 @@ Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder dejar de seguir a otros usuarios. Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder ocultar una palabra o frase. Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder mostrar una palabra o frase. + Zaps Total de visualizaciones Impulsar Impulsada editada edición #%1$s + original Cita Bifurcar Proponer una edición @@ -89,6 +93,7 @@ Error al subir la imagen Dirección del relé Publicaciones + Bytes Errores El número de errores de conexión en esta sesión Tus noticias @@ -195,6 +200,9 @@ Dirección de Nostr nunca ahora + h + m + d Desnudez Groserías o lenguaje que incita al odio Reportar lenguaje que incita al odio @@ -220,6 +228,7 @@ Error de autenticación El sistema biométrico no pudo autenticar al propietario de este teléfono El sistema biométrico no pudo autenticar al propietario de este teléfono. Error: %1$s + Error "Creado por %1$s" "Imagen de premio de insignia por %1$s" Recibiste un nuevo premio de insignia @@ -259,6 +268,7 @@ Suplantación de identidad malintencionada Desnudez o contenido gráfico Comportamiento ilegal + Malware Si bloqueas a un usuario, se ocultará su contenido en tu app. Tus notas todavía son visibles públicamente, incluso para las personas que bloquees. Los usuarios bloqueados aparecen en la pantalla \"Filtros de seguridad\". Reportar abuso @@ -296,6 +306,7 @@ Zap mínimo Zap máximo Consenso + (0–100)% Cerrar después de días No se puede votar @@ -326,9 +337,18 @@ El remitente y el destinatario pueden verse entre sí y leer el mensaje Anónimo El receptor y el público no saben quién envió el pago + No zap No hay rastro en Nostr, solo en Lightning Servidor de archivos Dirección o @usuario de Lightning + Servidores multimedia + Configura tus servidores preferidos para subir contenido multimedia. + No tienes ningún servidor multimedia personalizado. Puedes utilizar la lista de Amethyst, o agregar uno de la lista a continuación ↓ + Servidores multimedia integrados + Lista predeterminada de Amethyst. Puedes agregarlos individualmente o añadir la lista. + Usar lista predeterminada + Agregar servidor multimedia + Eliminar servidor multimedia Tus relés (NIP-95) Los archivos están alojados en tus relés. Nuevo NIP: comprueba si son compatibles. Configuración de Tor/Orbot @@ -336,8 +356,10 @@ ¿Desconectarse de tu Orbot/Tor? Los datos se transferirán de inmediato en la red normal + No Lista de seguidos Todos los seguidos + Global Lista de silenciados ## Conéctate a través de Tor con Orbot \n\n1. Instala [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android). @@ -355,6 +377,7 @@ Te notifica cuando llega un mensaje privado Zaps recibidos Te notifica cuando alguien te zapea + %1$s sats De %1$s por %1$s Notificar: @@ -374,6 +397,7 @@ Avisar cuando las publicaciones tengan reportes de tus seguidos Nuevo símbolo de reacción No hay tipos de reacción preseleccionados para este usuario. Deja presionado el botón de corazón para cambiarlos. + Recaudación de zaps Agrega una meta de sats para recaudar por esta publicación. Los clientes compatibles pueden mostrarla como una barra de progreso para incentivar las donaciones. Cantidad objetivo en sats Zapraiser en %1$s. %2$s sats hasta la meta @@ -384,6 +408,7 @@ Se produjo un error al intentar obtener información sobre el relé de %1$s Propietario Versión + Software Contacto NIP compatibles Tarifas de admisión @@ -402,6 +427,7 @@ Etiquetas de evento máximas Longitud del contenido PoW mínima + Autenticación Pago Token de Cashu Canjear @@ -423,6 +449,7 @@ Mercado En vivo Comunidad + Chats Publicaciones aprobadas Este grupo no tiene descripción ni reglas. Habla con el propietario para agregar una. Esta comunidad no tiene descripción. Habla con el propietario para agregar una. @@ -431,6 +458,7 @@ Configuración Siempre Solo Wi-Fi + WiFi sin medición Nunca Completo Simplificado @@ -449,6 +477,7 @@ Modo de interfaz Elegir el estilo de publicación Cargar imagen + Spammers Silenciado. Hacer clic para reactivar el sonido. Sonido activado. Hacer clic para silenciar. Buscar grabaciones locales y remotas @@ -496,6 +525,7 @@ Los votos se calculan según la cantidad de zaps. Puedes establecer una cantidad mínima para evitar spammers y una máxima para que los zappers grandes no puedan dominar la encuesta. Usa la misma cantidad en ambos campos para asegurarte de que cada voto tenga el mismo valor. Déjalos vacíos para aceptar cualquier cantidad. No se pudo enviar el zap Mensaje para el usuario + OK No se pudo llegar a %1$s: %2$s Error al ensamblar la url NIP-11 para %1$s: %2$s No se pudo contactar %1$s: %2$s @@ -507,6 +537,8 @@ Activo para: Inicio Mensajes directos + Chats + Global Búsqueda Dividir y reenviar zaps Los clientes compatibles dividirán y reenviarán los zaps a los usuarios que se agreguen aquí en lugar de a ti. @@ -515,6 +547,7 @@ Lightning no está configurado El usuario %1$s no tiene una dirección de Lightning configurada para recibir sats. Porcentaje + 25 Dividir zaps con Reenviar zaps a No se encontraron billeteras de Lightning @@ -581,9 +614,11 @@ Hola, ¿esto todavía está disponible? Vender un artículo Título + iPhone 13 Estado Categoría Precio (en sats) + 1000 Ubicación Ciudad, estado, país. Nuevo @@ -617,6 +652,8 @@ El servidor no proporcionó una URL después de la subida No se pudo descargar el contenido subido desde el servidor No se pudo preparar el archivo local para subir: %1$s + Error al subir: %1$s + Error al eliminar: %1$s Editar borrador Iniciar sesión con código QR Ruta @@ -625,6 +662,7 @@ Descubrir Mensajes Notificaciones + Global Cortos Filtros de seguridad Nueva publicación @@ -635,6 +673,8 @@ Responder Impulsar o citar Me gusta + Zap + Cambiar reacciones rápidas Imagen de perfil de %1$s Relé %1$s Ampliar lista de relés @@ -645,6 +685,7 @@ Factura de Bitcoin Cancelar factura de Bitcoin Cancelar la venta de un artículo + Recaudación de zaps Cancelar zapraiser Ubicación Eliminar ubicación @@ -695,7 +736,9 @@ Bifurcación de BIFURCACIÓN Repositorio Git: %1$s + Web: Clon: + OTS: %1$s Prueba de marca de tiempo Hay prueba de que esta publicación se firmó en algún momento antes de %1$s. La prueba se marcó en la cadena de bloques de Bitcoin en esa fecha y hora. Editar publicación @@ -723,4 +766,34 @@ Solicitando trabajo a DVM Solicitud de pago enviada, esperando confirmación de tu billetera Esperando a que DVM confirme el pago o envíe resultados + Solicitud incorrecta: el servidor no puede procesar o no procesará la solicitud. + No autorizado: el usuario no tiene credenciales de autenticación válidas. + Pago requerido: el servidor requiere pago para completar la solicitud. + Prohibido: el usuario no tiene derechos de acceso para realizar la solicitud. + No se encontró: el servidor no puede encontrar la dirección solicitada. + Método no permitido: el servidor admite el método de solicitud, pero no el recurso de destino. + No aceptable: el servidor no encuentra ningún contenido que satisfaga la solicitud. + Autenticación de proxy requerida: el usuario no tiene credenciales de autenticación válidas. + Tiempo de espera de la solicitud: se agotó el tiempo de espera del servidor. + Conflicto: el servidor no puede satisfacer la solicitud porque hay un conflicto con el recurso. + Borrado: el contenido solicitado se eliminó permanentemente del servidor y no se restablecerá. + Longitud requerida: el servidor rechaza la solicitud porque requiere una longitud definida. + Precondición fallida: las precondiciones de la solicitud en los campos de cabecera que el servidor no cumple. + Carga demasiado grande: la solicitud supera los límites definidos por el servidor, por lo que este se niega a procesarla. + URI demasiado largo: la URL solicitada por el cliente es demasiado larga para que el servidor pueda procesarla. + Tipo de contenido multimedia no admitido: la solicitud usa un formato multimedia que el servidor no admite. + Intervalo no satisfactorio: el servidor no puede satisfacer el valor indicado en el campo de cabecera del intervalo de la solicitud. + Expectativa fallida: el servidor no puede cumplir los requisitos indicados por el campo de cabecera de la solicitud Expect. + Actualización requerida: el servidor se niega a procesar la solicitud utilizando el protocolo actual a menos que el cliente se actualice a un protocolo diferente. + Error interno del servidor: el servidor encontró un error inesperado y no puede completar la solicitud. + No implementado: el servidor no puede completar la solicitud o no reconoce el método de solicitud. + Gateway incorrecto: el servidor actúa como gateway y obtuvo una respuesta no válida del host. + Servicio no disponible: esto ocurre a menudo cuando un servidor está sobrecargado o caído por mantenimiento. + Tiempo de espera del gateway: el servidor estaba actuando como gateway o proxy y se agotó el tiempo de espera de una respuesta. + Versión HTTP no admitida: el servidor no admite la versión HTTP de la solicitud. + La variante también negocia: el servidor tiene un error de configuración interno. + Almacenamiento insuficiente: el servidor no dispone de almacenamiento suficiente para procesar correctamente la solicitud. + Bucle detectado: el servidor detecta un bucle infinito al procesar la solicitud. + Autenticación de red requerida: el cliente debe estar autenticado para acceder a la red. + Agregar servidor NIP-96 diff --git a/amethyst/src/main/res/values-es-rUS/strings.xml b/amethyst/src/main/res/values-es-rUS/strings.xml index decf118f1..8d6c99acc 100644 --- a/amethyst/src/main/res/values-es-rUS/strings.xml +++ b/amethyst/src/main/res/values-es-rUS/strings.xml @@ -14,6 +14,7 @@ No se pudo desencriptar el mensaje Imagen del grupo Contenido explícito + Spam El número de eventos de spam procedentes de este relé Suplantación de identidad Comportamiento ilegal @@ -35,6 +36,7 @@ Reportar suplantación de identidad Reportar contenido explícito Reportar comportamiento ilegal + Reportar malware Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder responder Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder impulsar publicaciones Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para indicar que te gustan las publicaciones. @@ -44,11 +46,13 @@ Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder dejar de seguir a otros usuarios. Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder ocultar una palabra o frase. Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder mostrar una palabra o frase. + Zaps Total vistas Impulsar Impulsada editada edición #%1$s + original Cita Bifurcar Proponer una edición @@ -89,6 +93,7 @@ Error al subir la imagen Dirección del relé Publicaciones + Bytes Errores El número de errores de conexión en esta sesión Tus noticias @@ -195,6 +200,9 @@ Dirección de Nostr nunca ahora + h + m + d Desnudez Groserías o lenguaje que incita al odio Reportar lenguaje que incita al odio @@ -220,6 +228,7 @@ Error de autenticación El sistema biométrico no pudo autenticar al propietario de este teléfono El sistema biométrico no pudo autenticar al propietario de este teléfono. Error: %1$s + Error "Creado por %1$s" "Imagen de premio de insignia por %1$s" Recibiste un nuevo premio de insignia @@ -259,6 +268,7 @@ Suplantación de identidad malintencionada Desnudez o contenido gráfico Comportamiento ilegal + Malware Si bloqueas a un usuario, se ocultará su contenido en tu app. Tus notas todavía son visibles públicamente, incluso para las personas que bloquees. Los usuarios bloqueados aparecen en la pantalla \"Filtros de seguridad\". Reportar abuso @@ -296,6 +306,7 @@ Zap mínimo Zap máximo Consenso + (0–100)% Cerrar después de días No se puede votar @@ -326,9 +337,18 @@ El remitente y el receptor pueden verse entre sí y leer el mensaje Anónimo El receptor y el público no saben quién envió el pago + No zap No hay rastro en Nostr, solo en Lightning Servidor de archivos Dirección o @usuario de Lightning + Servidores multimedia + Configura tus servidores preferidos para subir contenido multimedia. + No tienes ningún servidor multimedia personalizado. Puedes utilizar la lista de Amethyst, o agregar uno de la lista a continuación ↓ + Servidores multimedia integrados + Lista predeterminada de Amethyst. Puedes agregarlos individualmente o añadir la lista. + Usar lista predeterminada + Agregar servidor multimedia + Eliminar servidor multimedia Tus relés (NIP-95) Los archivos están alojados en tus relés. Nuevo NIP: comprueba si son compatibles. Configuración de Tor/Orbot @@ -336,8 +356,10 @@ ¿Desconectarse de tu Orbot/Tor? Los datos se transferirán de inmediato en la red normal + No Lista de seguidos Todos los seguidos + Global Lista de silenciados ## Conéctate a través de Tor con Orbot \n\n1. Instala [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android). @@ -355,6 +377,7 @@ Te notifica cuando llega un mensaje privado Zaps recibidos Te notifica cuando alguien te zapea + %1$s sats De %1$s por %1$s Notificar: @@ -374,6 +397,7 @@ Avisar cuando las publicaciones tengan reportes de tus seguidos Nuevo símbolo de reacción No hay tipos de reacción preseleccionados para este usuario. Deja presionado el botón de corazón para cambiarlos. + Recaudación de zaps Agrega una meta de sats para recaudar por esta publicación. Los clientes compatibles pueden mostrarla como una barra de progreso para incentivar las donaciones. Cantidad objetivo en sats Zapraiser en %1$s. %2$s sats hasta la meta @@ -384,6 +408,7 @@ Se produjo un error al intentar obtener información sobre el relé de %1$s Propietario Versión + Software Contacto NIP compatibles Comisiones de admisión @@ -402,6 +427,7 @@ Etiquetas de evento máximas Longitud del contenido PoW mínima + Autenticación Pago Token de Cashu Canjear @@ -423,6 +449,7 @@ Mercado En vivo Comunidad + Chats Publicaciones aprobadas Este grupo no tiene descripción ni reglas. Habla con el propietario para agregar una. Esta comunidad no tiene descripción. Habla con el propietario para agregar una. @@ -431,6 +458,7 @@ Configuración Siempre Solo Wi-Fi + WiFi sin medición Nunca Completo Simplificado @@ -449,6 +477,7 @@ Modo de interfaz Elegir el estilo de publicación Cargar imagen + Spammers Silenciado. Hacer clic para reactivar el sonido. Sonido activado. Hacer clic para silenciar. Buscar grabaciones locales y remotas @@ -496,6 +525,7 @@ Los votos se calculan según la cantidad de zaps. Puedes establecer una cantidad mínima para evitar spammers y una máxima para que los zappers grandes no puedan dominar la encuesta. Usa la misma cantidad en ambos campos para asegurarte de que cada voto tenga el mismo valor. Déjalos vacíos para aceptar cualquier cantidad. No se pudo enviar el zap Mensaje para el usuario + OK No se pudo llegar a %1$s: %2$s Error al ensamblar la url NIP-11 para %1$s: %2$s No se pudo contactar %1$s: %2$s @@ -507,6 +537,8 @@ Activo para: Inicio Mensajes directos + Chats + Global Búsqueda Dividir y reenviar zaps Los clientes compatibles dividirán y reenviarán los zaps a los usuarios que se agreguen aquí en lugar de a ti. @@ -515,6 +547,7 @@ Lightning no está configurado El usuario %1$s no tiene una dirección de Lightning configurada para recibir sats. Porcentaje + 25 Dividir zaps con Reenviar zaps a No se encontraron billeteras de Lightning @@ -581,9 +614,11 @@ Hola, ¿esto todavía está disponible? Vender un artículo Título + iPhone 13 Estado Categoría Precio (en sats) + 1000 Ubicación Ciudad, estado, país. Nuevo @@ -617,6 +652,8 @@ El servidor no proporcionó una URL después de la subida No se pudo descargar el contenido subido desde el servidor No se pudo preparar el archivo local para subir: %1$s + Error al subir: %1$s + Error al eliminar: %1$s Editar borrador Iniciar sesión con código QR Ruta @@ -625,6 +662,7 @@ Descubrir Mensajes Notificaciones + Global Cortos Filtros de seguridad Nueva publicación @@ -635,6 +673,8 @@ Responder Impulsar o citar Me gusta + Zap + Cambiar reacciones rápidas Imagen de perfil de %1$s Relé %1$s Ampliar lista de relés @@ -645,6 +685,7 @@ Factura de Bitcoin Cancelar factura de Bitcoin Cancelar la venta de un artículo + Recaudación de zaps Cancelar zapraiser Ubicación Eliminar ubicación @@ -695,7 +736,9 @@ Bifurcación de BIFURCACIÓN Repositorio Git: %1$s + Web: Clon: + OTS: %1$s Prueba de marca de tiempo Hay prueba de que esta publicación se firmó en algún momento antes de %1$s. La prueba se marcó en la cadena de bloques de Bitcoin en esa fecha y hora. Editar publicación @@ -723,4 +766,34 @@ Solicitando trabajo a DVM Solicitud de pago enviada, esperando confirmación de tu billetera Esperando a que DVM confirme el pago o envíe resultados + Solicitud incorrecta: el servidor no puede procesar o no procesará la solicitud. + No autorizado: el usuario no tiene credenciales de autenticación válidas. + Pago requerido: el servidor requiere pago para completar la solicitud. + Prohibido: el usuario no tiene derechos de acceso para realizar la solicitud. + No se encontró: el servidor no puede encontrar la dirección solicitada. + Método no permitido: el servidor admite el método de solicitud, pero no el recurso de destino. + No aceptable: el servidor no encuentra ningún contenido que satisfaga la solicitud. + Autenticación de proxy requerida: el usuario no tiene credenciales de autenticación válidas. + Tiempo de espera de la solicitud: se agotó el tiempo de espera del servidor. + Conflicto: el servidor no puede satisfacer la solicitud porque hay un conflicto con el recurso. + Borrado: el contenido solicitado se eliminó permanentemente del servidor y no se restablecerá. + Longitud requerida: el servidor rechaza la solicitud porque requiere una longitud definida. + Precondición fallida: las precondiciones de la solicitud en los campos de cabecera que el servidor no cumple. + Carga demasiado grande: la solicitud supera los límites definidos por el servidor, por lo que este se niega a procesarla. + URI demasiado largo: la URL solicitada por el cliente es demasiado larga para que el servidor pueda procesarla. + Tipo de contenido multimedia no admitido: la solicitud usa un formato multimedia que el servidor no admite. + Intervalo no satisfactorio: el servidor no puede satisfacer el valor indicado en el campo de cabecera del intervalo de la solicitud. + Expectativa fallida: el servidor no puede cumplir los requisitos indicados por el campo de cabecera de la solicitud Expect. + Actualización requerida: el servidor se niega a procesar la solicitud utilizando el protocolo actual a menos que el cliente se actualice a un protocolo diferente. + Error interno del servidor: el servidor encontró un error inesperado y no puede completar la solicitud. + No implementado: el servidor no puede completar la solicitud o no reconoce el método de solicitud. + Gateway incorrecto: el servidor actúa como gateway y obtuvo una respuesta no válida del host. + Servicio no disponible: esto ocurre a menudo cuando un servidor está sobrecargado o caído por mantenimiento. + Tiempo de espera del gateway: el servidor estaba actuando como gateway o proxy y se agotó el tiempo de espera de una respuesta. + Versión HTTP no admitida: el servidor no admite la versión HTTP de la solicitud. + La variante también negocia: el servidor tiene un error de configuración interno. + Almacenamiento insuficiente: el servidor no dispone de almacenamiento suficiente para procesar correctamente la solicitud. + Bucle detectado: el servidor detecta un bucle infinito al procesar la solicitud. + Autenticación de red requerida: el cliente debe estar autenticado para acceder a la red. + Agregar servidor NIP-96 diff --git a/amethyst/src/main/res/values-fr/strings.xml b/amethyst/src/main/res/values-fr/strings.xml index 4a5264570..bb90e91cb 100644 --- a/amethyst/src/main/res/values-fr/strings.xml +++ b/amethyst/src/main/res/values-fr/strings.xml @@ -36,6 +36,7 @@ Signaler une falsification d\'identité Signaler un contenu choquant Signaler un comportement illégal + Signaler un programme malveillant Connectez vous avec une clé privée pour pouvoir répondre Connectez vous avec une clé privée pour pouvoir booster des posts Connectez vous avec une clé privée pour pouvoir aimer des posts @@ -267,6 +268,7 @@ Usurpation d\'identité Nudité ou contenu graphique Comportement illégal + Programme malveillant Bloquer un utilisateur cachera son contenu dans votre application. Vos notes sont toujours visibles publiquement, y compris pour les personnes que vous bloquez. Les utilisateurs bloqués sont listés sur l\'écran Filtres de Sécurité. Signaler un Abus @@ -339,6 +341,14 @@ Aucune trace sur Nostr, seulement sur le Lightning Serveur fichier Adresse LN ou @Utilisateur + Serveurs média + Définissez vos serveurs de téléversement de média préférés. + Vous n\'avez pas de serveur média personnalisé. Vous pouvez utiliser la liste d\'Amethyst, ou en ajouter un ci-dessous + Serveurs média intégrés + Liste par défaut d\'Amethyst. Vous pouvez les ajouter individuellement ou ajouter la liste. + Utiliser la liste par défaut + Ajouter un serveur média + Supprimer le serveur média Vos relais (NIP-95) Les fichiers sont hébergés par vos relais. Nouveau NIP: vérifiez s\'ils sont supportés Configuration Tor/Orbot @@ -642,6 +652,8 @@ Le serveur n\'a pas fourni d\'URL après le téléversement Impossible de télécharger le média depuis le serveur Impossible de préparer le fichier local à téléverser: %1$s + Échec du téléversement: %1$s + Échec de la suppression : %1$s Modifier le brouillon Se connecter avec un QR Code Route @@ -754,4 +766,5 @@ Tâche demandée par DVM Demande de paiement envoyée, en attente de confirmation depuis votre portefeuille En attente que DVM confirme le paiement ou envoie les résultats + Ajouter un serveur NIP-96 diff --git a/amethyst/src/main/res/values-hi-rIN/strings.xml b/amethyst/src/main/res/values-hi-rIN/strings.xml index 18fdc51fc..7887e7c73 100644 --- a/amethyst/src/main/res/values-hi-rIN/strings.xml +++ b/amethyst/src/main/res/values-hi-rIN/strings.xml @@ -36,6 +36,7 @@ पररूपण की सूचना दें अभद्र विषयवस्तु की सूचना दें अवैध बरताव की सूचना दें + दुष्क्रमक की सूचना दें आप ख्याप्य कुंचिका का प्रयोग कर रहे हैं जिससे केवल पढ सकते हैं। उत्तर देने के लिए गुप्त कुंचिका के साथ प्रवेशांकन करें आप ख्याप्य कुंचिका का प्रयोग कर रहे हैं जिससे केवल पढ सकते हैं। प्रकाशित पत्रों को उद्धृत करने के लिए गुप्त कुंचिका के साथ प्रवेशांकन करें आप ख्याप्य कुंचिका का प्रयोग कर रहे हैं जिससे केवल पढ सकते हैं। प्रकाशित पत्रों को चाहने के लिए गुप्त कुंचिका के साथ प्रवेशांकन करें @@ -267,6 +268,7 @@ दुर्भावनापूर्ण पररूपण नग्नता अथवा आपत्तिजनक विषयवस्तु अवैध बरताव + दुष्क्रमक उपयोगकर्ता को बाधित करने से उनका विषयवस्तु आपके लिए छिपाया जाएगा क्रमक में। आपके टीकाएँ सार्वजनिक रूप से दृश्य रहेंगे, उन जनों के लिए भी जिनहे आपने बाधित किया। बाधित उपयोगकर्ता सूचित हैं सुरक्षा छलनियाँ पटल पर। दुर्व्यवहार की सूचना दें @@ -339,6 +341,14 @@ नोस्ट्र में कोई पदचिह्न नहीं, केवल लैटनिंग पर अभिलेख सेवासंगणक लै॰जाल पता अथवा @उपयोगकर्ता + प्रसारसंगणक + आपके प्रसारसंगणक आद्यताएँ स्थापित करें। + आपका कोई विशिष्ट प्रसारसंगणक स्थापित नहीं। आप अमेथिस्ट की सूची का प्रयोग कर सकते हैं अथवा प्रसारसंगणक नीचे जोड सकते हैं ↓ + अन्तर्निहित प्रसारसंगणक + अमेथिस्त की मूलविकल्प सूची। आप एक एक करके जोड सकते हैं अथवा सूची जोड सकते हैं। + मूलविकल्प सूची का प्रयोग करें + प्रसारसंगणक जोडें + प्रसारसंगणक मिटाएँ आपके पुनःप्रसारक (निप॰-९५) अभिलेख आपके पुनःप्रसारक द्वारा रखे जाते हैं। नया निप॰: जाँच करें यदि वे अवलम्बन करते हैं टोर / ओर्बोट स्थापन @@ -785,4 +795,5 @@ स्मृतिस्थान का अभाव - सेवासंगणक में पर्याप्त स्मृतिस्थान उपलब्ध नहीं अनुरोध पर सफलतापूर्वक काम करने के लिए क्रमचक्र दृष्ट - सेवासंगणक को अनन्त क्रमचक्र का पता चला अनुरोध पर काम करते हुए जाल प्रमाणीकरण आवश्यक - जाल उपलब्ध होने के लिए ग्राहक का प्रमाणीकरण अनिवार्य + निप॰-९६ सेवासंगणक जोडें diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index 3a4ad34ef..04529a94a 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -36,6 +36,7 @@ Zgłoś podszywanie się Zgłoś niedozwoloną zawartość Zgłoś antyspołeczne działania + Zgłoś złośliwe oprogramowanie Używasz klucza publicznego, a klucze publiczne są tylko do odczytu. Zaloguj się za pomocą klucza prywatnego, aby móc odpowiedzieć Używasz klucza publicznego, a klucze publiczne są tylko do odczytu. Zaloguj się za pomocą klucza prywatnego, aby zwiększyć liczbę postów Używasz klucza publicznego, a klucze publiczne są tylko do odczytu. Zaloguj się za pomocą klucza prywatnego, aby polubić posty @@ -267,6 +268,7 @@ Złośliwe podszywanie się Nagość lub obraźliwa grafika Antyspołeczne zachowanie + Złośliwe oprogramowanie Zablokowanie użytkownika ukryje jego zawartość w aplikacji. Twoje notatki są nadal widoczne publicznie, w tym dla osób, które blokujesz. Zablokowani użytkownicy są wymienieni na ekranie filtrów bezpieczeństwa. Zgłoś nadużycie @@ -339,6 +341,14 @@ Brak śladu w Nostr, tylko w Lightning Serwer Plików LnAdres lub @Użytkownik + Serwery Multimediów + Ustaw swoje preferowane serwery przesyłania multimediów. + Nie masz ustawionych własnych serwerów multimediów. Możesz użyć listy Amethyst\'a lub dodać jeden poniżej ↓ + Wbudowane serwery multimediów + Domyślna lista Amethyst-a. Możesz dodawać je pojedynczo lub dodać pełną listę. + Użyj domyślnej listy + Dodaj serwer multimediów + Usuń serwer multimediów Twoje transmitery (NIP-95) Pliki są przechowywane przez Twoje retransmitery. Nowy NIP: sprawdź, czy jest obsługiwany Tor/Orbot - Ustawienia @@ -426,6 +436,8 @@ Skopiuj token Nie ustawiono adresu Lightning Skopiowano token do schowka + NA ŻYWO + OFFLINE Zakończony ZAPLANOWANE Transmisja wyłączona @@ -776,4 +788,5 @@ 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 + Dodaj serwer NIP-96 From 2074b34ec346e434f4d45531ec008f52ff3e2334 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 8 Jul 2024 13:55:27 -0400 Subject: [PATCH 30/39] Trying maxMetaspaceSize to see if Github Actions can build it. --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index cdc50ba11..8d4301840 100644 --- a/gradle.properties +++ b/gradle.properties @@ -24,4 +24,4 @@ android.nonTransitiveRClass=true android.enableR8.fullMode=true android.nonFinalResIds=false -kotlin.daemon.jvmargs=-Xmx4096m \ No newline at end of file +kotlin.daemon.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=2g \ No newline at end of file From f6b947430a2afda44a8cdc19a96a6fa8bd9d983c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 9 Jul 2024 10:42:22 -0400 Subject: [PATCH 31/39] Removes the bugfix for reproducible builds since it has been fixed. --- amethyst/build.gradle | 34 ++-------------------------------- 1 file changed, 2 insertions(+), 32 deletions(-) diff --git a/amethyst/build.gradle b/amethyst/build.gradle index 83082f576..eafee3d77 100644 --- a/amethyst/build.gradle +++ b/amethyst/build.gradle @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { alias(libs.plugins.androidApplication) alias(libs.plugins.jetbrainsKotlinAndroid) @@ -293,35 +295,3 @@ dependencies { debugImplementation libs.androidx.ui.test.manifest } -// https://gitlab.com/fdroid/wiki/-/wikis/HOWTO:-diff-&-fix-APKs-for-Reproducible-Builds#differing-assetsdexoptbaselineprofm-easy-to-fix -// NB: Android Studio can't find the imports; this does not affect the -// actual build since Gradle can find them just fine. -import com.android.tools.profgen.ArtProfileKt -import com.android.tools.profgen.ArtProfileSerializer -import com.android.tools.profgen.DexFile -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -project.afterEvaluate { - tasks.each { task -> - if (task.name.startsWith("compile") && task.name.endsWith("ReleaseArtProfile")) { - task.doLast { - outputs.files.each { file -> - if (file.name.endsWith(".profm")) { - println("Sorting ${file} ...") - def version = ArtProfileSerializer.valueOf("METADATA_0_0_2") - def profile = ArtProfileKt.ArtProfile(file) - def keys = new ArrayList(profile.profileData.keySet()) - def sortedData = new LinkedHashMap() - Collections.sort keys, new DexFile.Companion() - keys.each { key -> sortedData[key] = profile.profileData[key] } - new FileOutputStream(file).with { - write(version.magicBytes$profgen) - write(version.versionBytes$profgen) - version.write$profgen(it, sortedData, "") - } - } - } - } - } - } -} From aa4e6c051e5738ca574be008e00069da5319dba7 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 9 Jul 2024 10:42:29 -0400 Subject: [PATCH 32/39] formatting --- .../com/vitorpamplona/amethyst/ui/dal/GeoHashFeedFilter.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/GeoHashFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/GeoHashFeedFilter.kt index f2cd9b72e..2c759ec09 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/GeoHashFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/GeoHashFeedFilter.kt @@ -62,7 +62,9 @@ class GeoHashFeedFilter( it.event is PrivateDmEvent || it.event is PollNoteEvent || it.event is AudioHeaderEvent - ) && it.event?.isTaggedGeoHash(geoTag) == true && account.isAcceptable(it) + ) && + it.event?.isTaggedGeoHash(geoTag) == true && + account.isAcceptable(it) override fun sort(collection: Set): List = collection.sortedWith(DefaultFeedOrder) } From 2a9e1bb08c26fdb53b4da19778db604ab9bc5697 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 9 Jul 2024 10:42:50 -0400 Subject: [PATCH 33/39] solves build slowdown on spotless dependencies. --- build.gradle | 41 +++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/build.gradle b/build.gradle index 306762d2b..ae88392b4 100644 --- a/build.gradle +++ b/build.gradle @@ -4,34 +4,47 @@ plugins { alias(libs.plugins.androidLibrary) apply false alias(libs.plugins.jetbrainsKotlinJvm) apply false alias(libs.plugins.androidBenchmark) apply false - alias(libs.plugins.diffplugSpotless) apply false + alias(libs.plugins.diffplugSpotless) alias(libs.plugins.googleServices) apply false alias(libs.plugins.jetbrainsComposeCompiler) apply false } -subprojects { +allprojects { apply plugin: 'com.diffplug.spotless' - spotless { - kotlin { - target '**/*.kt' - targetExclude("$layout.buildDirectory/**/*.kt") - ktlint("1.3.1") - licenseHeaderFile rootProject.file('spotless/copyright.kt'), "package|import|class|object|sealed|open|interface|abstract " + if (project === rootProject) { + spotless { + predeclareDeps() } - - groovyGradle { - target '*.gradle' + spotlessPredeclare { + kotlin { + ktlint("1.3.1") + } } + } else { + spotless { + kotlin { + target '**/*.kt' + targetExclude("$layout.buildDirectory/**/*.kt") - afterEvaluate { - tasks.named("preBuild") { - dependsOn("spotlessApply") + ktlint("1.3.1") + licenseHeaderFile rootProject.file('spotless/copyright.kt'), "package|import|class|object|sealed|open|interface|abstract " + } + + groovyGradle { + target '*.gradle' } } } } +subprojects { + afterEvaluate { + tasks.named("preBuild") { + dependsOn("spotlessApply") + } + } +} tasks.register('installGitHook', Copy) { from new File(rootProject.rootDir, 'git-hooks/pre-commit') From b360c3292f91a7bef20b4f9b92a613a382f355b9 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 9 Jul 2024 10:45:51 -0400 Subject: [PATCH 34/39] removes unnecessary minification for benchmark's debug flavor --- benchmark/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmark/build.gradle b/benchmark/build.gradle index c56b90ca0..2f907d171 100644 --- a/benchmark/build.gradle +++ b/benchmark/build.gradle @@ -31,7 +31,7 @@ android { debug { // Since debuggable can"t be modified by gradle for library modules, // it must be done in a manifest - see src/androidTest/AndroidManifest.xml - minifyEnabled true + minifyEnabled false proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "benchmark-proguard-rules.pro" } release { From 6fd228281814f8b5a92a0ee857043f954cb5587e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 9 Jul 2024 10:55:02 -0400 Subject: [PATCH 35/39] increasing memory for kotlin build --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 8d4301840..55f63fdfc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -24,4 +24,4 @@ android.nonTransitiveRClass=true android.enableR8.fullMode=true android.nonFinalResIds=false -kotlin.daemon.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=2g \ No newline at end of file +kotlin.daemon.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=3g \ No newline at end of file From 217870244bfdb63b8af811dc491888b024f5c194 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 9 Jul 2024 11:10:34 -0400 Subject: [PATCH 36/39] No need for material dependencies anymore. --- gradle/libs.versions.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3e5ba2eaa..e5f8dd1ef 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -47,8 +47,6 @@ zelory = "3.0.1" zoomable = "1.6.1" zxing = "3.5.3" zxingAndroidEmbedded = "4.3.0" -material = "1.10.0" -materialVersion = "1.12.0" [libraries] abedElazizShe-image-compressor = { group = "com.github.AbedElazizShe", name = "LightCompressor", version.ref = "lightcompressor" } @@ -119,7 +117,6 @@ zelory-video-compressor = { group = "id.zelory", name = "compressor", version.re zoomable = { group = "net.engawapg.lib", name = "zoomable", version.ref = "zoomable" } zxing = { group = "com.google.zxing", name = "core", version.ref = "zxing" } zxing-embedded = { group = "com.journeyapps", name = "zxing-android-embedded", version.ref = "zxingAndroidEmbedded" } -material = { group = "com.google.android.material", name = "material", version.ref = "materialVersion" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } From c7061066e9931e32d4bf1e43c9f268f894f1c7a6 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 9 Jul 2024 17:30:16 -0400 Subject: [PATCH 37/39] Adds an outbox cache to the Relay class in order to resend events after authentication. --- .../vitorpamplona/ammolite/relays/Relay.kt | 65 +++++++++---------- 1 file changed, 29 insertions(+), 36 deletions(-) diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/Relay.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/Relay.kt index 6a352b6e9..dcfb7e01a 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/Relay.kt +++ b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/Relay.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.ammolite.relays import android.util.Log -import com.vitorpamplona.ammolite.BuildConfig import com.vitorpamplona.ammolite.service.HttpClientManager import com.vitorpamplona.ammolite.service.checkNotInMainThread import com.vitorpamplona.quartz.encoders.HexKey @@ -75,7 +74,7 @@ class Relay( private var afterEOSEPerSubscription = mutableMapOf() private val authResponse = mutableMapOf() - private val sendWhenReady = mutableListOf() + private val outboxCache = mutableMapOf() fun register(listener: Listener) { listeners = listeners.plus(listener) @@ -93,6 +92,15 @@ class Relay( // Sends everything. renewFilters() + sendOutbox() + } + } + + private fun sendOutbox() { + synchronized(outboxCache) { + outboxCache.values.forEach { + send(it) + } } } @@ -157,13 +165,6 @@ class Relay( // Log.w("Relay", "Relay OnOpen, Loading All subscriptions $url") onConnected(this@Relay) - synchronized(sendWhenReady) { - sendWhenReady.forEach { - send(it) - } - sendWhenReady.clear() - } - listeners.forEach { it.onRelayStateChange(this@Relay, StateType.CONNECT, null) } } @@ -310,8 +311,17 @@ class Relay( if (authResponse.containsKey(eventId)) { val wasAlreadyAuthenticated = authResponse.get(eventId) authResponse.put(eventId, success) + println("AABBCC Auth Response $url $wasAlreadyAuthenticated $success") if (wasAlreadyAuthenticated != true && success) { renewFilters() + sendOutbox() + } + } + + if (outboxCache.contains(eventId) && !message.startsWith("auth-required")) { + Log.w("Relay", "Relay on OK $url with message `$message`") + synchronized(outboxCache) { + outboxCache.remove(eventId) } } @@ -325,7 +335,7 @@ class Relay( } "AUTH" -> listeners.forEach { - // Log.w("Relay", "Relay onAuth $url, ${msg[1].asString}") + Log.w("Relay", "Relay onAuth $url, ${ msgArray[1].asText()}") it.onAuth(this@Relay, msgArray[1].asText()) } "NOTIFY" -> @@ -477,23 +487,7 @@ class Relay( sendAuth(signedEvent) } else { if (write) { - if (isConnected()) { - if (isReady) { - writeToSocket("""["EVENT",${signedEvent.toJson()}]""") - } else { - synchronized(sendWhenReady) { - sendWhenReady.add(signedEvent) - } - } - } else { - // sends all filters after connection is successful. - connectAndRun { - writeToSocket("""["EVENT",${signedEvent.toJson()}]""") - - // Sends everything. - renewFilters() - } - } + sendEvent(signedEvent) } } } @@ -504,21 +498,20 @@ class Relay( } private fun sendEvent(signedEvent: EventInterface) { + synchronized(outboxCache) { + outboxCache.put(signedEvent.id(), signedEvent) + } + if (isConnected()) { if (isReady) { writeToSocket("""["EVENT",${signedEvent.toJson()}]""") - } else { - synchronized(sendWhenReady) { - sendWhenReady.add(signedEvent) - } } } else { // sends all filters after connection is successful. connectAndRun { - writeToSocket("""["EVENT",${signedEvent.toJson()}]""") - // Sends everything. renewFilters() + sendOutbox() } } } @@ -541,9 +534,9 @@ class Relay( } RelayStats.addBytesSent(url, str.bytesUsedInMemory()) - if (BuildConfig.DEBUG) { - Log.d("Relay", "Relay send $url $str") - } + // if (BuildConfig.DEBUG) { + Log.d("Relay", "Relay send $url $str") + // } } } From 8feac270f4a24c217d4504a648d3dc84aff97d79 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 9 Jul 2024 18:00:25 -0400 Subject: [PATCH 38/39] Removes unnecessary logs. --- .../java/com/vitorpamplona/ammolite/relays/Relay.kt | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/Relay.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/Relay.kt index dcfb7e01a..770058460 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/Relay.kt +++ b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/Relay.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.ammolite.relays import android.util.Log +import com.vitorpamplona.ammolite.BuildConfig import com.vitorpamplona.ammolite.service.HttpClientManager import com.vitorpamplona.ammolite.service.checkNotInMainThread import com.vitorpamplona.quartz.encoders.HexKey @@ -308,10 +309,11 @@ class Relay( val success = msgArray[2].asBoolean() val message = if (msgArray.size() > 2) msgArray[3].asText() else "" + Log.w("Relay", "Relay on OK $url, $eventId, $success, $message") + if (authResponse.containsKey(eventId)) { val wasAlreadyAuthenticated = authResponse.get(eventId) authResponse.put(eventId, success) - println("AABBCC Auth Response $url $wasAlreadyAuthenticated $success") if (wasAlreadyAuthenticated != true && success) { renewFilters() sendOutbox() @@ -319,14 +321,11 @@ class Relay( } if (outboxCache.contains(eventId) && !message.startsWith("auth-required")) { - Log.w("Relay", "Relay on OK $url with message `$message`") synchronized(outboxCache) { outboxCache.remove(eventId) } } - Log.w("Relay", "Relay on OK $url, $eventId, $success, $message") - if (!success) { RelayStats.newNotice(url, "Failed to receive $eventId: $message") } @@ -534,9 +533,9 @@ class Relay( } RelayStats.addBytesSent(url, str.bytesUsedInMemory()) - // if (BuildConfig.DEBUG) { - Log.d("Relay", "Relay send $url $str") - // } + if (BuildConfig.DEBUG) { + Log.d("Relay", "Relay send $url $str") + } } } From 1a70a02b442183d5eebf53688629b9b7f7b893a7 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 10 Jul 2024 16:04:39 -0400 Subject: [PATCH 39/39] Fixes scoping issues with flattenMerge --- .../vitorpamplona/amethyst/model/Account.kt | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 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 1995916c5..7ce523620 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -119,7 +119,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combineTransform import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flattenMerge +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transformLatest @@ -481,10 +481,9 @@ class Account( @OptIn(ExperimentalCoroutinesApi::class) private val liveNotificationList: Flow by lazy { - defaultNotificationFollowList - .transformLatest { listName -> - emit(loadPeopleListFlowFromListName(listName)) - }.flattenMerge() + defaultNotificationFollowList.flatMapLatest { listName -> + loadPeopleListFlowFromListName(listName) + } } val liveNotificationFollowLists: StateFlow by lazy { @@ -494,10 +493,9 @@ class Account( @OptIn(ExperimentalCoroutinesApi::class) private val liveStoriesList: Flow by lazy { - defaultStoriesFollowList - .transformLatest { listName -> - emit(loadPeopleListFlowFromListName(listName)) - }.flattenMerge() + defaultStoriesFollowList.flatMapLatest { listName -> + loadPeopleListFlowFromListName(listName) + } } val liveStoriesFollowLists: StateFlow by lazy { @@ -507,10 +505,9 @@ class Account( @OptIn(ExperimentalCoroutinesApi::class) private val liveDiscoveryList: Flow by lazy { - defaultDiscoveryFollowList - .transformLatest { listName -> - emit(loadPeopleListFlowFromListName(listName)) - }.flattenMerge() + defaultDiscoveryFollowList.flatMapLatest { listName -> + loadPeopleListFlowFromListName(listName) + } } val liveDiscoveryFollowLists: StateFlow by lazy {