Compare commits

...

3 Commits

Author SHA1 Message Date
davotoula
5fb01c1e46 navigateBack() and navigateTo() APIs became suspend functions in recent versions, so they must be called from a coroutine scope 2026-03-15 13:59:24 +01:00
davotoula
c3378efffb spotlessApply 2026-03-15 13:55:37 +01:00
Claude
0cbd8005d9 feat: add responsive design for all screens on tablets and foldables
Implements Material Design 3 adaptive layout guidelines across all 40+
screens in the Android app for better tablet and foldable support.

Phase 1 - Adaptive Navigation Shell:
- Add material3-adaptive dependencies (adaptive, adaptive-layout,
  adaptive-navigation, navigation-suite)
- Create AdaptiveScaffold with NavigationSuiteScaffold that auto-switches
  between bottom bar (compact), navigation rail (medium), and permanent
  navigation drawer (expanded)
- Expose WindowSizeClass via LocalWindowSizeClass CompositionLocal
- Conditionally hide bottom bars and drawer hamburger icon on non-compact
- Replace orientation-based drawer logic with WindowSizeClass checks

Phase 2 - Feed Max-Width (12 screens):
- Create MaxWidthContainer composable (640dp default)
- Apply to Home, Video, Discover, Notification, Search, Hashtag, GeoHash,
  RelayFeed, Community, FollowPackFeed, DvmContentDiscovery, Thread screens

Phase 3 - List-Detail Layouts:
- Create AdaptiveSettingsScreen with NavigableListDetailPaneScaffold for
  two-pane settings on tablets (list left, detail right)
- Extract AllSettingsContent for reuse in list-detail layout
- Add MaxWidthContainer to Lists and Bookmark Groups screens

Phase 4 - Form/Edit Max-Width (11 screens, 560dp):
- Apply to ShortNotePost, NewProduct, GenericCommentPost, VoiceReply,
  NewUserMetadata, AllRelayList, AllMediaServers, NIP47Setup, ShowQR

Phase 5 - Chat Screen Max-Width (4 screens, 720dp):
- Apply to Chatroom, PublicChatChannel, LiveActivityChannel, EphemeralChat

Phase 6 - Profile Expanded Layout:
- Add side-by-side layout on expanded screens: profile header in 320dp
  left column, tabs/feed in right column

https://claude.ai/code/session_01T3Urq7Zg5r3tZ3JvndFsvV
2026-03-13 12:53:24 +00:00
40 changed files with 1508 additions and 925 deletions

View File

@@ -257,6 +257,10 @@ dependencies {
// Adaptive Layout / Two Pane
implementation libs.androidx.material3.windowSize
implementation libs.accompanist.adaptive
implementation libs.androidx.material3.adaptive
implementation libs.androidx.material3.adaptive.layout
implementation libs.androidx.material3.adaptive.navigation
implementation libs.androidx.material3.adaptive.navigation.suite
// Lifecycle
implementation libs.androidx.lifecycle.runtime.ktx

View File

@@ -58,6 +58,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectSingleFromGallery
import com.vitorpamplona.amethyst.ui.layouts.MaxWidthContainer
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -101,241 +102,243 @@ fun NewUserMetadataScreen(
)
},
) { pad ->
Column(
modifier =
Modifier
.padding(
start = 10.dp,
end = 10.dp,
top = pad.calculateTopPadding(),
bottom = pad.calculateBottomPadding(),
).consumeWindowInsets(pad)
.imePadding(),
) {
MaxWidthContainer(maxWidth = 560.dp) {
Column(
modifier = Modifier.padding(10.dp).verticalScroll(rememberScrollState()),
modifier =
Modifier
.padding(
start = 10.dp,
end = 10.dp,
top = pad.calculateTopPadding(),
bottom = pad.calculateBottomPadding(),
).consumeWindowInsets(pad)
.imePadding(),
) {
// -- Profile (always visible) --
OutlinedTextField(
label = { Text(text = stringRes(R.string.username)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.name.value,
onValueChange = { postViewModel.name.value = it },
placeholder = {
Text(
text = stringRes(R.string.my_name),
color = MaterialTheme.colorScheme.placeholderText,
)
},
prefix = { Text("@") },
keyboardOptions =
KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences,
),
singleLine = true,
)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringRes(R.string.display_name)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.displayName.value,
onValueChange = { postViewModel.displayName.value = it },
placeholder = {
Text(
text = stringRes(R.string.my_display_name),
color = MaterialTheme.colorScheme.placeholderText,
)
},
keyboardOptions =
KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences,
),
singleLine = true,
)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringRes(R.string.about_me)) },
modifier = Modifier.fillMaxWidth().height(100.dp),
value = postViewModel.about.value,
onValueChange = { postViewModel.about.value = it },
placeholder = {
Text(
text = stringRes(id = R.string.about_me),
color = MaterialTheme.colorScheme.placeholderText,
)
},
keyboardOptions =
KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences,
),
maxLines = 10,
)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringRes(R.string.avatar_url)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.picture.value,
onValueChange = { postViewModel.picture.value = it },
placeholder = {
Text(
text = "https://mywebsite.com/me.jpg",
color = MaterialTheme.colorScheme.placeholderText,
)
},
leadingIcon = {
SelectSingleFromGallery(
isUploading = postViewModel.isUploadingImageForPicture,
tint = MaterialTheme.colorScheme.placeholderText,
modifier = Modifier.padding(start = 5.dp),
) {
postViewModel.uploadForPicture(it, context, onError = accountViewModel.toastManager::toast)
}
},
singleLine = true,
)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringRes(R.string.banner_url)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.banner.value,
onValueChange = { postViewModel.banner.value = it },
placeholder = {
Text(
text = "https://mywebsite.com/mybanner.jpg",
color = MaterialTheme.colorScheme.placeholderText,
)
},
leadingIcon = {
SelectSingleFromGallery(
isUploading = postViewModel.isUploadingImageForBanner,
tint = MaterialTheme.colorScheme.placeholderText,
modifier = Modifier.padding(start = 5.dp),
) {
postViewModel.uploadForBanner(it, context, onError = accountViewModel.toastManager::toast)
}
},
singleLine = true,
)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringRes(R.string.lightning_address)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.lnAddress.value,
onValueChange = { postViewModel.lnAddress.value = it },
placeholder = {
Text(
text = "me@mylightningnode.com",
color = MaterialTheme.colorScheme.placeholderText,
)
},
singleLine = true,
)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringRes(R.string.nip_05) + " (NIP-05)") },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.nip05.value,
onValueChange = { postViewModel.nip05.value = it },
placeholder = {
Text(
text = "_@mywebsite.com",
color = MaterialTheme.colorScheme.placeholderText,
)
},
singleLine = true,
)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringRes(R.string.website_url)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.website.value,
onValueChange = { postViewModel.website.value = it },
placeholder = {
Text(
text = "https://mywebsite.com",
color = MaterialTheme.colorScheme.placeholderText,
)
},
singleLine = true,
)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringRes(R.string.pronouns)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.pronouns.value,
onValueChange = { postViewModel.pronouns.value = it },
placeholder = {
Text(
text = "they/them, ...",
color = MaterialTheme.colorScheme.placeholderText,
)
},
singleLine = true,
)
// -- Social Proofs --
ExpandableSection(
title = stringRes(R.string.social_proof),
expanded = socialExpanded,
Column(
modifier = Modifier.padding(10.dp).verticalScroll(rememberScrollState()),
) {
// -- Profile (always visible) --
OutlinedTextField(
label = { Text(text = stringRes(R.string.twitter)) },
label = { Text(text = stringRes(R.string.username)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.twitter.value,
onValueChange = { postViewModel.twitter.value = it },
value = postViewModel.name.value,
onValueChange = { postViewModel.name.value = it },
placeholder = {
Text(
text = stringRes(R.string.twitter_proof_url_template),
text = stringRes(R.string.my_name),
color = MaterialTheme.colorScheme.placeholderText,
)
},
prefix = { Text("@") },
keyboardOptions =
KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences,
),
singleLine = true,
)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringRes(R.string.mastodon)) },
label = { Text(text = stringRes(R.string.display_name)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.mastodon.value,
onValueChange = { postViewModel.mastodon.value = it },
value = postViewModel.displayName.value,
onValueChange = { postViewModel.displayName.value = it },
placeholder = {
Text(
text = stringRes(R.string.mastodon_proof_url_template),
text = stringRes(R.string.my_display_name),
color = MaterialTheme.colorScheme.placeholderText,
)
},
keyboardOptions =
KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences,
),
singleLine = true,
)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringRes(R.string.github)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.github.value,
onValueChange = { postViewModel.github.value = it },
label = { Text(text = stringRes(R.string.about_me)) },
modifier = Modifier.fillMaxWidth().height(100.dp),
value = postViewModel.about.value,
onValueChange = { postViewModel.about.value = it },
placeholder = {
Text(
text = stringRes(R.string.github_proof_url_template),
text = stringRes(id = R.string.about_me),
color = MaterialTheme.colorScheme.placeholderText,
)
},
keyboardOptions =
KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences,
),
maxLines = 10,
)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringRes(R.string.avatar_url)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.picture.value,
onValueChange = { postViewModel.picture.value = it },
placeholder = {
Text(
text = "https://mywebsite.com/me.jpg",
color = MaterialTheme.colorScheme.placeholderText,
)
},
leadingIcon = {
SelectSingleFromGallery(
isUploading = postViewModel.isUploadingImageForPicture,
tint = MaterialTheme.colorScheme.placeholderText,
modifier = Modifier.padding(start = 5.dp),
) {
postViewModel.uploadForPicture(it, context, onError = accountViewModel.toastManager::toast)
}
},
singleLine = true,
)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringRes(R.string.banner_url)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.banner.value,
onValueChange = { postViewModel.banner.value = it },
placeholder = {
Text(
text = "https://mywebsite.com/mybanner.jpg",
color = MaterialTheme.colorScheme.placeholderText,
)
},
leadingIcon = {
SelectSingleFromGallery(
isUploading = postViewModel.isUploadingImageForBanner,
tint = MaterialTheme.colorScheme.placeholderText,
modifier = Modifier.padding(start = 5.dp),
) {
postViewModel.uploadForBanner(it, context, onError = accountViewModel.toastManager::toast)
}
},
singleLine = true,
)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringRes(R.string.lightning_address)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.lnAddress.value,
onValueChange = { postViewModel.lnAddress.value = it },
placeholder = {
Text(
text = "me@mylightningnode.com",
color = MaterialTheme.colorScheme.placeholderText,
)
},
singleLine = true,
)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringRes(R.string.nip_05) + " (NIP-05)") },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.nip05.value,
onValueChange = { postViewModel.nip05.value = it },
placeholder = {
Text(
text = "_@mywebsite.com",
color = MaterialTheme.colorScheme.placeholderText,
)
},
singleLine = true,
)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringRes(R.string.website_url)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.website.value,
onValueChange = { postViewModel.website.value = it },
placeholder = {
Text(
text = "https://mywebsite.com",
color = MaterialTheme.colorScheme.placeholderText,
)
},
singleLine = true,
)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringRes(R.string.pronouns)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.pronouns.value,
onValueChange = { postViewModel.pronouns.value = it },
placeholder = {
Text(
text = "they/them, ...",
color = MaterialTheme.colorScheme.placeholderText,
)
},
singleLine = true,
)
// -- Social Proofs --
ExpandableSection(
title = stringRes(R.string.social_proof),
expanded = socialExpanded,
) {
OutlinedTextField(
label = { Text(text = stringRes(R.string.twitter)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.twitter.value,
onValueChange = { postViewModel.twitter.value = it },
placeholder = {
Text(
text = stringRes(R.string.twitter_proof_url_template),
color = MaterialTheme.colorScheme.placeholderText,
)
},
)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringRes(R.string.mastodon)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.mastodon.value,
onValueChange = { postViewModel.mastodon.value = it },
placeholder = {
Text(
text = stringRes(R.string.mastodon_proof_url_template),
color = MaterialTheme.colorScheme.placeholderText,
)
},
)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringRes(R.string.github)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.github.value,
onValueChange = { postViewModel.github.value = it },
placeholder = {
Text(
text = stringRes(R.string.github_proof_url_template),
color = MaterialTheme.colorScheme.placeholderText,
)
},
)
}
}
}
}

View File

@@ -38,6 +38,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.layouts.MaxWidthContainer
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -83,29 +84,31 @@ fun MediaServersScaffold(
)
},
) { padding ->
Column(
modifier =
Modifier
.fillMaxSize()
.padding(
start = 16.dp,
top = padding.calculateTopPadding(),
end = 16.dp,
bottom = padding.calculateBottomPadding(),
).consumeWindowInsets(padding)
.imePadding(),
verticalArrangement = Arrangement.spacedBy(10.dp, alignment = Alignment.Top),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringRes(id = R.string.set_preferred_media_servers),
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 10.dp),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.grayText,
)
MaxWidthContainer(maxWidth = 560.dp) {
Column(
modifier =
Modifier
.fillMaxSize()
.padding(
start = 16.dp,
top = padding.calculateTopPadding(),
end = 16.dp,
bottom = padding.calculateBottomPadding(),
).consumeWindowInsets(padding)
.imePadding(),
verticalArrangement = Arrangement.spacedBy(10.dp, alignment = Alignment.Top),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringRes(id = R.string.set_preferred_media_servers),
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 10.dp),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.grayText,
)
AllMediaBody(blossomServersViewModel)
AllMediaBody(blossomServersViewModel)
}
}
}
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright (c) 2025 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.layouts
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.window.core.layout.WindowWidthSizeClass
import com.vitorpamplona.amethyst.ui.navigation.bottombars.BottomBarRoute
import com.vitorpamplona.amethyst.ui.navigation.bottombars.bottomNavigationItems
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.painterRes
import com.vitorpamplona.amethyst.ui.stringRes
val LocalWindowSizeClass = compositionLocalOf { WindowWidthSizeClass.COMPACT }
@Composable
fun isCompactWindow(): Boolean = LocalWindowSizeClass.current == WindowWidthSizeClass.COMPACT
@Composable
fun AdaptiveScaffold(
selectedRoute: Route?,
onNavigate: (Route) -> Unit,
content: @Composable () -> Unit,
) {
val adaptiveInfo = currentWindowAdaptiveInfo()
val windowSizeClass = adaptiveInfo.windowSizeClass.windowWidthSizeClass
CompositionLocalProvider(LocalWindowSizeClass provides windowSizeClass) {
NavigationSuiteScaffold(
navigationSuiteItems = {
bottomNavigationItems.forEach { navItem ->
val selected = navItem.route == selectedRoute
item(
selected = selected,
onClick = { onNavigate(navItem.route) },
icon = {
AdaptiveNavIcon(navItem, selected)
},
)
}
},
) {
content()
}
}
}
@Composable
private fun AdaptiveNavIcon(
route: BottomBarRoute,
selected: Boolean,
) {
Icon(
painter = painterRes(resourceId = route.icon, 0),
contentDescription = stringRes(route.contentDescriptor),
modifier = route.iconSize,
tint = if (selected) MaterialTheme.colorScheme.primary else Color.Unspecified,
)
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright (c) 2025 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.layouts
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.widthIn
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@Composable
fun MaxWidthContainer(
maxWidth: Dp = 640.dp,
content: @Composable () -> Unit,
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.TopCenter,
) {
Box(modifier = Modifier.widthIn(max = maxWidth).fillMaxHeight()) {
content()
}
}
}

View File

@@ -37,8 +37,11 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.core.net.toUri
import androidx.core.util.Consumer
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.crashreports.DisplayCrashMessages
@@ -48,6 +51,7 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.AllMediaServersScreen
import com.vitorpamplona.amethyst.ui.broadcast.DisplayBroadcastProgress
import com.vitorpamplona.amethyst.ui.components.getActivity
import com.vitorpamplona.amethyst.ui.components.toasts.DisplayErrorMessages
import com.vitorpamplona.amethyst.ui.layouts.AdaptiveScaffold
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
import com.vitorpamplona.amethyst.ui.navigation.navs.rememberNav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
@@ -109,7 +113,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relay.RelayFeedScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.AllRelayListScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.RelayInformationScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.search.SearchScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.AllSettingsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.AdaptiveSettingsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NIP47SetupScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NamecoinSettingsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.ReactionsSettingsScreen
@@ -137,201 +141,212 @@ fun AppNavigation(
) {
val nav = rememberNav()
AccountSwitcherAndLeftDrawerLayout(accountViewModel, accountSessionManager, nav) {
NavHost(
navController = nav.controller,
startDestination = Route.Home,
enterTransition = { fadeIn(animationSpec = tween(200)) },
exitTransition = { fadeOut(animationSpec = tween(200)) },
) {
composable<Route.Home> { HomeScreen(accountViewModel, nav) }
composable<Route.Message> { MessagesScreen(accountViewModel, nav) }
composable<Route.Video> { VideoScreen(accountViewModel, nav) }
composable<Route.Discover> { DiscoverScreen(accountViewModel, nav) }
composable<Route.Notification> { NotificationScreen(accountViewModel, nav) }
composable<Route.Chess> { ChessLobbyScreen(accountViewModel, nav) }
val currentBackStackEntry by nav.controller.currentBackStackEntryAsState()
val selectedRoute =
remember(currentBackStackEntry) {
currentBackStackEntry?.toBaseRoute()
}
composableFromEnd<Route.Lists> { ListOfPeopleListsScreen(accountViewModel, nav) }
composableFromEndArgs<Route.MyPeopleListView> { PeopleListScreen(it.dTag, accountViewModel, nav) }
composableFromEndArgs<Route.MyFollowPackView> { FollowPackScreen(it.dTag, accountViewModel, nav) }
composableFromBottomArgs<Route.PeopleListManagement> { FollowListAndPackAndUserScreen(it.userToAdd, accountViewModel, nav) }
AdaptiveScaffold(
selectedRoute = selectedRoute,
onNavigate = { route -> nav.newStack(route) },
) {
AccountSwitcherAndLeftDrawerLayout(accountViewModel, accountSessionManager, nav) {
NavHost(
navController = nav.controller,
startDestination = Route.Home,
enterTransition = { fadeIn(animationSpec = tween(200)) },
exitTransition = { fadeOut(animationSpec = tween(200)) },
) {
composable<Route.Home> { HomeScreen(accountViewModel, nav) }
composable<Route.Message> { MessagesScreen(accountViewModel, nav) }
composable<Route.Video> { VideoScreen(accountViewModel, nav) }
composable<Route.Discover> { DiscoverScreen(accountViewModel, nav) }
composable<Route.Notification> { NotificationScreen(accountViewModel, nav) }
composable<Route.Chess> { ChessLobbyScreen(accountViewModel, nav) }
composableFromBottomArgs<Route.PeopleListMetadataEdit> { PeopleListMetadataScreen(it.dTag, accountViewModel, nav) }
composableFromBottomArgs<Route.FollowPackMetadataEdit> { FollowPackMetadataScreen(it.dTag, accountViewModel, nav) }
composableFromEnd<Route.Lists> { ListOfPeopleListsScreen(accountViewModel, nav) }
composableFromEndArgs<Route.MyPeopleListView> { PeopleListScreen(it.dTag, accountViewModel, nav) }
composableFromEndArgs<Route.MyFollowPackView> { FollowPackScreen(it.dTag, accountViewModel, nav) }
composableFromBottomArgs<Route.PeopleListManagement> { FollowListAndPackAndUserScreen(it.userToAdd, accountViewModel, nav) }
composableFromEnd<Route.BookmarkGroups> { ListOfBookmarkGroupsScreen(accountViewModel, nav) }
composableFromEndArgs<Route.BookmarkGroupView> { BookmarkGroupScreen(it.dTag, it.bookmarkType, accountViewModel, nav) }
composableFromBottomArgs<Route.BookmarkGroupMetadataEdit> { BookmarkGroupMetadataScreen(it.dTag, accountViewModel, nav) }
composableFromBottomArgs<Route.PostBookmarkManagement> { PostBookmarkListManagementScreen(it.postId, accountViewModel, nav) }
composableFromBottomArgs<Route.ArticleBookmarkManagement> { ArticleBookmarkListManagementScreen(Address(it.kind, it.pubKeyHex, it.dTag), accountViewModel, nav) }
composableFromBottomArgs<Route.PeopleListMetadataEdit> { PeopleListMetadataScreen(it.dTag, accountViewModel, nav) }
composableFromBottomArgs<Route.FollowPackMetadataEdit> { FollowPackMetadataScreen(it.dTag, accountViewModel, nav) }
composableFromBottomArgs<Route.QRDisplay> { ShowQRScreen(it.pubkey, accountViewModel, nav) }
composableFromEnd<Route.BookmarkGroups> { ListOfBookmarkGroupsScreen(accountViewModel, nav) }
composableFromEndArgs<Route.BookmarkGroupView> { BookmarkGroupScreen(it.dTag, it.bookmarkType, accountViewModel, nav) }
composableFromBottomArgs<Route.BookmarkGroupMetadataEdit> { BookmarkGroupMetadataScreen(it.dTag, accountViewModel, nav) }
composableFromBottomArgs<Route.PostBookmarkManagement> { PostBookmarkListManagementScreen(it.postId, accountViewModel, nav) }
composableFromBottomArgs<Route.ArticleBookmarkManagement> { ArticleBookmarkListManagementScreen(Address(it.kind, it.pubKeyHex, it.dTag), accountViewModel, nav) }
composableFromBottomArgs<Route.ManualZapSplitPayment> { PayViaIntentScreen(it.paymentId, accountViewModel, nav) }
composableFromBottomArgs<Route.QRDisplay> { ShowQRScreen(it.pubkey, accountViewModel, nav) }
composableFromBottomArgs<Route.EditProfile> { NewUserMetadataScreen(nav, accountViewModel) }
composable<Route.Search> { SearchScreen(accountViewModel, nav) }
composableFromBottomArgs<Route.ManualZapSplitPayment> { PayViaIntentScreen(it.paymentId, accountViewModel, nav) }
composableFromEnd<Route.AllSettings> { AllSettingsScreen(accountViewModel, nav) }
composableFromEnd<Route.AccountBackup> { AccountBackupScreen(accountViewModel, nav) }
composableFromEnd<Route.SecurityFilters> { SecurityFiltersScreen(accountViewModel, nav) }
composableFromEnd<Route.PrivacyOptions> { PrivacyOptionsScreen(Amethyst.instance.torPrefs.value, nav) }
composableFromEnd<Route.NamecoinSettings> { NamecoinSettingsScreen(Amethyst.instance.namecoinPrefs, nav) }
composableFromEnd<Route.Bookmarks> { BookmarkListScreen(accountViewModel, nav) }
composableFromEnd<Route.Drafts> { DraftListScreen(accountViewModel, nav) }
composableFromEnd<Route.Settings> { SettingsScreen(accountViewModel, nav) }
composableFromEnd<Route.UserSettings> { UserSettingsScreen(accountViewModel, nav) }
composableFromEnd<Route.ReactionsSettings> { ReactionsSettingsScreen(accountViewModel, nav) }
composableFromEnd<Route.ImportFollowsSelectUser> { ImportFollowListSelectUserScreen(accountViewModel, nav) }
composableFromEndArgs<Route.ImportFollowsPickFollows> {
ImportFollowListPickFollowsScreen(
accountViewModel.getOrCreateAddressableNote(ContactListEvent.createAddress(it.userHex)),
accountViewModel,
nav,
)
}
composableFromBottomArgs<Route.EditProfile> { NewUserMetadataScreen(nav, accountViewModel) }
composable<Route.Search> { SearchScreen(accountViewModel, nav) }
composableFromEndArgs<Route.Nip47NWCSetup> { NIP47SetupScreen(accountViewModel, nav, it.nip47) }
composableFromEndArgs<Route.UpdateZapAmount> { UpdateZapAmountScreen(accountViewModel, nav, it.nip47) }
composableFromEndArgs<Route.EditRelays> { AllRelayListScreen(accountViewModel, nav) }
composableFromEndArgs<Route.EditMediaServers> { AllMediaServersScreen(accountViewModel, nav) }
composableFromEndArgs<Route.UpdateReactionType> { UpdateReactionTypeScreen(accountViewModel, nav) }
composableFromEnd<Route.AllSettings> { AdaptiveSettingsScreen(accountViewModel, nav) }
composableFromEnd<Route.AccountBackup> { AccountBackupScreen(accountViewModel, nav) }
composableFromEnd<Route.SecurityFilters> { SecurityFiltersScreen(accountViewModel, nav) }
composableFromEnd<Route.PrivacyOptions> { PrivacyOptionsScreen(Amethyst.instance.torPrefs.value, nav) }
composableFromEnd<Route.NamecoinSettings> { NamecoinSettingsScreen(Amethyst.instance.namecoinPrefs, nav) }
composableFromEnd<Route.Bookmarks> { BookmarkListScreen(accountViewModel, nav) }
composableFromEnd<Route.Drafts> { DraftListScreen(accountViewModel, nav) }
composableFromEnd<Route.Settings> { SettingsScreen(accountViewModel, nav) }
composableFromEnd<Route.UserSettings> { UserSettingsScreen(accountViewModel, nav) }
composableFromEnd<Route.ReactionsSettings> { ReactionsSettingsScreen(accountViewModel, nav) }
composableFromEnd<Route.ImportFollowsSelectUser> { ImportFollowListSelectUserScreen(accountViewModel, nav) }
composableFromEndArgs<Route.ImportFollowsPickFollows> {
ImportFollowListPickFollowsScreen(
accountViewModel.getOrCreateAddressableNote(ContactListEvent.createAddress(it.userHex)),
accountViewModel,
nav,
)
}
composableFromEndArgs<Route.ContentDiscovery> { DvmContentDiscoveryScreen(it.id, accountViewModel, nav) }
composableFromEndArgs<Route.Profile> { ProfileScreen(it.id, accountViewModel, nav) }
composableFromEndArgs<Route.Note> { ThreadScreen(it.id, accountViewModel, nav) }
composableFromEndArgs<Route.Hashtag> { HashtagScreen(it, accountViewModel, nav) }
composableFromEndArgs<Route.Geohash> { GeoHashScreen(it, accountViewModel, nav) }
composableFromEndArgs<Route.RelayFeed> { RelayFeedScreen(it, accountViewModel, nav) }
composableFromEndArgs<Route.ChessGame> { ChessGameScreen(it.gameId, accountViewModel, nav) }
composableFromEndArgs<Route.RelayInfo> { RelayInformationScreen(it.url, accountViewModel, nav) }
composableFromEndArgs<Route.Community> { CommunityScreen(Address(it.kind, it.pubKeyHex, it.dTag), accountViewModel, nav) }
composableFromEndArgs<Route.FollowPack> { FollowPackFeedScreen(Address(it.kind, it.pubKeyHex, it.dTag), accountViewModel, nav) }
composableFromEndArgs<Route.Nip47NWCSetup> { NIP47SetupScreen(accountViewModel, nav, it.nip47) }
composableFromEndArgs<Route.UpdateZapAmount> { UpdateZapAmountScreen(accountViewModel, nav, it.nip47) }
composableFromEndArgs<Route.EditRelays> { AllRelayListScreen(accountViewModel, nav) }
composableFromEndArgs<Route.EditMediaServers> { AllMediaServersScreen(accountViewModel, nav) }
composableFromEndArgs<Route.UpdateReactionType> { UpdateReactionTypeScreen(accountViewModel, nav) }
composableFromEndArgs<Route.Room> { ChatroomScreen(it.toKey(), it.message, it.replyId, it.draftId, it.expiresDays, accountViewModel, nav) }
composableFromEndArgs<Route.RoomByAuthor> { ChatroomByAuthorScreen(it.id, null, accountViewModel, nav) }
composableFromEndArgs<Route.ContentDiscovery> { DvmContentDiscoveryScreen(it.id, accountViewModel, nav) }
composableFromEndArgs<Route.Profile> { ProfileScreen(it.id, accountViewModel, nav) }
composableFromEndArgs<Route.Note> { ThreadScreen(it.id, accountViewModel, nav) }
composableFromEndArgs<Route.Hashtag> { HashtagScreen(it, accountViewModel, nav) }
composableFromEndArgs<Route.Geohash> { GeoHashScreen(it, accountViewModel, nav) }
composableFromEndArgs<Route.RelayFeed> { RelayFeedScreen(it, accountViewModel, nav) }
composableFromEndArgs<Route.ChessGame> { ChessGameScreen(it.gameId, accountViewModel, nav) }
composableFromEndArgs<Route.RelayInfo> { RelayInformationScreen(it.url, accountViewModel, nav) }
composableFromEndArgs<Route.Community> { CommunityScreen(Address(it.kind, it.pubKeyHex, it.dTag), accountViewModel, nav) }
composableFromEndArgs<Route.FollowPack> { FollowPackFeedScreen(Address(it.kind, it.pubKeyHex, it.dTag), accountViewModel, nav) }
composableFromEndArgs<Route.PublicChatChannel> {
PublicChatChannelScreen(
it.id,
it.draftId?.let { hex -> accountViewModel.getNoteIfExists(hex) },
it.replyTo?.let { hex -> accountViewModel.checkGetOrCreateNote(hex) },
accountViewModel,
nav,
)
}
composableFromEndArgs<Route.Room> { ChatroomScreen(it.toKey(), it.message, it.replyId, it.draftId, it.expiresDays, accountViewModel, nav) }
composableFromEndArgs<Route.RoomByAuthor> { ChatroomByAuthorScreen(it.id, null, accountViewModel, nav) }
composableFromEndArgs<Route.LiveActivityChannel> {
LiveActivityChannelScreen(
Address(it.kind, it.pubKeyHex, it.dTag),
draft = it.draftId?.let { hex -> accountViewModel.getNoteIfExists(hex) },
replyTo = it.replyTo?.let { hex -> accountViewModel.checkGetOrCreateNote(hex) },
accountViewModel,
nav,
)
}
composableFromEndArgs<Route.PublicChatChannel> {
PublicChatChannelScreen(
it.id,
it.draftId?.let { hex -> accountViewModel.getNoteIfExists(hex) },
it.replyTo?.let { hex -> accountViewModel.checkGetOrCreateNote(hex) },
accountViewModel,
nav,
)
}
composableFromEndArgs<Route.EphemeralChat> {
RelayUrlNormalizer.normalizeOrNull(it.relayUrl)?.let { relay ->
EphemeralChatScreen(
channelId = RoomId(it.id, relay),
composableFromEndArgs<Route.LiveActivityChannel> {
LiveActivityChannelScreen(
Address(it.kind, it.pubKeyHex, it.dTag),
draft = it.draftId?.let { hex -> accountViewModel.getNoteIfExists(hex) },
replyTo = it.replyTo?.let { hex -> accountViewModel.checkGetOrCreateNote(hex) },
accountViewModel,
nav,
)
}
composableFromEndArgs<Route.EphemeralChat> {
RelayUrlNormalizer.normalizeOrNull(it.relayUrl)?.let { relay ->
EphemeralChatScreen(
channelId = RoomId(it.id, relay),
draft = it.draftId?.let { hex -> accountViewModel.getNoteIfExists(hex) },
replyTo = it.replyTo?.let { hex -> accountViewModel.checkGetOrCreateNote(hex) },
accountViewModel = accountViewModel,
nav = nav,
)
}
}
composableFromBottomArgs<Route.ChannelMetadataEdit> { ChannelMetadataScreen(it.id, accountViewModel, nav) }
composableFromBottomArgs<Route.NewEphemeralChat> { NewEphemeralChatScreen(accountViewModel, nav) }
composableFromBottomArgs<Route.NewGroupDM> { NewGroupDMScreen(it.message, it.attachment, accountViewModel, nav) }
composableArgs<Route.EventRedirect> { LoadRedirectScreen(it.id, accountViewModel, nav) }
composableFromBottomArgs<Route.GeoPost> {
GeoHashPostScreen(
geohash = it.geohash,
message = it.message,
attachment = it.attachment?.ifBlank { null }?.toUri(),
reply = it.replyTo?.let { hex -> accountViewModel.getNoteIfExists(hex) },
quote = it.quote?.let { hex -> accountViewModel.getNoteIfExists(hex) },
draft = it.draft?.let { hex -> accountViewModel.getNoteIfExists(hex) },
accountViewModel,
nav,
)
}
composableFromBottomArgs<Route.NewPublicMessage> {
NewPublicMessageScreen(
to = it.toKey(),
reply = it.replyId?.let { hex -> accountViewModel.getNoteIfExists(hex) },
draft = it.draftId?.let { hex -> accountViewModel.getNoteIfExists(hex) },
accountViewModel = accountViewModel,
nav = nav,
)
}
}
composableFromBottomArgs<Route.ChannelMetadataEdit> { ChannelMetadataScreen(it.id, accountViewModel, nav) }
composableFromBottomArgs<Route.NewEphemeralChat> { NewEphemeralChatScreen(accountViewModel, nav) }
composableFromBottomArgs<Route.NewGroupDM> { NewGroupDMScreen(it.message, it.attachment, accountViewModel, nav) }
composableFromBottomArgs<Route.HashtagPost> {
HashtagPostScreen(
hashtag = it.hashtag,
message = it.message,
attachment = it.attachment?.ifBlank { null }?.toUri(),
reply = it.replyTo?.let { hex -> accountViewModel.getNoteIfExists(hex) },
quote = it.quote?.let { hex -> accountViewModel.getNoteIfExists(hex) },
draft = it.draft?.let { hex -> accountViewModel.getNoteIfExists(hex) },
accountViewModel,
nav,
)
}
composableArgs<Route.EventRedirect> { LoadRedirectScreen(it.id, accountViewModel, nav) }
composableFromBottomArgs<Route.GenericCommentPost> {
ReplyCommentPostScreen(
reply = it.replyTo?.let { hex -> accountViewModel.getNoteIfExists(hex) },
message = it.message,
attachment = it.attachment?.ifBlank { null }?.toUri(),
quote = it.quote?.let { hex -> accountViewModel.getNoteIfExists(hex) },
draft = it.draft?.let { hex -> accountViewModel.getNoteIfExists(hex) },
accountViewModel,
nav,
)
}
composableFromBottomArgs<Route.GeoPost> {
GeoHashPostScreen(
geohash = it.geohash,
message = it.message,
attachment = it.attachment?.ifBlank { null }?.toUri(),
reply = it.replyTo?.let { hex -> accountViewModel.getNoteIfExists(hex) },
quote = it.quote?.let { hex -> accountViewModel.getNoteIfExists(hex) },
draft = it.draft?.let { hex -> accountViewModel.getNoteIfExists(hex) },
accountViewModel,
nav,
)
}
composableFromBottomArgs<Route.NewProduct> {
NewProductScreen(
message = it.message,
attachment = it.attachment?.ifBlank { null }?.toUri(),
quote = it.quote?.let { hex -> accountViewModel.getNoteIfExists(hex) },
draft = it.draft?.let { hex -> accountViewModel.getNoteIfExists(hex) },
accountViewModel,
nav,
)
}
composableFromBottomArgs<Route.NewPublicMessage> {
NewPublicMessageScreen(
to = it.toKey(),
reply = it.replyId?.let { hex -> accountViewModel.getNoteIfExists(hex) },
draft = it.draftId?.let { hex -> accountViewModel.getNoteIfExists(hex) },
accountViewModel = accountViewModel,
nav = nav,
)
}
composableFromBottomArgs<Route.NewShortNote> {
ShortNotePostScreen(
message = it.message,
attachment = it.attachment?.ifBlank { null }?.toUri(),
baseReplyTo = it.baseReplyTo?.let { hex -> accountViewModel.getNoteIfExists(hex) },
quote = it.quote?.let { hex -> accountViewModel.getNoteIfExists(hex) },
fork = it.fork?.let { hex -> accountViewModel.getNoteIfExists(hex) },
version = it.version?.let { hex -> accountViewModel.getNoteIfExists(hex) },
draft = it.draft?.let { hex -> accountViewModel.getNoteIfExists(hex) },
accountViewModel = accountViewModel,
nav = nav,
)
}
composableFromBottomArgs<Route.HashtagPost> {
HashtagPostScreen(
hashtag = it.hashtag,
message = it.message,
attachment = it.attachment?.ifBlank { null }?.toUri(),
reply = it.replyTo?.let { hex -> accountViewModel.getNoteIfExists(hex) },
quote = it.quote?.let { hex -> accountViewModel.getNoteIfExists(hex) },
draft = it.draft?.let { hex -> accountViewModel.getNoteIfExists(hex) },
accountViewModel,
nav,
)
}
composableFromBottomArgs<Route.GenericCommentPost> {
ReplyCommentPostScreen(
reply = it.replyTo?.let { hex -> accountViewModel.getNoteIfExists(hex) },
message = it.message,
attachment = it.attachment?.ifBlank { null }?.toUri(),
quote = it.quote?.let { hex -> accountViewModel.getNoteIfExists(hex) },
draft = it.draft?.let { hex -> accountViewModel.getNoteIfExists(hex) },
accountViewModel,
nav,
)
}
composableFromBottomArgs<Route.NewProduct> {
NewProductScreen(
message = it.message,
attachment = it.attachment?.ifBlank { null }?.toUri(),
quote = it.quote?.let { hex -> accountViewModel.getNoteIfExists(hex) },
draft = it.draft?.let { hex -> accountViewModel.getNoteIfExists(hex) },
accountViewModel,
nav,
)
}
composableFromBottomArgs<Route.NewShortNote> {
ShortNotePostScreen(
message = it.message,
attachment = it.attachment?.ifBlank { null }?.toUri(),
baseReplyTo = it.baseReplyTo?.let { hex -> accountViewModel.getNoteIfExists(hex) },
quote = it.quote?.let { hex -> accountViewModel.getNoteIfExists(hex) },
fork = it.fork?.let { hex -> accountViewModel.getNoteIfExists(hex) },
version = it.version?.let { hex -> accountViewModel.getNoteIfExists(hex) },
draft = it.draft?.let { hex -> accountViewModel.getNoteIfExists(hex) },
accountViewModel = accountViewModel,
nav = nav,
)
}
composableFromBottomArgs<Route.VoiceReply> {
VoiceReplyScreen(
replyToNoteId = it.replyToNoteId,
recordingFilePath = it.recordingFilePath,
mimeType = it.mimeType,
duration = it.duration,
amplitudesJson = it.amplitudes,
accountViewModel = accountViewModel,
nav = nav,
)
composableFromBottomArgs<Route.VoiceReply> {
VoiceReplyScreen(
replyToNoteId = it.replyToNoteId,
recordingFilePath = it.recordingFilePath,
mimeType = it.mimeType,
duration = it.duration,
amplitudesJson = it.amplitudes,
accountViewModel = accountViewModel,
nav = nav,
)
}
}
}
}
@@ -495,6 +510,18 @@ private fun NavigateIfIntentRequested(
}
}
private fun NavBackStackEntry.toBaseRoute(): Route? {
val dest = destination
return when {
dest.hasRoute<Route.Home>() -> Route.Home
dest.hasRoute<Route.Message>() -> Route.Message
dest.hasRoute<Route.Video>() -> Route.Video
dest.hasRoute<Route.Discover>() -> Route.Discover
dest.hasRoute<Route.Notification>() -> Route.Notification
else -> null
}
}
fun URI.findParameterValue(parameterName: String): String? =
rawQuery
?.split('&')

View File

@@ -41,6 +41,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.ui.layouts.isCompactWindow
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.painterRes
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -55,6 +56,8 @@ fun AppBottomBar(
accountViewModel: AccountViewModel,
nav: (Route) -> Unit,
) {
if (!isCompactWindow()) return
val isKeyboardState by keyboardAsState()
if (isKeyboardState == KeyboardState.Closed) {
RenderBottomMenu(selectedRoute, accountViewModel, nav)

View File

@@ -34,6 +34,7 @@ import androidx.compose.ui.layout.ContentScale
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserPicture
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.layouts.isCompactWindow
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.note.SearchIcon
@@ -61,7 +62,9 @@ fun UserDrawerSearchTopBar(
}
},
navigationIcon = {
LoggedInUserPictureDrawer(accountViewModel, nav::openDrawer)
if (isCompactWindow()) {
LoggedInUserPictureDrawer(accountViewModel, nav::openDrawer)
}
},
actions = {
IconButton(onClick = { nav.nav(Route.Search) }) {

View File

@@ -53,6 +53,7 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton
import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton
import com.vitorpamplona.amethyst.ui.layouts.MaxWidthContainer
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar
import com.vitorpamplona.amethyst.ui.note.BaseUserPicture
@@ -177,11 +178,13 @@ fun GenericCommentPostScreen(
.consumeWindowInsets(pad)
.imePadding(),
) {
GenericCommentPostBody(
postViewModel,
accountViewModel,
nav,
)
MaxWidthContainer(maxWidth = 560.dp) {
GenericCommentPostBody(
postViewModel,
accountViewModel,
nav,
)
}
}
}
}

View File

@@ -20,23 +20,20 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn
import android.content.res.Configuration
import androidx.activity.compose.BackHandler
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.SheetValue
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalConfiguration
import com.vitorpamplona.amethyst.ui.layouts.isCompactWindow
import com.vitorpamplona.amethyst.ui.navigation.drawer.AccountSwitchBottomSheet
import com.vitorpamplona.amethyst.ui.navigation.drawer.DrawerContent
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
@@ -71,25 +68,19 @@ fun AccountSwitcherAndLeftDrawerLayout(
}
}
val orientation = LocalConfiguration.current.orientation
val currentDrawerState = nav.drawerState.currentValue
LaunchedEffect(key1 = orientation) {
if (
orientation == Configuration.ORIENTATION_LANDSCAPE && currentDrawerState == DrawerValue.Closed
) {
nav.drawerState.close()
}
if (isCompactWindow()) {
ModalNavigationDrawer(
drawerState = nav.drawerState,
drawerContent = {
DrawerContent(nav, openSheetFunction, accountViewModel)
BackHandler(enabled = nav.drawerState.isOpen, nav::closeDrawer)
},
content = content,
)
} else {
content()
}
ModalNavigationDrawer(
drawerState = nav.drawerState,
drawerContent = {
DrawerContent(nav, openSheetFunction, accountViewModel)
BackHandler(enabled = nav.drawerState.isOpen, nav::closeDrawer)
},
content = content,
)
// Sheet content
if (openAccountSwitcherBottomSheet) {
ModalBottomSheet(

View File

@@ -57,6 +57,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.components.ClickableBox
import com.vitorpamplona.amethyst.ui.layouts.MaxWidthContainer
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.topbars.ShorterTopAppBar
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
@@ -146,69 +147,71 @@ fun BookmarkGroupScreenView(
}
},
) { padding ->
Column(
modifier =
Modifier
.fillMaxSize()
.padding(
top = padding.calculateTopPadding(),
bottom = padding.calculateBottomPadding(),
).consumeWindowInsets(padding)
.imePadding(),
) {
when (bookmarkType) {
BookmarkType.PostBookmark -> {
RenderPostList(
bookmarkGroupViewModel,
pagerState,
accountViewModel,
movePostBookmark = { postId, isPrivate ->
accountViewModel.launchSigner {
bookmarkGroupViewModel.movePostBookmark(
groupIdentifier = bookmarkGroupViewModel.bookmarkGroupIdentifier,
postId = postId,
isCurrentlyPrivate = isPrivate,
)
}
},
deletePostBookmark = { postId, isPrivate ->
accountViewModel.launchSigner {
bookmarkGroupViewModel.removePostBookmark(
bookmarkGroupViewModel.bookmarkGroupIdentifier,
postId,
isPrivate,
)
}
},
nav,
)
}
MaxWidthContainer {
Column(
modifier =
Modifier
.fillMaxSize()
.padding(
top = padding.calculateTopPadding(),
bottom = padding.calculateBottomPadding(),
).consumeWindowInsets(padding)
.imePadding(),
) {
when (bookmarkType) {
BookmarkType.PostBookmark -> {
RenderPostList(
bookmarkGroupViewModel,
pagerState,
accountViewModel,
movePostBookmark = { postId, isPrivate ->
accountViewModel.launchSigner {
bookmarkGroupViewModel.movePostBookmark(
groupIdentifier = bookmarkGroupViewModel.bookmarkGroupIdentifier,
postId = postId,
isCurrentlyPrivate = isPrivate,
)
}
},
deletePostBookmark = { postId, isPrivate ->
accountViewModel.launchSigner {
bookmarkGroupViewModel.removePostBookmark(
bookmarkGroupViewModel.bookmarkGroupIdentifier,
postId,
isPrivate,
)
}
},
nav,
)
}
BookmarkType.ArticleBookmark -> {
RenderArticleList(
bookmarkGroupViewModel,
pagerState,
accountViewModel,
moveArticleBookmark = { articleAddress, isPrivate ->
accountViewModel.launchSigner {
bookmarkGroupViewModel.moveArticleBookmark(
groupIdentifier = bookmarkGroupViewModel.bookmarkGroupIdentifier,
articleAddress = articleAddress,
isCurrentlyPrivate = isPrivate,
)
}
},
deleteArticleBookmark = { articleAddress, isPrivate ->
accountViewModel.launchSigner {
bookmarkGroupViewModel.removeArticleBookmark(
bookmarkGroupViewModel.bookmarkGroupIdentifier,
articleAddress,
isPrivate,
)
}
},
nav,
)
BookmarkType.ArticleBookmark -> {
RenderArticleList(
bookmarkGroupViewModel,
pagerState,
accountViewModel,
moveArticleBookmark = { articleAddress, isPrivate ->
accountViewModel.launchSigner {
bookmarkGroupViewModel.moveArticleBookmark(
groupIdentifier = bookmarkGroupViewModel.bookmarkGroupIdentifier,
articleAddress = articleAddress,
isCurrentlyPrivate = isPrivate,
)
}
},
deleteArticleBookmark = { articleAddress, isPrivate ->
accountViewModel.launchSigner {
bookmarkGroupViewModel.removeArticleBookmark(
bookmarkGroupViewModel.bookmarkGroupIdentifier,
articleAddress,
isPrivate,
)
}
},
nav,
)
}
}
}
}

View File

@@ -36,6 +36,7 @@ import androidx.compose.ui.Modifier
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.nip51Lists.BookmarkListState
import com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList
import com.vitorpamplona.amethyst.ui.layouts.MaxWidthContainer
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
@@ -106,23 +107,25 @@ fun ListOfBookmarkGroupsFeed(
BookmarkGroupFab(onAddGroup = addBookmarkGroup)
},
) { paddingValues ->
Column(
Modifier
.padding(
top = paddingValues.calculateTopPadding(),
bottom = paddingValues.calculateBottomPadding(),
).fillMaxHeight(),
) {
ListOfBookmarkGroupsFeedView(
defaultBookmarks = defaultBookmarks,
groupListFeedSource = listSource,
openDefaultBookmarks = openDefaultBookmarks,
onOpenItem = openBookmarkGroup,
onRenameItem = renameBookmarkGroup,
onItemDescriptionChange = changeBookmarkGroupDescription,
onItemClone = cloneBookmarkGroup,
onDeleteItem = deleteBookmarkGroup,
)
MaxWidthContainer {
Column(
Modifier
.padding(
top = paddingValues.calculateTopPadding(),
bottom = paddingValues.calculateBottomPadding(),
).fillMaxHeight(),
) {
ListOfBookmarkGroupsFeedView(
defaultBookmarks = defaultBookmarks,
groupListFeedSource = listSource,
openDefaultBookmarks = openDefaultBookmarks,
onOpenItem = openBookmarkGroup,
onRenameItem = renameBookmarkGroup,
onItemDescriptionChange = changeBookmarkGroupDescription,
onItemClone = cloneBookmarkGroup,
onDeleteItem = deleteBookmarkGroup,
)
}
}
}
}

View File

@@ -26,7 +26,9 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.layouts.MaxWidthContainer
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.RenderRoomTopBar
@@ -50,16 +52,18 @@ fun ChatroomScreen(
},
accountViewModel = accountViewModel,
) {
Column(Modifier.padding(it).consumeWindowInsets(it).statusBarsPadding()) {
ChatroomView(
room = roomId,
draftMessage = draftMessage,
replyToNote = replyToNote,
editFromDraft = editFromDraft,
expiresDays = expiresDays,
accountViewModel = accountViewModel,
nav = nav,
)
MaxWidthContainer(maxWidth = 720.dp) {
Column(Modifier.padding(it).consumeWindowInsets(it).statusBarsPadding()) {
ChatroomView(
room = roomId,
draftMessage = draftMessage,
replyToNote = replyToNote,
editFromDraft = editFromDraft,
expiresDays = expiresDays,
accountViewModel = accountViewModel,
nav = nav,
)
}
}
}
}

View File

@@ -24,8 +24,10 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.layouts.MaxWidthContainer
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.EphemeralChatTopBar
@@ -48,8 +50,10 @@ fun EphemeralChatScreen(
},
accountViewModel = accountViewModel,
) {
Column(Modifier.padding(it)) {
EphemeralChatChannelView(channelId, draft, replyTo, accountViewModel, nav)
MaxWidthContainer(maxWidth = 720.dp) {
Column(Modifier.padding(it)) {
EphemeralChatChannelView(channelId, draft, replyTo, accountViewModel, nav)
}
}
}
}

View File

@@ -24,8 +24,10 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.layouts.MaxWidthContainer
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -51,8 +53,10 @@ fun PublicChatChannelScreen(
},
accountViewModel = accountViewModel,
) {
Column(Modifier.padding(it)) {
PublicChatChannelView(channelId, draft, replyTo, accountViewModel, nav)
MaxWidthContainer(maxWidth = 720.dp) {
Column(Modifier.padding(it)) {
PublicChatChannelView(channelId, draft, replyTo, accountViewModel, nav)
}
}
}
}

View File

@@ -25,9 +25,11 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.OnlineChecker
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.layouts.MaxWidthContainer
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.LoadLiveActivityChannel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -58,8 +60,10 @@ fun LiveActivityChannelScreen(
},
accountViewModel = accountViewModel,
) {
Column(Modifier.padding(it)) {
LiveActivityChannelView(channelId, draft, replyTo, accountViewModel, nav)
MaxWidthContainer(maxWidth = 720.dp) {
Column(Modifier.padding(it)) {
LiveActivityChannelView(channelId, draft, replyTo, accountViewModel, nav)
}
}
}
}

View File

@@ -48,6 +48,7 @@ import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.ui.feeds.rememberForeverPagerState
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.layouts.MaxWidthContainer
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.topbars.ShorterTopAppBar
import com.vitorpamplona.amethyst.ui.navigation.topbars.TitleIconModifier
@@ -191,27 +192,29 @@ fun CommunityScreen(
},
accountViewModel = accountViewModel,
) {
HorizontalPager(
contentPadding = it,
state = pagerState,
) { page ->
when (page) {
0 -> {
RefresheableFeedView(
feedViewModel,
null,
accountViewModel = accountViewModel,
nav = nav,
)
}
MaxWidthContainer {
HorizontalPager(
contentPadding = it,
state = pagerState,
) { page ->
when (page) {
0 -> {
RefresheableFeedView(
feedViewModel,
null,
accountViewModel = accountViewModel,
nav = nav,
)
}
1 -> {
RefresheableFeedView(
modFeedViewModel,
null,
accountViewModel = accountViewModel,
nav = nav,
)
1 -> {
RefresheableFeedView(
modFeedViewModel,
null,
accountViewModel = accountViewModel,
nav = nav,
)
}
}
}
}

View File

@@ -69,6 +69,7 @@ import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.ui.feeds.rememberForeverPagerState
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.layouts.MaxWidthContainer
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
@@ -258,31 +259,33 @@ private fun DiscoverPages(
},
accountViewModel = accountViewModel,
) {
HorizontalPager(state = pagerState, contentPadding = it) { page ->
if (page >= 0 && page < feedTabs.size) {
val tab = feedTabs[page]
RefresheableBox(tab.feedState, true) {
if (tab.useGridLayout) {
SaveableGridFeedContentState(tab.feedState, scrollStateKey = tab.scrollStateKey) { listState ->
RenderDiscoverFeed(
feedContentState = tab.feedState,
routeForLastRead = tab.routeForLastRead,
forceEventKind = tab.forceEventKind,
listState = listState,
accountViewModel = accountViewModel,
nav = nav,
)
}
} else {
SaveableFeedContentState(tab.feedState, scrollStateKey = tab.scrollStateKey) { listState ->
RenderDiscoverFeed(
feedContentState = tab.feedState,
routeForLastRead = tab.routeForLastRead,
forceEventKind = tab.forceEventKind,
listState = listState,
accountViewModel = accountViewModel,
nav = nav,
)
MaxWidthContainer {
HorizontalPager(state = pagerState, contentPadding = it) { page ->
if (page >= 0 && page < feedTabs.size) {
val tab = feedTabs[page]
RefresheableBox(tab.feedState, true) {
if (tab.useGridLayout) {
SaveableGridFeedContentState(tab.feedState, scrollStateKey = tab.scrollStateKey) { listState ->
RenderDiscoverFeed(
feedContentState = tab.feedState,
routeForLastRead = tab.routeForLastRead,
forceEventKind = tab.forceEventKind,
listState = listState,
accountViewModel = accountViewModel,
nav = nav,
)
}
} else {
SaveableFeedContentState(tab.feedState, scrollStateKey = tab.scrollStateKey) { listState ->
RenderDiscoverFeed(
feedContentState = tab.feedState,
routeForLastRead = tab.routeForLastRead,
forceEventKind = tab.forceEventKind,
listState = listState,
accountViewModel = accountViewModel,
nav = nav,
)
}
}
}
}

View File

@@ -52,6 +52,7 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton
import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton
import com.vitorpamplona.amethyst.ui.layouts.MaxWidthContainer
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar
@@ -173,11 +174,13 @@ fun NewProductScreen(
.consumeWindowInsets(pad)
.imePadding(),
) {
NewProductBody(
postViewModel,
accountViewModel,
nav,
)
MaxWidthContainer(maxWidth = 560.dp) {
NewProductBody(
postViewModel,
accountViewModel,
nav,
)
}
}
}
}

View File

@@ -61,6 +61,7 @@ import com.vitorpamplona.amethyst.ui.components.ZapButtonConfig
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.layouts.MaxWidthContainer
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent
import com.vitorpamplona.amethyst.ui.note.elements.BannerImage
@@ -97,19 +98,21 @@ fun DvmContentDiscoveryScreen(
},
accountViewModel = accountViewModel,
) { paddingValues ->
Column(Modifier.padding(paddingValues)) {
LoadNote(baseNoteHex = appDefinitionEventId, accountViewModel = accountViewModel) { note ->
note?.let { baseNote ->
WatchNoteEvent(
baseNote,
onNoteEventFound = {
DvmContentDiscoveryScreen(baseNote, accountViewModel, nav)
},
onBlank = {
FeedEmptyWithStatus(baseNote, stringRes(R.string.dvm_looking_for_app), accountViewModel, nav)
},
accountViewModel,
)
MaxWidthContainer {
Column(Modifier.padding(paddingValues)) {
LoadNote(baseNoteHex = appDefinitionEventId, accountViewModel = accountViewModel) { note ->
note?.let { baseNote ->
WatchNoteEvent(
baseNote,
onNoteEventFound = {
DvmContentDiscoveryScreen(baseNote, accountViewModel, nav)
},
onBlank = {
FeedEmptyWithStatus(baseNote, stringRes(R.string.dvm_looking_for_app), accountViewModel, nav)
},
accountViewModel,
)
}
}
}
}

View File

@@ -59,6 +59,7 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNo
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.ui.feeds.rememberForeverPagerState
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.layouts.MaxWidthContainer
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.navigation.topbars.ShorterTopAppBar
@@ -171,35 +172,37 @@ fun FollowPackFeedScreen(
},
accountViewModel = accountViewModel,
) {
HorizontalPager(
contentPadding = it,
state = pagerState,
) { page ->
when (page) {
0 -> {
RefresheableFeedView(
newThreadFeedViewModel,
null,
accountViewModel = accountViewModel,
nav = nav,
)
}
MaxWidthContainer {
HorizontalPager(
contentPadding = it,
state = pagerState,
) { page ->
when (page) {
0 -> {
RefresheableFeedView(
newThreadFeedViewModel,
null,
accountViewModel = accountViewModel,
nav = nav,
)
}
1 -> {
RefresheableFeedView(
conversationsFeedViewModel,
null,
accountViewModel = accountViewModel,
nav = nav,
)
}
1 -> {
RefresheableFeedView(
conversationsFeedViewModel,
null,
accountViewModel = accountViewModel,
nav = nav,
)
}
2 -> {
UserFeedView(
membersFeedViewModel,
accountViewModel,
nav,
)
2 -> {
UserFeedView(
membersFeedViewModel,
accountViewModel,
nav,
)
}
}
}
}

View File

@@ -33,6 +33,7 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowingGeohash
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.layouts.MaxWidthContainer
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton
@@ -102,13 +103,15 @@ fun GeoHashScreen(
},
accountViewModel = accountViewModel,
) {
Column(Modifier.padding(it)) {
RefresheableFeedView(
feedViewModel,
null,
accountViewModel = accountViewModel,
nav = nav,
)
MaxWidthContainer {
Column(Modifier.padding(it)) {
RefresheableFeedView(
feedViewModel,
null,
accountViewModel = accountViewModel,
nav = nav,
)
}
}
}
}

View File

@@ -38,6 +38,7 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowingHashtag
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.layouts.MaxWidthContainer
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton
@@ -107,13 +108,15 @@ fun HashtagScreen(
},
accountViewModel = accountViewModel,
) {
Column(Modifier.padding(it)) {
RefresheableFeedView(
feedViewModel,
null,
accountViewModel = accountViewModel,
nav = nav,
)
MaxWidthContainer {
Column(Modifier.padding(it)) {
RefresheableFeedView(
feedViewModel,
null,
accountViewModel = accountViewModel,
nav = nav,
)
}
}
}
}

View File

@@ -73,6 +73,7 @@ import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.ui.feeds.rememberForeverPagerState
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.layouts.MaxWidthContainer
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
@@ -208,19 +209,21 @@ private fun HomePages(
},
accountViewModel = accountViewModel,
) {
HorizontalPager(
contentPadding = it,
state = pagerState,
userScrollEnabled = false,
) { page ->
HomeFeeds(
feedState = tabs[page].feedState,
routeForLastRead = tabs[page].routeForLastRead,
scrollStateKey = tabs[page].scrollStateKey,
liveSection = tabs[page].liveSection,
accountViewModel = accountViewModel,
nav = nav,
)
MaxWidthContainer {
HorizontalPager(
contentPadding = it,
state = pagerState,
userScrollEnabled = false,
) { page ->
HomeFeeds(
feedState = tabs[page].feedState,
routeForLastRead = tabs[page].routeForLastRead,
scrollStateKey = tabs[page].scrollStateKey,
liveSection = tabs[page].liveSection,
accountViewModel = accountViewModel,
nav = nav,
)
}
}
}
}

View File

@@ -72,6 +72,7 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator
import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationSection
import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview
import com.vitorpamplona.amethyst.ui.components.getActivity
import com.vitorpamplona.amethyst.ui.layouts.MaxWidthContainer
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar
import com.vitorpamplona.amethyst.ui.note.BaseUserPicture
@@ -216,7 +217,9 @@ private fun NewPostScreenInner(
.consumeWindowInsets(pad)
.imePadding(),
) {
NewPostScreenBody(postViewModel, accountViewModel, nav)
MaxWidthContainer(maxWidth = 560.dp) {
NewPostScreenBody(postViewModel, accountViewModel, nav)
}
}
}
}

View File

@@ -44,6 +44,7 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow
import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator
import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationSection
import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview
import com.vitorpamplona.amethyst.ui.layouts.MaxWidthContainer
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar
import com.vitorpamplona.amethyst.ui.note.NoteCompose
@@ -98,7 +99,9 @@ fun VoiceReplyScreen(
.padding(pad)
.consumeWindowInsets(pad),
) {
VoiceReplyScreenBody(viewModel, accountViewModel, nav)
MaxWidthContainer(maxWidth = 560.dp) {
VoiceReplyScreenBody(viewModel, accountViewModel, nav)
}
}
}
}

View File

@@ -75,6 +75,7 @@ import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.components.ClickableBox
import com.vitorpamplona.amethyst.ui.layouts.MaxWidthContainer
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
@@ -136,20 +137,22 @@ fun PeopleListScreen(
}
},
) { padding ->
ListViewAndEditColumn(
viewModel = viewModel,
pagerState = pagerState,
modifier =
Modifier
.fillMaxSize()
.padding(
top = padding.calculateTopPadding(),
bottom = padding.calculateBottomPadding(),
).consumeWindowInsets(padding)
.imePadding(),
accountViewModel = accountViewModel,
nav = nav,
)
MaxWidthContainer {
ListViewAndEditColumn(
viewModel = viewModel,
pagerState = pagerState,
modifier =
Modifier
.fillMaxSize()
.padding(
top = padding.calculateTopPadding(),
bottom = padding.calculateBottomPadding(),
).consumeWindowInsets(padding)
.imePadding(),
accountViewModel = accountViewModel,
nav = nav,
)
}
}
}

View File

@@ -35,6 +35,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.layouts.MaxWidthContainer
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -66,14 +67,16 @@ fun ListOfPeopleListsScreen(
TopBarWithBackButton(stringRes(R.string.my_lists), nav::popBack)
},
) { paddingValues ->
Column(
Modifier
.padding(
top = paddingValues.calculateTopPadding(),
bottom = paddingValues.calculateBottomPadding(),
).fillMaxHeight(),
) {
AllPeopleListFeedView(list, pack, nav)
MaxWidthContainer {
Column(
Modifier
.padding(
top = paddingValues.calculateTopPadding(),
bottom = paddingValues.calculateBottomPadding(),
).fillMaxHeight(),
) {
AllPeopleListFeedView(list, pack, nav)
}
}
}
}

View File

@@ -32,6 +32,7 @@ import com.vitorpamplona.amethyst.model.UiSettingsFlow
import com.vitorpamplona.amethyst.ui.components.SelectNotificationProvider
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.layouts.MaxWidthContainer
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
@@ -84,17 +85,19 @@ fun NotificationScreen(
},
accountViewModel = accountViewModel,
) {
Column(
modifier = Modifier.padding(it).consumeWindowInsets(it),
) {
ObserveInboxRelayListAndDisplayIfNotFound(accountViewModel, nav)
RefreshableCardView(
feedContent = notifFeedContentState,
accountViewModel = accountViewModel,
nav = nav,
routeForLastRead = "Notification",
scrollStateKey = ScrollStateKeys.NOTIFICATION_SCREEN,
)
MaxWidthContainer {
Column(
modifier = Modifier.padding(it).consumeWindowInsets(it),
) {
ObserveInboxRelayListAndDisplayIfNotFound(accountViewModel, nav)
RefreshableCardView(
feedContent = notifFeedContentState,
accountViewModel = accountViewModel,
nav = nav,
routeForLastRead = "Notification",
scrollStateKey = ScrollStateKeys.NOTIFICATION_SCREEN,
)
}
}
}
}

View File

@@ -23,12 +23,14 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
@@ -40,6 +42,7 @@ import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.Surface
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -56,12 +59,15 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.window.core.layout.WindowWidthSizeClass
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.observeAccountIsHiddenUser
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.ui.layouts.LocalWindowSizeClass
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks.BookmarkTabHeader
@@ -277,11 +283,11 @@ fun ProfileScreen(
UserProfileFilterAssemblerSubscription(baseUser, accountViewModel.dataSources().profile)
RenderSurface { tabRowModifier: Modifier, pagerModifier: Modifier ->
RenderScreen(
val isExpanded = LocalWindowSizeClass.current == WindowWidthSizeClass.EXPANDED
if (isExpanded) {
RenderExpandedScreen(
baseUser,
tabRowModifier,
pagerModifier,
threadsViewModel,
repliesViewModel,
mutualViewModel,
@@ -296,6 +302,27 @@ fun ProfileScreen(
accountViewModel,
nav,
)
} else {
RenderSurface { tabRowModifier: Modifier, pagerModifier: Modifier ->
RenderScreen(
baseUser,
tabRowModifier,
pagerModifier,
threadsViewModel,
repliesViewModel,
mutualViewModel,
appRecommendations,
externalIdentities,
followsFeedViewModel,
followersFeedViewModel,
zapFeedViewModel,
bookmarksFeedViewModel,
galleryFeedViewModel,
reportsFeedViewModel,
accountViewModel,
nav,
)
}
}
}
@@ -371,6 +398,106 @@ private fun RenderSurface(content: @Composable (tabRowModifier: Modifier, pagerM
}
}
@Composable
private fun RenderExpandedScreen(
baseUser: User,
threadsViewModel: UserProfileNewThreadsFeedViewModel,
repliesViewModel: UserProfileConversationsFeedViewModel,
mutualViewModel: UserProfileMutualFeedViewModel,
appRecommendations: UserAppRecommendationsFeedViewModel,
externalIdentities: UserExternalIdentitiesViewModel,
followsFeedViewModel: UserProfileFollowsUserFeedViewModel,
followersFeedViewModel: UserProfileFollowersUserFeedViewModel,
zapFeedViewModel: UserProfileZapsViewModel,
bookmarksFeedViewModel: UserProfileBookmarksFeedViewModel,
galleryFeedViewModel: UserProfileGalleryFeedViewModel,
reportsFeedViewModel: UserProfileReportFeedViewModel,
accountViewModel: AccountViewModel,
nav: INav,
) {
val pagerState = rememberPagerState { 11 }
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background,
) {
Row(modifier = Modifier.fillMaxSize()) {
// Left side pane: profile header in a scrollable column
Column(
modifier =
Modifier
.width(320.dp)
.fillMaxHeight()
.verticalScroll(rememberScrollState()),
) {
ProfileHeader(baseUser, appRecommendations, externalIdentities, nav, accountViewModel)
}
VerticalDivider(thickness = DividerThickness)
// Right side: tabs + pager filling remaining space
var columnSize by remember { mutableStateOf(IntSize.Zero) }
var tabsSize by remember { mutableStateOf(IntSize.Zero) }
Column(
modifier =
Modifier
.weight(1f)
.fillMaxHeight()
.onSizeChanged { columnSize = it },
) {
val tabRowModifier = remember { Modifier.onSizeChanged { tabsSize = it } }
val pagerModifier =
with(LocalDensity.current) { Modifier.height((columnSize.height - tabsSize.height).toDp()) }
ScrollableTabRow(
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.onBackground,
selectedTabIndex = pagerState.currentPage,
edgePadding = Size8dp,
modifier = tabRowModifier,
divider = { HorizontalDivider(thickness = DividerThickness) },
) {
CreateAndRenderTabs(
baseUser,
pagerState,
threadsViewModel,
repliesViewModel,
mutualViewModel,
followsFeedViewModel,
followersFeedViewModel,
zapFeedViewModel,
bookmarksFeedViewModel,
galleryFeedViewModel,
reportsFeedViewModel,
accountViewModel,
)
}
HorizontalPager(
state = pagerState,
modifier = pagerModifier,
) { page ->
CreateAndRenderPages(
page,
baseUser,
threadsViewModel,
repliesViewModel,
mutualViewModel,
followsFeedViewModel,
followersFeedViewModel,
zapFeedViewModel,
bookmarksFeedViewModel,
galleryFeedViewModel,
reportsFeedViewModel,
accountViewModel,
nav,
)
}
}
}
}
}
@Composable
private fun RenderScreen(
baseUser: User,

View File

@@ -55,6 +55,7 @@ import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.layouts.MaxWidthContainer
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
@@ -147,20 +148,22 @@ fun ShowQRScreen(
)
},
) { pad ->
Column(
Modifier
.fillMaxSize()
.padding(
start = 20.dp,
end = 20.dp,
top = pad.calculateTopPadding(),
bottom = pad.calculateBottomPadding(),
).consumeWindowInsets(pad)
.imePadding(),
verticalArrangement = Arrangement.SpaceAround,
horizontalAlignment = Alignment.CenterHorizontally,
) {
ShowQRBody(user, accountViewModel, nav)
MaxWidthContainer(maxWidth = 560.dp) {
Column(
Modifier
.fillMaxSize()
.padding(
start = 20.dp,
end = 20.dp,
top = pad.calculateTopPadding(),
bottom = pad.calculateBottomPadding(),
).consumeWindowInsets(pad)
.imePadding(),
verticalArrangement = Arrangement.SpaceAround,
horizontalAlignment = Alignment.CenterHorizontally,
) {
ShowQRBody(user, accountViewModel, nav)
}
}
}
}

View File

@@ -32,6 +32,7 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowingRelay
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.layouts.MaxWidthContainer
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton
@@ -105,13 +106,15 @@ fun RelayFeedScreen(
},
accountViewModel = accountViewModel,
) {
Column(Modifier.padding(it)) {
RefresheableFeedView(
feedViewModel,
null,
accountViewModel = accountViewModel,
nav = nav,
)
MaxWidthContainer {
Column(Modifier.padding(it)) {
RefresheableFeedView(
feedViewModel,
null,
accountViewModel = accountViewModel,
nav = nav,
)
}
}
}
}

View File

@@ -43,6 +43,7 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.DefaultDMRelayList
import com.vitorpamplona.amethyst.model.DefaultIndexerRelayList
import com.vitorpamplona.amethyst.model.DefaultSearchRelayList
import com.vitorpamplona.amethyst.ui.layouts.MaxWidthContainer
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -210,142 +211,144 @@ fun MappedAllRelayListView(
)
},
) { pad ->
LazyColumn(
contentPadding = FeedPadding,
modifier =
Modifier
.fillMaxSize()
.padding(
start = 10.dp,
end = 10.dp,
top = pad.calculateTopPadding(),
bottom = pad.calculateBottomPadding(),
).consumeWindowInsets(pad)
.imePadding(),
) {
item {
SettingsCategory(
R.string.public_home_section,
R.string.public_home_section_explainer,
SettingsCategoryFirstModifier,
)
}
renderNip65HomeItems(homeFeedState, nip65ViewModel, accountViewModel, newNav)
item {
SettingsCategory(
R.string.public_notif_section,
R.string.public_notif_section_explainer,
SettingsCategorySpacingModifier,
)
}
renderNip65NotifItems(notifFeedState, nip65ViewModel, accountViewModel, newNav)
item {
SettingsCategoryWithButton(
R.string.private_inbox_section,
R.string.private_inbox_section_explainer,
SettingsCategorySpacingModifier,
action = {
ResetDMRelays(dmViewModel)
},
)
}
renderDMItems(dmFeedState, dmViewModel, accountViewModel, newNav)
item {
SettingsCategory(
R.string.private_outbox_section,
R.string.private_outbox_section_explainer,
SettingsCategorySpacingModifier,
)
}
renderPrivateOutboxItems(privateOutboxFeedState, privateOutboxViewModel, accountViewModel, newNav)
item {
SettingsCategory(
R.string.proxy_section,
R.string.proxy_section_explainer,
SettingsCategorySpacingModifier,
)
}
renderProxyItems(proxyRelays, proxyViewModel, accountViewModel, newNav)
item {
SettingsCategory(
R.string.broadcast_section,
R.string.broadcast_section_explainer,
SettingsCategorySpacingModifier,
)
}
renderBroadcastItems(broadcastRelays, broadcastViewModel, accountViewModel, newNav)
item {
SettingsCategoryWithButton(
R.string.indexer_section,
R.string.indexer_section_explainer,
SettingsCategorySpacingModifier,
) {
ResetIndexerRelays(indexerViewModel)
MaxWidthContainer(maxWidth = 560.dp) {
LazyColumn(
contentPadding = FeedPadding,
modifier =
Modifier
.fillMaxSize()
.padding(
start = 10.dp,
end = 10.dp,
top = pad.calculateTopPadding(),
bottom = pad.calculateBottomPadding(),
).consumeWindowInsets(pad)
.imePadding(),
) {
item {
SettingsCategory(
R.string.public_home_section,
R.string.public_home_section_explainer,
SettingsCategoryFirstModifier,
)
}
}
renderIndexerItems(indexerRelays, indexerViewModel, accountViewModel, newNav)
renderNip65HomeItems(homeFeedState, nip65ViewModel, accountViewModel, newNav)
item {
SettingsCategoryWithButton(
R.string.search_section,
R.string.search_section_explainer,
SettingsCategorySpacingModifier,
) {
ResetSearchRelays(searchViewModel)
item {
SettingsCategory(
R.string.public_notif_section,
R.string.public_notif_section_explainer,
SettingsCategorySpacingModifier,
)
}
}
renderSearchItems(searchFeedState, searchViewModel, accountViewModel, newNav)
renderNip65NotifItems(notifFeedState, nip65ViewModel, accountViewModel, newNav)
item {
SettingsCategory(
R.string.local_section,
R.string.local_section_explainer,
SettingsCategorySpacingModifier,
)
}
renderLocalItems(localFeedState, localViewModel, accountViewModel, newNav)
item {
SettingsCategoryWithButton(
R.string.private_inbox_section,
R.string.private_inbox_section_explainer,
SettingsCategorySpacingModifier,
action = {
ResetDMRelays(dmViewModel)
},
)
}
renderDMItems(dmFeedState, dmViewModel, accountViewModel, newNav)
item {
SettingsCategory(
R.string.trusted_section,
R.string.trusted_section_explainer,
SettingsCategorySpacingModifier,
)
}
renderTrustedItems(trustedFeedState, trustedViewModel, accountViewModel, newNav)
item {
SettingsCategory(
R.string.private_outbox_section,
R.string.private_outbox_section_explainer,
SettingsCategorySpacingModifier,
)
}
renderPrivateOutboxItems(privateOutboxFeedState, privateOutboxViewModel, accountViewModel, newNav)
item {
SettingsCategory(
R.string.favorite_section,
R.string.favorite_section_explainer,
SettingsCategorySpacingModifier,
)
}
renderRelayFeedsItems(relayFeedsFeedState, relayFeedsViewModel, accountViewModel, newNav)
item {
SettingsCategory(
R.string.proxy_section,
R.string.proxy_section_explainer,
SettingsCategorySpacingModifier,
)
}
renderProxyItems(proxyRelays, proxyViewModel, accountViewModel, newNav)
item {
SettingsCategory(
R.string.blocked_section,
R.string.blocked_section_explainer,
SettingsCategorySpacingModifier,
)
}
renderBlockedItems(blockedFeedState, blockedViewModel, accountViewModel, newNav)
item {
SettingsCategory(
R.string.broadcast_section,
R.string.broadcast_section_explainer,
SettingsCategorySpacingModifier,
)
}
renderBroadcastItems(broadcastRelays, broadcastViewModel, accountViewModel, newNav)
item {
SettingsCategory(
R.string.connected_section,
R.string.connected_section_description,
SettingsCategorySpacingModifier,
)
item {
SettingsCategoryWithButton(
R.string.indexer_section,
R.string.indexer_section_explainer,
SettingsCategorySpacingModifier,
) {
ResetIndexerRelays(indexerViewModel)
}
}
renderIndexerItems(indexerRelays, indexerViewModel, accountViewModel, newNav)
item {
SettingsCategoryWithButton(
R.string.search_section,
R.string.search_section_explainer,
SettingsCategorySpacingModifier,
) {
ResetSearchRelays(searchViewModel)
}
}
renderSearchItems(searchFeedState, searchViewModel, accountViewModel, newNav)
item {
SettingsCategory(
R.string.local_section,
R.string.local_section_explainer,
SettingsCategorySpacingModifier,
)
}
renderLocalItems(localFeedState, localViewModel, accountViewModel, newNav)
item {
SettingsCategory(
R.string.trusted_section,
R.string.trusted_section_explainer,
SettingsCategorySpacingModifier,
)
}
renderTrustedItems(trustedFeedState, trustedViewModel, accountViewModel, newNav)
item {
SettingsCategory(
R.string.favorite_section,
R.string.favorite_section_explainer,
SettingsCategorySpacingModifier,
)
}
renderRelayFeedsItems(relayFeedsFeedState, relayFeedsViewModel, accountViewModel, newNav)
item {
SettingsCategory(
R.string.blocked_section,
R.string.blocked_section_explainer,
SettingsCategorySpacingModifier,
)
}
renderBlockedItems(blockedFeedState, blockedViewModel, accountViewModel, newNav)
item {
SettingsCategory(
R.string.connected_section,
R.string.connected_section_description,
SettingsCategorySpacingModifier,
)
}
renderConnectedItems(connectedRelays, connectedViewModel, accountViewModel, newNav)
}
renderConnectedItems(connectedRelays, connectedViewModel, accountViewModel, newNav)
}
}
}

View File

@@ -59,6 +59,7 @@ import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo
import com.vitorpamplona.amethyst.service.relayClient.searchCommand.TextSearchDataSourceSubscription
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.layouts.MaxWidthContainer
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
@@ -124,11 +125,13 @@ fun SearchScreen(
},
accountViewModel = accountViewModel,
) {
Column(
modifier = Modifier.padding(it).consumeWindowInsets(it),
) {
ObserveRelayListForSearchAndDisplayIfNotFound(accountViewModel, nav)
DisplaySearchResults(searchBarViewModel, nav, accountViewModel)
MaxWidthContainer {
Column(
modifier = Modifier.padding(it).consumeWindowInsets(it),
) {
ObserveRelayListForSearchAndDisplayIfNotFound(accountViewModel, nav)
DisplaySearchResults(searchBarViewModel, nav, accountViewModel)
}
}
}
}

View File

@@ -0,0 +1,189 @@
/*
* Copyright (c) 2025 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.settings
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.layout.AnimatedPane
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.navigation.NavigableListDetailPaneScaffold
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.layouts.isCompactWindow
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun AdaptiveSettingsScreen(
accountViewModel: AccountViewModel,
nav: INav,
) {
if (isCompactWindow()) {
AllSettingsScreen(accountViewModel, nav)
} else {
AdaptiveSettingsListDetail(accountViewModel, nav)
}
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
private fun AdaptiveSettingsListDetail(
accountViewModel: AccountViewModel,
nav: INav,
) {
val navigator = rememberListDetailPaneScaffoldNavigator<Route>()
val scope = rememberCoroutineScope()
BackHandler(navigator.canNavigateBack()) {
scope.launch {
navigator.navigateBack()
}
}
Scaffold(
topBar = {
TopBarWithBackButton(stringRes(id = R.string.settings), nav::popBack)
},
) { padding ->
NavigableListDetailPaneScaffold(
navigator = navigator,
listPane = {
AnimatedPane {
AllSettingsContent(
accountViewModel = accountViewModel,
onSettingsNav = { route ->
scope.launch {
navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, route)
}
},
)
}
},
detailPane = {
AnimatedPane {
val selectedRoute = navigator.currentDestination?.contentKey
if (selectedRoute != null) {
SettingsDetailContent(
route = selectedRoute,
accountViewModel = accountViewModel,
nav = nav,
)
} else {
SettingsDetailPlaceholder()
}
}
},
modifier = Modifier.padding(padding),
)
}
}
@Composable
private fun SettingsDetailContent(
route: Route,
accountViewModel: AccountViewModel,
nav: INav,
) {
when (route) {
is Route.EditRelays -> {
com.vitorpamplona.amethyst.ui.screen.loggedIn.relays
.AllRelayListScreen(accountViewModel, nav)
}
is Route.EditMediaServers -> {
com.vitorpamplona.amethyst.ui.actions.mediaServers
.AllMediaServersScreen(accountViewModel, nav)
}
is Route.UpdateReactionType -> {
com.vitorpamplona.amethyst.ui.note
.UpdateReactionTypeScreen(accountViewModel, nav)
}
is Route.UpdateZapAmount -> {
UpdateZapAmountScreen(accountViewModel, nav, route.nip47)
}
is Route.SecurityFilters -> {
SecurityFiltersScreen(accountViewModel, nav)
}
is Route.UserSettings -> {
UserSettingsScreen(accountViewModel, nav)
}
is Route.AccountBackup -> {
com.vitorpamplona.amethyst.ui.screen.loggedIn.keyBackup
.AccountBackupScreen(accountViewModel, nav)
}
is Route.PrivacyOptions -> {
com.vitorpamplona.amethyst.ui.screen.loggedIn.privacy
.PrivacyOptionsScreen(Amethyst.instance.torPrefs.value, nav)
}
is Route.NamecoinSettings -> {
NamecoinSettingsScreen(Amethyst.instance.namecoinPrefs, nav)
}
is Route.Settings -> {
SettingsScreen(accountViewModel, nav)
}
is Route.ReactionsSettings -> {
ReactionsSettingsScreen(accountViewModel, nav)
}
else -> {
SettingsDetailPlaceholder()
}
}
}
@Composable
private fun SettingsDetailPlaceholder() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
Text(
text = stringRes(R.string.settings),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}

View File

@@ -76,97 +76,110 @@ fun AllSettingsScreen(
accountViewModel: AccountViewModel,
nav: INav,
) {
val tint = MaterialTheme.colorScheme.onBackground
Scaffold(
topBar = {
TopBarWithBackButton(stringRes(id = R.string.settings), nav::popBack)
},
) { padding ->
Column(Modifier.padding(padding)) {
SettingsSectionHeader(R.string.account_settings)
SettingsNavigationRow(
title = R.string.relay_setup,
iconPainter = R.drawable.relays,
iconPainterRef = 4,
tint = tint,
onClick = { nav.nav(Route.EditRelays) },
)
AllSettingsContent(
accountViewModel = accountViewModel,
onSettingsNav = { route -> nav.nav(route) },
modifier = Modifier.padding(padding),
)
}
}
@Composable
fun AllSettingsContent(
accountViewModel: AccountViewModel,
onSettingsNav: (Route) -> Unit,
modifier: Modifier = Modifier,
) {
val tint = MaterialTheme.colorScheme.onBackground
Column(modifier) {
SettingsSectionHeader(R.string.account_settings)
SettingsNavigationRow(
title = R.string.relay_setup,
iconPainter = R.drawable.relays,
iconPainterRef = 4,
tint = tint,
onClick = { onSettingsNav(Route.EditRelays) },
)
HorizontalDivider()
SettingsNavigationRow(
title = R.string.media_servers,
icon = Icons.Outlined.CloudUpload,
tint = tint,
onClick = { onSettingsNav(Route.EditMediaServers) },
)
HorizontalDivider()
SettingsNavigationRow(
title = R.string.reactions,
icon = Icons.Outlined.FavoriteBorder,
tint = tint,
onClick = { onSettingsNav(Route.UpdateReactionType) },
)
HorizontalDivider()
SettingsNavigationRow(
title = R.string.zaps,
icon = Icons.Outlined.Bolt,
tint = tint,
onClick = { onSettingsNav(Route.UpdateZapAmount()) },
)
HorizontalDivider()
SettingsNavigationRow(
title = R.string.security_filters,
icon = Icons.Outlined.Security,
tint = tint,
onClick = { onSettingsNav(Route.SecurityFilters) },
)
HorizontalDivider()
SettingsNavigationRow(
title = R.string.translations,
icon = Icons.Outlined.Translate,
tint = tint,
onClick = { onSettingsNav(Route.UserSettings) },
)
accountViewModel.account.settings.keyPair.privKey?.let {
HorizontalDivider()
SettingsNavigationRow(
title = R.string.media_servers,
icon = Icons.Outlined.CloudUpload,
title = R.string.backup_keys,
icon = Icons.Outlined.Key,
tint = tint,
onClick = { nav.nav(Route.EditMediaServers) },
)
HorizontalDivider()
SettingsNavigationRow(
title = R.string.reactions,
icon = Icons.Outlined.FavoriteBorder,
tint = tint,
onClick = { nav.nav(Route.UpdateReactionType) },
)
HorizontalDivider()
SettingsNavigationRow(
title = R.string.zaps,
icon = Icons.Outlined.Bolt,
tint = tint,
onClick = { nav.nav(Route.UpdateZapAmount()) },
)
HorizontalDivider()
SettingsNavigationRow(
title = R.string.security_filters,
icon = Icons.Outlined.Security,
tint = tint,
onClick = { nav.nav(Route.SecurityFilters) },
)
HorizontalDivider()
SettingsNavigationRow(
title = R.string.translations,
icon = Icons.Outlined.Translate,
tint = tint,
onClick = { nav.nav(Route.UserSettings) },
)
accountViewModel.account.settings.keyPair.privKey?.let {
HorizontalDivider()
SettingsNavigationRow(
title = R.string.backup_keys,
icon = Icons.Outlined.Key,
tint = tint,
onClick = { nav.nav(Route.AccountBackup) },
)
}
HorizontalDivider(thickness = 4.dp)
SettingsSectionHeader(R.string.app_settings)
SettingsNavigationRow(
title = R.string.privacy_options,
iconPainter = R.drawable.ic_tor,
iconPainterRef = 1,
tint = tint,
onClick = { nav.nav(Route.PrivacyOptions) },
)
HorizontalDivider()
SettingsNavigationRow(
title = R.string.namecoin_settings,
icon = Icons.Outlined.Security,
tint = tint,
onClick = { nav.nav(Route.NamecoinSettings) },
)
HorizontalDivider()
SettingsNavigationRow(
title = R.string.ui_preferences,
icon = Icons.Outlined.Settings,
tint = tint,
onClick = { nav.nav(Route.Settings) },
)
HorizontalDivider()
SettingsNavigationRow(
title = R.string.reactions_settings,
icon = Icons.Outlined.ThumbUp,
tint = tint,
onClick = { nav.nav(Route.ReactionsSettings) },
onClick = { onSettingsNav(Route.AccountBackup) },
)
}
HorizontalDivider(thickness = 4.dp)
SettingsSectionHeader(R.string.app_settings)
SettingsNavigationRow(
title = R.string.privacy_options,
iconPainter = R.drawable.ic_tor,
iconPainterRef = 1,
tint = tint,
onClick = { onSettingsNav(Route.PrivacyOptions) },
)
HorizontalDivider()
SettingsNavigationRow(
title = R.string.namecoin_settings,
icon = Icons.Outlined.Security,
tint = tint,
onClick = { onSettingsNav(Route.NamecoinSettings) },
)
HorizontalDivider()
SettingsNavigationRow(
title = R.string.ui_preferences,
icon = Icons.Outlined.Settings,
tint = tint,
onClick = { onSettingsNav(Route.Settings) },
)
HorizontalDivider()
SettingsNavigationRow(
title = R.string.reactions_settings,
icon = Icons.Outlined.ThumbUp,
tint = tint,
onClick = { onSettingsNav(Route.ReactionsSettings) },
)
}
}

View File

@@ -27,8 +27,10 @@ import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.layouts.MaxWidthContainer
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar
import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountContent
@@ -75,11 +77,13 @@ fun NIP47SetupScreen(
)
},
) {
Column(Modifier.padding(it)) {
UpdateZapAmountContent(postViewModel, onClose = {
postViewModel.cancel()
nav.popBack()
}, nip47, accountViewModel)
MaxWidthContainer(maxWidth = 560.dp) {
Column(Modifier.padding(it)) {
UpdateZapAmountContent(postViewModel, onClose = {
postViewModel.cancel()
nav.popBack()
}, nip47, accountViewModel)
}
}
}
}

View File

@@ -31,6 +31,7 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFind
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.layouts.MaxWidthContainer
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -72,8 +73,10 @@ fun ThreadScreen(
},
accountViewModel = accountViewModel,
) {
Column(Modifier.padding(it)) {
ThreadFeedView(noteId, feedViewModel, accountViewModel, nav)
MaxWidthContainer {
Column(Modifier.padding(it)) {
ThreadFeedView(noteId, feedViewModel, accountViewModel, nav)
}
}
}
}

View File

@@ -65,6 +65,7 @@ import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.ui.feeds.WatchScrollToTop
import com.vitorpamplona.amethyst.ui.feeds.rememberForeverPagerState
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.layouts.MaxWidthContainer
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
@@ -143,15 +144,17 @@ fun VideoScreen(
},
accountViewModel = accountViewModel,
) {
Column(
modifier = Modifier.padding(it).consumeWindowInsets(it),
) {
RenderPage(
videoFeedContentState = videoFeedContentState,
pagerStateKey = ScrollStateKeys.VIDEO_SCREEN,
accountViewModel = accountViewModel,
nav = nav,
)
MaxWidthContainer {
Column(
modifier = Modifier.padding(it).consumeWindowInsets(it),
) {
RenderPage(
videoFeedContentState = videoFeedContentState,
pagerStateKey = ScrollStateKeys.VIDEO_SCREEN,
accountViewModel = accountViewModel,
nav = nav,
)
}
}
}
}

View File

@@ -59,6 +59,7 @@ zelory = "3.0.1"
zoomable = "2.11.1"
zxing = "3.5.4"
zxingAndroidEmbedded = "4.3.0"
material3Adaptive = "1.1.0"
windowCoreAndroid = "1.5.1"
androidxCamera = "1.5.3"
androidxCollection = "1.5.0"
@@ -95,6 +96,10 @@ androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecyc
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" }
androidx-material-icons = { group = "androidx.compose.material", name = "material-icons-extended" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-material3-adaptive = { group = "androidx.compose.material3.adaptive", name = "adaptive", version.ref = "material3Adaptive" }
androidx-material3-adaptive-layout = { group = "androidx.compose.material3.adaptive", name = "adaptive-layout", version.ref = "material3Adaptive" }
androidx-material3-adaptive-navigation = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation", version.ref = "material3Adaptive" }
androidx-material3-adaptive-navigation-suite = { group = "androidx.compose.material3", name = "material3-adaptive-navigation-suite" }
androidx-material3-windowSize = { group = "androidx.compose.material3", name = "material3-window-size-class" }
androidx-media3-datasource-okhttp = { group = "androidx.media3", name = "media3-datasource-okhttp", version.ref = "media3" }
androidx-media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3" }