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<User, Set<Note>>()
         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<Boolean>,
     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<Int>? = null,
     keepPlaying: MutableState<Boolean>,
     automaticallyStartPlayback: State<Boolean>,
@@ -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<Boolean>,
     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<Boolean>,
     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<BaseMediaContent>(
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<GalleryThumb>,
+    modifier: Modifier = Modifier,
+    parentBackgroundColor: MutableState<Color>? = 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<Color>? = 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<Color>? = 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<GalleryThumb>()
+                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 @@
     <string name="share_or_save">Share or Save</string>
     <string name="copy_url_to_clipboard">Copy URL to clipboard</string>
     <string name="copy_the_note_id_to_the_clipboard">Copy Note ID to clipboard</string>
+    <string name="add_media_to_gallery">Add Media to Gallery</string>
 
     <string name="created_at">Created at</string>
     <string name="rules">Rules</string>
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<Array<String>>,
+    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<Array<String>>,
+            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<Array<String>>,
+            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<String>? = null,
+            videos: List<String>? = null,
+            audios: List<String>? = null,
+            privEvents: List<String>? = null,
+            privUsers: List<String>? = null,
+            privAddresses: List<ATag>? = null,
+            signer: NostrSigner,
+            createdAt: Long = TimeUtils.now(),
+            onReady: (GalleryListEvent) -> Unit,
+        ) {
+            val tags = mutableListOf<Array<String>>()
+            tags.add(arrayOf("d", name))
+
+            images?.forEach { tags.add(arrayOf("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)
+            }
+        }
+    }
+}