feat(media): wire profile gallery tab and lightbox

- Add Notes/Gallery PrimaryTabRow to UserProfileScreen
- Subscribe to PictureEvent (kind 20) for gallery grid
- Wire GalleryTab composable with image click callbacks
- Wire LightboxOverlay for full-screen image viewing
- Pass onImageClick to FeedNoteCard in notes tab

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
nrobi144
2026-03-16 12:25:59 +02:00
parent 7773844d2f
commit adecc35721
@@ -49,12 +49,15 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
@@ -79,12 +82,16 @@ import com.vitorpamplona.amethyst.desktop.subscriptions.SubscriptionConfig
import com.vitorpamplona.amethyst.desktop.subscriptions.createContactListSubscription import com.vitorpamplona.amethyst.desktop.subscriptions.createContactListSubscription
import com.vitorpamplona.amethyst.desktop.subscriptions.createMetadataSubscription import com.vitorpamplona.amethyst.desktop.subscriptions.createMetadataSubscription
import com.vitorpamplona.amethyst.desktop.subscriptions.createUserPostsSubscription import com.vitorpamplona.amethyst.desktop.subscriptions.createUserPostsSubscription
import com.vitorpamplona.amethyst.desktop.subscriptions.generateSubId
import com.vitorpamplona.amethyst.desktop.subscriptions.rememberSubscription import com.vitorpamplona.amethyst.desktop.subscriptions.rememberSubscription
import com.vitorpamplona.amethyst.desktop.ui.media.LightboxOverlay
import com.vitorpamplona.amethyst.desktop.ui.profile.GalleryTab
import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArrayOrNull import com.vitorpamplona.quartz.nip01Core.core.hexToByteArrayOrNull
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip19Bech32.toNpub import com.vitorpamplona.quartz.nip19Bech32.toNpub
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -141,6 +148,11 @@ fun UserProfileScreen(
var postsError by remember { mutableStateOf<String?>(null) } var postsError by remember { mutableStateOf<String?>(null) }
var retryTrigger by remember { mutableStateOf(0) } var retryTrigger by remember { mutableStateOf(0) }
// Tab and gallery state
var selectedTab by remember { mutableStateOf(0) }
var lightboxState by remember { mutableStateOf<Pair<List<String>, Int>?>(null) }
val pictureEvents = remember { mutableStateListOf<PictureEvent>() }
// Follow state // Follow state
val followState = val followState =
remember(account) { remember(account) {
@@ -300,6 +312,33 @@ fun UserProfileScreen(
} }
} }
// Subscribe to picture events (kind 20) for gallery tab
rememberSubscription(connectedRelays, pubKeyHex, retryTrigger, relayManager = relayManager) {
if (connectedRelays.isNotEmpty()) {
pictureEvents.clear()
SubscriptionConfig(
subId = generateSubId("pics-${pubKeyHex.take(8)}"),
filters =
listOf(
FilterBuilders.byAuthors(
authors = listOf(pubKeyHex),
kinds = listOf(PictureEvent.KIND),
limit = 100,
),
),
relays = connectedRelays,
onEvent = { event, _, _, _ ->
if (event is PictureEvent && pictureEvents.none { it.id == event.id }) {
pictureEvents.add(event)
}
},
onEose = { _, _ -> },
)
} else {
null
}
}
Column(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.fillMaxSize()) {
// Broadcast banner for profile updates // Broadcast banner for profile updates
ProfileBroadcastBanner( ProfileBroadcastBanner(
@@ -550,16 +589,22 @@ fun UserProfileScreen(
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
// User's posts // Notes / Gallery tabs
Text( PrimaryTabRow(selectedTabIndex = selectedTab) {
"Posts", Tab(selected = selectedTab == 0, onClick = { selectedTab = 0 }) {
style = MaterialTheme.typography.titleMedium, Text("Notes", modifier = Modifier.padding(12.dp))
modifier = Modifier.padding(bottom = 8.dp), }
) Tab(selected = selectedTab == 1, onClick = { selectedTab = 1 }) {
Text("Gallery", modifier = Modifier.padding(12.dp))
}
}
Spacer(Modifier.height(8.dp))
when (selectedTab) {
0 -> {
when { when {
postsError != null -> { postsError != null -> {
// Error state with retry
Box( Box(
modifier = Modifier.fillMaxWidth().padding(32.dp), modifier = Modifier.fillMaxWidth().padding(32.dp),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
@@ -585,7 +630,6 @@ fun UserProfileScreen(
} }
postsLoading -> { postsLoading -> {
// Loading state
Box( Box(
modifier = Modifier.fillMaxWidth().padding(32.dp), modifier = Modifier.fillMaxWidth().padding(32.dp),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
@@ -603,7 +647,6 @@ fun UserProfileScreen(
} }
events.isEmpty() -> { events.isEmpty() -> {
// Empty state (loaded but no posts)
Box( Box(
modifier = Modifier.fillMaxWidth().padding(32.dp), modifier = Modifier.fillMaxWidth().padding(32.dp),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
@@ -617,7 +660,6 @@ fun UserProfileScreen(
} }
else -> { else -> {
// Posts loaded successfully
LazyColumn( LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
@@ -631,12 +673,33 @@ fun UserProfileScreen(
onReply = onCompose, onReply = onCompose,
onZapFeedback = onZapFeedback, onZapFeedback = onZapFeedback,
onNavigateToProfile = onNavigateToProfile, onNavigateToProfile = onNavigateToProfile,
onImageClick = { urls, index ->
lightboxState = urls to index
},
) )
} }
} }
} }
} }
} }
1 -> {
GalleryTab(
pictureEvents = pictureEvents,
onImageClick = { urls, index -> lightboxState = urls to index },
)
}
}
}
}
// Lightbox overlay
lightboxState?.let { (urls, index) ->
LightboxOverlay(
urls = urls,
initialIndex = index,
onDismiss = { lightboxState = null },
)
} }
// Edit Profile Dialog // Edit Profile Dialog