refactor(commonsUI): move the remaining movable layouts, feed helpers and link components

Most of the sweep's "Batch 4" had already landed in other PRs; this moves what
was left that had no real blocker.

- NewItemsBubble -> commonsUI components.
- ChatHeaderLayout, LeftPictureLayout, listItem/SlimListItemLayout -> commonsUI
  layouts. Their @Previews use app drawables, TimeAgo and TextCount, so the
  previews stay in the app as *Preview.kt files.
- WatchScrollToTop (list/grid/pager) -> commonsUI feeds; the
  CardFeedContentState overload stays in the app. The app's StickToTopOnPrepend
  was a copy of the commons one and is deleted; the commons one now collects
  with collectAsStateWithLifecycle, as the app copy did.
- ChannelFeedState, ChannelFeedContentState -> commons feeds (headless).
  checkNotInMainThread() becomes LocalCache.appHost.assertNotMainThread(),
  which Android wires to the same check.
- ClickableBuzzInviteLink, ClickableConcordInviteLink, ClickableRelayGroupLink,
  ClickableRelayUrl, OutlinedThinPaddingTextField -> commonsUI components.
- The app's ClickableEmail/ClickablePhone are deleted in favour of the commons
  ones, which open mailto:/tel: through LocalUriHandler rather than
  ACTION_SEND/ACTION_DIAL intents. The commons ClickablePhone is new.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjQQN9CgWHVtNCSWqnKoqY
This commit is contained in:
Claude
2026-09-26 21:13:14 +00:00
parent 150177f5a0
commit 9b26de4515
45 changed files with 471 additions and 556 deletions
@@ -78,6 +78,7 @@ import com.vitorpamplona.amethyst.commons.resources.lightning_create_and_add_inv
import com.vitorpamplona.amethyst.commons.resources.lightning_invoice
import com.vitorpamplona.amethyst.commons.resources.what_s_on_your_mind
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.commons.ui.components.OutlinedThinPaddingTextField
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar
@@ -98,7 +99,6 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromFiles
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
import com.vitorpamplona.amethyst.ui.components.BechLink
import com.vitorpamplona.amethyst.ui.components.LoadUrlPreview
import com.vitorpamplona.amethyst.ui.components.OutlinedThinPaddingTextField
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest
import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription
@@ -52,6 +52,12 @@ import com.vitorpamplona.amethyst.commons.richtext.RichTextViewerState
import com.vitorpamplona.amethyst.commons.richtext.SecretEmoji
import com.vitorpamplona.amethyst.commons.richtext.Segment
import com.vitorpamplona.amethyst.commons.richtext.WithdrawSegment
import com.vitorpamplona.amethyst.commons.ui.components.ClickableBuzzInviteLink
import com.vitorpamplona.amethyst.commons.ui.components.ClickableConcordInviteLink
import com.vitorpamplona.amethyst.commons.ui.components.ClickableEmail
import com.vitorpamplona.amethyst.commons.ui.components.ClickablePhone
import com.vitorpamplona.amethyst.commons.ui.components.ClickableRelayGroupLink
import com.vitorpamplona.amethyst.commons.ui.components.ClickableRelayUrl
import com.vitorpamplona.amethyst.commons.ui.components.NowhereLinkCard
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.richtext.LocalRichTextInteractions
@@ -1,60 +0,0 @@
/*
* 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.components
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import com.vitorpamplona.amethyst.commons.ui.components.ClickableTextPrimary
import kotlinx.coroutines.CancellationException
@Composable
fun ClickableEmail(email: String) {
val stripped = email.replaceFirst("mailto:", "")
val context = LocalContext.current
ClickableTextPrimary(
text = stripped,
onClick = { runCatching { context.sendMail(stripped) } },
)
}
fun Context.sendMail(
to: String,
subject: String? = null,
) {
try {
val intent = Intent(Intent.ACTION_SEND)
intent.type = "vnd.android.cursor.item/email" // or "message/rfc822"
intent.putExtra(Intent.EXTRA_EMAIL, arrayOf(to))
if (subject != null) {
intent.putExtra(Intent.EXTRA_SUBJECT, subject)
}
startActivity(intent)
} catch (e: ActivityNotFoundException) {
// TODO: Handle case where no email app is available
} catch (t: Throwable) {
if (t is CancellationException) throw t
// TODO: Handle potential other type of exceptions
}
}
@@ -49,6 +49,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.concord_home_title
import com.vitorpamplona.amethyst.commons.resources.concord_invite_card_join
import com.vitorpamplona.amethyst.commons.resources.concord_invite_card_subtitle
import com.vitorpamplona.amethyst.commons.ui.components.ClickableConcordInviteLink
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings
@@ -53,6 +53,7 @@ import com.vitorpamplona.amethyst.commons.resources.relay_group_badge_invite_onl
import com.vitorpamplona.amethyst.commons.resources.relay_group_badge_private
import com.vitorpamplona.amethyst.commons.resources.relay_group_member_count
import com.vitorpamplona.amethyst.commons.resources.relay_group_open
import com.vitorpamplona.amethyst.commons.ui.components.ClickableRelayGroupLink
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
@@ -102,6 +102,12 @@ import com.vitorpamplona.amethyst.commons.richtext.Segment
import com.vitorpamplona.amethyst.commons.richtext.VideoSegment
import com.vitorpamplona.amethyst.commons.richtext.WithdrawSegment
import com.vitorpamplona.amethyst.commons.ui.components.AnimatedBorderTextCornerRadius
import com.vitorpamplona.amethyst.commons.ui.components.ClickableBuzzInviteLink
import com.vitorpamplona.amethyst.commons.ui.components.ClickableConcordInviteLink
import com.vitorpamplona.amethyst.commons.ui.components.ClickableEmail
import com.vitorpamplona.amethyst.commons.ui.components.ClickablePhone
import com.vitorpamplona.amethyst.commons.ui.components.ClickableRelayGroupLink
import com.vitorpamplona.amethyst.commons.ui.components.ClickableRelayUrl
import com.vitorpamplona.amethyst.commons.ui.components.ClickableTextPrimary
import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.commons.ui.components.NowhereLinkCard
@@ -35,6 +35,8 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.FeedEmpty
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedError
import com.vitorpamplona.amethyst.commons.ui.feeds.LoadingFeed
import com.vitorpamplona.amethyst.commons.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.commons.ui.feeds.StickToTopOnPrepend
import com.vitorpamplona.amethyst.commons.ui.feeds.WatchScrollToTop
import com.vitorpamplona.amethyst.commons.ui.feeds.rememberForeverLazyGridState
import com.vitorpamplona.amethyst.commons.ui.feeds.rememberForeverLazyListState
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
@@ -21,52 +21,11 @@
package com.vitorpamplona.amethyst.ui.feeds
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.pager.PagerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.feeds.FeedState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CardFeedContentState
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
@Composable
fun WatchScrollToTop(
feedContentState: FeedContentState,
listState: LazyListState,
) {
val scrollToTop by feedContentState.scrollToTop.collectAsStateWithLifecycle()
LaunchedEffect(scrollToTop) {
if (scrollToTop > 0 && feedContentState.scrollToTopPending) {
listState.scrollToItem(index = 0)
feedContentState.sentToTop()
}
}
}
@Composable
fun WatchScrollToTop(
feedContentState: FeedContentState,
listState: LazyGridState,
) {
val scrollToTop by feedContentState.scrollToTop.collectAsStateWithLifecycle()
LaunchedEffect(scrollToTop) {
if (scrollToTop > 0 && feedContentState.scrollToTopPending) {
listState.scrollToItem(index = 0)
feedContentState.sentToTop()
}
}
}
@Composable
fun WatchScrollToTop(
@@ -82,166 +41,3 @@ fun WatchScrollToTop(
}
}
}
@Composable
fun WatchScrollToTop(
videoFeedContentState: FeedContentState,
pagerState: PagerState,
) {
val scrollToTop by videoFeedContentState.scrollToTop.collectAsStateWithLifecycle()
LaunchedEffect(scrollToTop) {
if (scrollToTop > 0 && videoFeedContentState.scrollToTopPending) {
pagerState.scrollToPage(page = 0)
videoFeedContentState.sentToTop()
}
}
}
/**
* Keeps the user pinned to index 0 when new items prepend to a feed, but
* only if they were already at the very top right before the update.
*
* Why this is non-trivial: every feed uses stable `key = item.idHex` in
* its lazy list, which makes Compose preserve the visual anchor across
* data changes. When N items prepend, the user's previously-visible
* top item is still on screen but its index is now N — so
* `firstVisibleItemIndex` shifts from 0 to N without any user gesture.
* A naive `if (firstVisibleItemIndex <= 1) scrollToItem(0)` check inside
* `LaunchedEffect(items.firstOrNull())` therefore fails as soon as more
* than one item arrives in the same batch.
*
* The trick: track "was at top" continuously via [snapshotFlow], but
* only flip it from true → false when [LazyListState.isScrollInProgress]
* is true (i.e. the user is actively scrolling). Compose's keyed-item
* shift after a data update does not set that flag — only real touch
* gestures and `animate*` calls do — so data-driven index shifts can
* never poison the cached value. When [firstItemKey] changes (head of
* the list moved), if the cached value is still true, snap back to 0
* with an instant (non-animated) scroll so the prepend appears as
* in-place growth rather than a visible jump-then-scroll.
*
* Most callers should not invoke this directly: [SaveableFeedContentState],
* [SaveableGridFeedContentState], and the analogous wrappers in
* `ui/screen/FeedView.kt` already apply auto-stick to every feed they
* own. Invoke the explicit overload only when the listState is
* constructed outside one of those wrappers, or when the key that
* should trigger the snap is not the default `items.list[0].idHex`
* (e.g. notifications, chats, or feeds keyed on something other than a
* Note's hex id).
*/
@Composable
fun StickToTopOnPrepend(
listState: LazyListState,
firstItemKey: Any?,
) {
stickToTopOnPrepend(
stateKey = listState,
firstItemKey = firstItemKey,
initialAtTop = {
listState.firstVisibleItemIndex == 0 && listState.firstVisibleItemScrollOffset == 0
},
sampler = {
snapshotFlow {
listState.firstVisibleItemIndex == 0 && listState.firstVisibleItemScrollOffset == 0
}
},
isScrollInProgress = { listState.isScrollInProgress },
firstVisibleItemIndex = { listState.firstVisibleItemIndex },
scrollToTop = { listState.scrollToItem(0) },
)
}
@Composable
fun StickToTopOnPrepend(
gridState: LazyGridState,
firstItemKey: Any?,
) {
stickToTopOnPrepend(
stateKey = gridState,
firstItemKey = firstItemKey,
initialAtTop = {
gridState.firstVisibleItemIndex == 0 && gridState.firstVisibleItemScrollOffset == 0
},
sampler = {
snapshotFlow {
gridState.firstVisibleItemIndex == 0 && gridState.firstVisibleItemScrollOffset == 0
}
},
isScrollInProgress = { gridState.isScrollInProgress },
firstVisibleItemIndex = { gridState.firstVisibleItemIndex },
scrollToTop = { gridState.scrollToItem(0) },
)
}
/**
* Auto-stick wired straight to a [FeedContentState]: derives the head
* key from `feedContent → Loaded.feed → list.firstOrNull()?.idHex` so
* callers don't have to collect the inner feed flow themselves. Used
* by the Saveable* wrappers; suitable for any Note-keyed feed.
*/
@Composable
fun StickToTopOnPrepend(
feedContentState: FeedContentState,
listState: LazyListState,
) {
StickToTopOnPrepend(listState, rememberFirstItemIdHex(feedContentState))
}
@Composable
fun StickToTopOnPrepend(
feedContentState: FeedContentState,
gridState: LazyGridState,
) {
StickToTopOnPrepend(gridState, rememberFirstItemIdHex(feedContentState))
}
@OptIn(ExperimentalCoroutinesApi::class)
@Composable
private fun rememberFirstItemIdHex(feedContentState: FeedContentState): String? {
val flow =
remember(feedContentState) {
feedContentState.feedContent.flatMapLatest { state ->
when (state) {
is FeedState.Loaded -> state.feed.map { it.list.firstOrNull()?.idHex }
else -> flowOf(null)
}
}
}
val key by flow.collectAsStateWithLifecycle(initialValue = null)
return key
}
@Composable
private fun stickToTopOnPrepend(
stateKey: Any,
firstItemKey: Any?,
initialAtTop: () -> Boolean,
sampler: () -> Flow<Boolean>,
isScrollInProgress: () -> Boolean,
firstVisibleItemIndex: () -> Int,
scrollToTop: suspend () -> Unit,
) {
// Plain holder instead of mutableStateOf — we only read this inside
// effects, never in composition, so we don't need snapshot tracking.
// Seed from the actual restored scroll position: when the user returns
// to a feed via rememberForeverLazyListState, the saved offset is
// already in place, and a hardcoded `true` would mis-snap them to 0.
val wasAtTop = remember(stateKey) { booleanArrayOf(initialAtTop()) }
LaunchedEffect(stateKey) {
sampler().collect { atTop ->
if (atTop) {
wasAtTop[0] = true
} else if (isScrollInProgress()) {
wasAtTop[0] = false
}
}
}
LaunchedEffect(firstItemKey) {
if (firstItemKey != null && wasAtTop[0] && firstVisibleItemIndex() > 0) {
scrollToTop()
}
}
}
@@ -20,16 +20,10 @@
*/
package com.vitorpamplona.amethyst.ui.layouts
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.ListItem
import androidx.compose.material3.Text
@@ -38,20 +32,19 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.profile_banner
import com.vitorpamplona.amethyst.commons.ui.components.NewItemsBubble
import com.vitorpamplona.amethyst.commons.ui.layouts.ChatHeaderLayout
import com.vitorpamplona.amethyst.commons.ui.layouts.listItem.SlimListItem
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.commons.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.commons.ui.theme.Height4dpModifier
import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.ui.layouts.listItem.SlimListItem
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo
import com.vitorpamplona.amethyst.ui.painterRes
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.NewItemsBubble
import com.vitorpamplona.quartz.utils.TimeUtils
private const val PREVIEW_AUTHOR = "This is my author"
@@ -136,43 +129,3 @@ fun ChannelNamePreview() {
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ChatHeaderLayout(
channelPicture: @Composable () -> Unit,
firstRow: @Composable RowScope.() -> Unit,
secondRow: @Composable RowScope.() -> Unit,
onClick: () -> Unit,
onLongClick: (() -> Unit)? = null,
) {
Row(
modifier =
Modifier
.combinedClickable(onClick = onClick, onLongClick = onLongClick)
.padding(10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Box(Size55Modifier) { channelPicture() }
Spacer(modifier = DoubleHorzSpacer)
Column(
modifier = Modifier.fillMaxWidth(),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
firstRow()
}
Spacer(modifier = Height4dpModifier)
Row(
verticalAlignment = Alignment.CenterVertically,
) {
secondRow()
}
}
}
}
@@ -21,18 +21,11 @@
package com.vitorpamplona.amethyst.ui.layouts
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
@@ -43,10 +36,10 @@ import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.profile_banner
import com.vitorpamplona.amethyst.commons.ui.layouts.LeftPictureLayout
import com.vitorpamplona.amethyst.commons.ui.note.LikeIcon
import com.vitorpamplona.amethyst.commons.ui.note.ZappedIcon
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.commons.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.commons.ui.theme.Size16Modifier
import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier
@@ -104,47 +97,3 @@ fun LeftPictureLayoutPreviewCard() {
onBottomRow = { Text("This is my Moderator List") },
)
}
@Composable
fun LeftPictureLayout(
onImage: @Composable () -> Unit,
onTitleRow: @Composable RowScope.() -> Unit,
onDescription: @Composable () -> Unit,
onBottomRow: @Composable RowScope.() -> Unit,
imageFraction: Float = 0.25f,
) {
Row(Modifier.aspectRatio(ratio = 1 / imageFraction)) {
Column(
modifier = Modifier.fillMaxWidth(imageFraction).aspectRatio(ratio = 1f),
) {
onImage()
}
Spacer(modifier = DoubleHorzSpacer)
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.SpaceBetween,
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
onTitleRow()
}
Row(
modifier = Modifier.fillMaxWidth().weight(1f),
) {
onDescription()
}
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
onBottomRow()
}
}
}
}
@@ -0,0 +1,132 @@
/*
* 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.listItem
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.ListItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.tooling.preview.Preview
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.profile_banner
import com.vitorpamplona.amethyst.commons.ui.components.NewItemsBubble
import com.vitorpamplona.amethyst.commons.ui.layouts.ChatHeaderLayout
import com.vitorpamplona.amethyst.commons.ui.layouts.listItem.SlimListItem
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.commons.ui.theme.Height4dpModifier
import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo
import com.vitorpamplona.amethyst.ui.painterRes
import com.vitorpamplona.quartz.utils.TimeUtils
private const val PREVIEW_AUTHOR = "This is my author"
private const val PREVIEW_MESSAGE = "This is a message from this person"
@Composable
@Preview
fun ChannelNamePreview() {
ThemeComparisonColumn {
Column {
ChatHeaderLayout(
channelPicture = {
Image(
painter = painterRes(R.drawable.github, 1),
contentDescription = stringRes(id = Res.string.profile_banner),
contentScale = ContentScale.FillWidth,
)
},
firstRow = {
Text(PREVIEW_AUTHOR, Modifier.weight(1f))
TimeAgo(TimeUtils.now())
},
secondRow = {
Text(PREVIEW_MESSAGE, Modifier.weight(1f))
NewItemsBubble()
},
onClick = {},
)
HorizontalDivider(thickness = DividerThickness)
SlimListItem(
headlineContent = {
Row(verticalAlignment = CenterVertically) {
Text(PREVIEW_AUTHOR, Modifier.weight(1f))
TimeAgo(TimeUtils.now())
}
},
supportingContent = {
Row(verticalAlignment = CenterVertically) {
Text(PREVIEW_MESSAGE, Modifier.weight(1f))
Spacer(modifier = Height4dpModifier)
NewItemsBubble()
}
},
leadingContent = {
Image(
painter = painterRes(R.drawable.github, 2),
contentDescription = stringRes(id = Res.string.profile_banner),
contentScale = ContentScale.FillWidth,
modifier = Size55Modifier,
)
},
)
HorizontalDivider(thickness = DividerThickness)
ListItem(
headlineContent = {
Row(verticalAlignment = CenterVertically) {
Text(PREVIEW_AUTHOR, Modifier.weight(1f))
TimeAgo(TimeUtils.now())
}
},
supportingContent = {
Row(verticalAlignment = CenterVertically) {
Text(PREVIEW_MESSAGE, Modifier.weight(1f))
Spacer(modifier = Height4dpModifier)
NewItemsBubble()
}
},
leadingContent = {
Image(
painter = painterRes(R.drawable.github, 2),
contentDescription = stringRes(id = Res.string.profile_banner),
contentScale = ContentScale.FillWidth,
modifier = Size55Modifier,
)
},
)
HorizontalDivider(thickness = DividerThickness)
}
}
}
@@ -43,6 +43,7 @@ import com.vitorpamplona.amethyst.commons.resources.follow
import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_follow
import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_unfollow
import com.vitorpamplona.amethyst.commons.resources.read_only_user
import com.vitorpamplona.amethyst.commons.ui.layouts.listItem.SlimListItem
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.ShowUserButton
import com.vitorpamplona.amethyst.commons.ui.theme.Size55dp
@@ -51,7 +52,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.observeAccountIsHiddenUser
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserAboutMe
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowing
import com.vitorpamplona.amethyst.ui.layouts.listItem.SlimListItem
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -88,6 +88,7 @@ import com.vitorpamplona.amethyst.commons.resources.discover_follows
import com.vitorpamplona.amethyst.commons.resources.follow_sets
import com.vitorpamplona.amethyst.commons.resources.num_selected
import com.vitorpamplona.amethyst.commons.resources.select_all
import com.vitorpamplona.amethyst.commons.ui.components.OutlinedThinPaddingTextField
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
@@ -95,7 +96,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size24dp
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
import com.vitorpamplona.amethyst.commons.ui.theme.warningColor
import com.vitorpamplona.amethyst.ui.components.OutlinedThinPaddingTextField
import com.vitorpamplona.amethyst.ui.note.BaseUserPicture
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList
@@ -53,6 +53,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.Nip05State
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.user_suggestion_in_this_chat
import com.vitorpamplona.amethyst.commons.ui.layouts.listItem.SlimListItem
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.commons.ui.theme.Font14SP
@@ -60,7 +61,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.NIP05IconSize
import com.vitorpamplona.amethyst.commons.ui.theme.Size55dp
import com.vitorpamplona.amethyst.commons.ui.theme.nip05
import com.vitorpamplona.amethyst.service.relayClient.searchCommand.UserSearchDataSourceSubscription
import com.vitorpamplona.amethyst.ui.layouts.listItem.SlimListItem
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
import com.vitorpamplona.amethyst.ui.note.ObserveAndRenderNIP05VerifiedSymbol
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
@@ -45,6 +45,7 @@ import com.vitorpamplona.amethyst.commons.resources.zap_split_explainer
import com.vitorpamplona.amethyst.commons.resources.zap_split_search_and_add_user
import com.vitorpamplona.amethyst.commons.resources.zap_split_search_and_add_user_placeholder
import com.vitorpamplona.amethyst.commons.resources.zap_split_title
import com.vitorpamplona.amethyst.commons.ui.components.OutlinedThinPaddingTextField
import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.IZapField
import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ZapSplitIcon
import com.vitorpamplona.amethyst.commons.ui.stringRes
@@ -55,7 +56,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size55dp
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.actions.MentionPreservingInputTransformation
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation
import com.vitorpamplona.amethyst.ui.components.OutlinedThinPaddingTextField
import com.vitorpamplona.amethyst.ui.note.BaseUserPicture
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -35,14 +35,14 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.FeedEmpty
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedError
import com.vitorpamplona.amethyst.commons.ui.feeds.LoadingFeed
import com.vitorpamplona.amethyst.commons.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.commons.ui.feeds.StickToTopOnPrepend
import com.vitorpamplona.amethyst.commons.ui.feeds.WatchScrollToTop
import com.vitorpamplona.amethyst.commons.ui.feeds.rememberForeverLazyGridState
import com.vitorpamplona.amethyst.commons.ui.feeds.rememberForeverLazyListState
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.viewmodels.FeedViewModel
import com.vitorpamplona.amethyst.ui.feeds.FeedLoaded
import com.vitorpamplona.amethyst.ui.feeds.PrefetchLoadedFeedMedia
import com.vitorpamplona.amethyst.ui.feeds.StickToTopOnPrepend
import com.vitorpamplona.amethyst.ui.feeds.WatchScrollToTop
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
@@ -35,11 +35,11 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.FeedEmpty
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedError
import com.vitorpamplona.amethyst.commons.ui.feeds.LoadingFeed
import com.vitorpamplona.amethyst.commons.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.commons.ui.feeds.StickToTopOnPrepend
import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.feeds.StickToTopOnPrepend
import com.vitorpamplona.amethyst.ui.note.UserCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -22,13 +22,13 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn
import android.content.ComponentCallbacks2
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.commons.feeds.ChannelFeedContentState
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.feeds.ChannelFeedContentState
import com.vitorpamplona.amethyst.ui.screen.TopNavFilterState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.articles.dal.ArticlesFeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.badges.dal.BadgesFeedFilter
@@ -64,13 +64,13 @@ import com.vitorpamplona.amethyst.commons.resources.calendar_export_event
import com.vitorpamplona.amethyst.commons.resources.calendar_export_share_title
import com.vitorpamplona.amethyst.commons.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.commons.ui.feeds.ScrollStateKeys
import com.vitorpamplona.amethyst.commons.ui.feeds.WatchScrollToTop
import com.vitorpamplona.amethyst.commons.ui.feeds.rememberForeverLazyListState
import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.feeds.WatchScrollToTop
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.UserCardHeader
import com.vitorpamplona.quartz.nip01Core.core.Address
@@ -47,11 +47,11 @@ import com.vitorpamplona.amethyst.commons.resources.calendar_empty_feed_title
import com.vitorpamplona.amethyst.commons.resources.calendar_section_past
import com.vitorpamplona.amethyst.commons.resources.calendar_section_upcoming
import com.vitorpamplona.amethyst.commons.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.commons.ui.feeds.WatchScrollToTop
import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.feeds.WatchScrollToTop
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.utils.TimeUtils
@@ -106,7 +106,9 @@ import com.vitorpamplona.amethyst.commons.resources.relay_group_server_label
import com.vitorpamplona.amethyst.commons.resources.remove_from_messages
import com.vitorpamplona.amethyst.commons.resources.unmute_notifications
import com.vitorpamplona.amethyst.commons.resources.unpin_conversation
import com.vitorpamplona.amethyst.commons.ui.components.NewItemsBubble
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.commons.ui.layouts.ChatHeaderLayout
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.HeaderPill
import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings
@@ -118,7 +120,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size15Modifier
import com.vitorpamplona.amethyst.commons.ui.theme.Size55dp
import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
import com.vitorpamplona.amethyst.commons.ui.theme.newItemBubbleModifier
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
import com.vitorpamplona.amethyst.model.buzz.toMembershipNotice
import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo
@@ -126,7 +127,6 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observe
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteHasEvent
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderByParentFilterAssemblerSubscription
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName
import com.vitorpamplona.amethyst.ui.layouts.ChatHeaderLayout
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.ui.note.BlankNote
import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContentOrNull
@@ -1377,8 +1377,3 @@ private fun TimeAgo(channelLastTime: Long?) {
color = MaterialTheme.colorScheme.grayText,
)
}
@Composable
fun NewItemsBubble() {
Box(MaterialTheme.colorScheme.newItemBubbleModifier)
}
@@ -53,6 +53,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.Mute
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.chat_about_topic
import com.vitorpamplona.amethyst.commons.ui.layouts.LeftPictureLayout
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.HalfTopPadding
@@ -61,7 +62,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.RowColSpacing
import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel
import com.vitorpamplona.amethyst.ui.layouts.LeftPictureLayout
import com.vitorpamplona.amethyst.ui.note.DisplayAuthorBanner
import com.vitorpamplona.amethyst.ui.note.Gallery
import com.vitorpamplona.amethyst.ui.note.LikeReaction
@@ -55,6 +55,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.Mute
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.community_about_topic
import com.vitorpamplona.amethyst.commons.ui.layouts.LeftPictureLayout
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.HalfTopPadding
@@ -63,7 +64,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.RowColSpacing
import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent
import com.vitorpamplona.amethyst.ui.layouts.LeftPictureLayout
import com.vitorpamplona.amethyst.ui.note.DisplayAuthorBanner
import com.vitorpamplona.amethyst.ui.note.Gallery
import com.vitorpamplona.amethyst.ui.note.LikeReaction
@@ -41,6 +41,7 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.commons.model.AddressableNote
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.ui.layouts.LeftPictureLayout
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.theme.HalfTopPadding
import com.vitorpamplona.amethyst.commons.ui.theme.RowColSpacing5dp
@@ -51,7 +52,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.grayText
import com.vitorpamplona.amethyst.commons.ui.theme.nip05
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.components.MyAsyncImage
import com.vitorpamplona.amethyst.ui.layouts.LeftPictureLayout
import com.vitorpamplona.amethyst.ui.note.LikeReaction
import com.vitorpamplona.amethyst.ui.note.ZapReaction
import com.vitorpamplona.amethyst.ui.note.elements.BannerImage
@@ -46,12 +46,12 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.FeedError
import com.vitorpamplona.amethyst.commons.ui.feeds.LoadingFeed
import com.vitorpamplona.amethyst.commons.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.commons.ui.feeds.ScrollStateKeys
import com.vitorpamplona.amethyst.commons.ui.feeds.StickToTopOnPrepend
import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.commons.ui.feeds.WatchScrollToTop
import com.vitorpamplona.amethyst.commons.ui.feeds.rememberForeverLazyGridState
import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.feeds.StickToTopOnPrepend
import com.vitorpamplona.amethyst.ui.feeds.WatchScrollToTop
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -61,6 +61,8 @@ import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.commons.feeds.ChannelFeedContentState
import com.vitorpamplona.amethyst.commons.feeds.ChannelFeedState
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.feeds.FeedState
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
@@ -96,8 +98,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.commons.ui.theme.TabRowHeight
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow
import com.vitorpamplona.amethyst.service.location.LocationState
import com.vitorpamplona.amethyst.ui.feeds.ChannelFeedContentState
import com.vitorpamplona.amethyst.ui.feeds.ChannelFeedState
import com.vitorpamplona.amethyst.ui.feeds.RenderFeedContentState
import com.vitorpamplona.amethyst.ui.feeds.SaveableFeedContentState
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
@@ -60,6 +60,7 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.LoadingFeed
import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachCursor
import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachDetailDialog
import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachMarkers
import com.vitorpamplona.amethyst.commons.ui.feeds.StickToTopOnPrepend
import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.CloseIcon
@@ -73,7 +74,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.commons.ui.theme.imageModifier
import com.vitorpamplona.amethyst.logTime
import com.vitorpamplona.amethyst.ui.feeds.StickToTopOnPrepend
import com.vitorpamplona.amethyst.ui.note.BadgeCompose
import com.vitorpamplona.amethyst.ui.note.MessageSetCompose
import com.vitorpamplona.amethyst.ui.note.MultiSetCompose
@@ -40,6 +40,7 @@ import com.vitorpamplona.amethyst.commons.resources.notification_tab_following
import com.vitorpamplona.amethyst.commons.ui.feeds.PagerStateKeys
import com.vitorpamplona.amethyst.commons.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.commons.ui.feeds.ScrollStateKeys
import com.vitorpamplona.amethyst.commons.ui.feeds.WatchScrollToTop
import com.vitorpamplona.amethyst.commons.ui.feeds.rememberForeverLazyListState
import com.vitorpamplona.amethyst.commons.ui.feeds.rememberForeverPagerState
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
@@ -40,12 +40,12 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.FeedEmpty
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedError
import com.vitorpamplona.amethyst.commons.ui.feeds.LoadingFeed
import com.vitorpamplona.amethyst.commons.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.commons.ui.feeds.StickToTopOnPrepend
import com.vitorpamplona.amethyst.commons.ui.feeds.WatchScrollToTop
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.feeds.PrefetchLoadedFeedMedia
import com.vitorpamplona.amethyst.ui.feeds.StickToTopOnPrepend
import com.vitorpamplona.amethyst.ui.feeds.WatchScrollToTop
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.newthreads.dal.UserProfileNewThreadsFeedViewModel
@@ -35,12 +35,12 @@ import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.ui.layouts.listItem.SlimListItem
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.commons.ui.theme.Size55dp
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.ui.layouts.listItem.SlimListItem
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.ui.note.UserActionOptions
import com.vitorpamplona.amethyst.ui.note.UserPicture
@@ -122,6 +122,7 @@ import com.vitorpamplona.amethyst.commons.resources.relay_management_tab_pubkeys
import com.vitorpamplona.amethyst.commons.resources.relay_management_tab_settings
import com.vitorpamplona.amethyst.commons.resources.relay_management_title
import com.vitorpamplona.amethyst.commons.resources.search_and_add_a_user
import com.vitorpamplona.amethyst.commons.ui.layouts.listItem.SlimListItem
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.ClearTextIcon
import com.vitorpamplona.amethyst.commons.ui.stringRes
@@ -133,7 +134,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SmallBorder
import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.commons.ui.theme.nip05
import com.vitorpamplona.amethyst.service.relayClient.searchCommand.UserSearchDataSourceSubscription
import com.vitorpamplona.amethyst.ui.layouts.listItem.SlimListItem
import com.vitorpamplona.amethyst.ui.note.AboutDisplay
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
import com.vitorpamplona.amethyst.ui.note.ObserveAndRenderNIP05VerifiedSymbol
@@ -18,26 +18,24 @@
* 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.feeds
package com.vitorpamplona.amethyst.commons.feeds
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import com.vitorpamplona.amethyst.commons.feeds.AdditiveComplexFeedFilter
import com.vitorpamplona.amethyst.commons.feeds.InvalidatableContent
import com.vitorpamplona.amethyst.commons.feeds.LoadedFeedState
import com.vitorpamplona.amethyst.commons.model.Channel
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.service.BundledInsert
import com.vitorpamplona.amethyst.commons.service.BundledUpdate
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
import com.vitorpamplona.amethyst.commons.util.equalImmutableLists
import com.vitorpamplona.quartz.utils.Log
import com.vitorpamplona.quartz.utils.flattenToSet
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
@@ -75,7 +73,7 @@ class ChannelFeedContentState(
}
fun refreshSuspended() {
checkNotInMainThread()
LocalCache.appHost.assertNotMainThread()
isRefreshing.value = true
try {
@@ -178,7 +176,7 @@ class ChannelFeedContentState(
}
fun destroy() {
Log.d("Init") { "OnCleared: ${this.javaClass.simpleName}" }
Log.d("Init") { "OnCleared: ${this::class.simpleName}" }
bundlerInsert.cancel()
bundler.cancel()
}
@@ -18,10 +18,9 @@
* 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.feeds
package com.vitorpamplona.amethyst.commons.feeds
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.commons.feeds.LoadedFeedState
import com.vitorpamplona.amethyst.commons.model.Channel
import kotlinx.coroutines.flow.MutableStateFlow
@@ -18,7 +18,7 @@
* 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.components
package com.vitorpamplona.amethyst.commons.ui.components
import androidx.compose.foundation.combinedClickable
import androidx.compose.material3.MaterialTheme
@@ -18,7 +18,7 @@
* 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.components
package com.vitorpamplona.amethyst.commons.ui.components
import androidx.compose.foundation.combinedClickable
import androidx.compose.material3.MaterialTheme
@@ -91,3 +91,27 @@ fun ClickableEmail(
},
)
}
/**
* A primary-colored clickable phone number that opens the platform dialer via
* [LocalUriHandler] (`tel:`). Strips a leading `tel:` from the display text.
* [underline] + hover cursor are the Desktop mouse-first affordances.
*/
@Composable
fun ClickablePhone(
number: String,
modifier: Modifier = Modifier,
underline: Boolean = false,
) {
val uriHandler = LocalUriHandler.current
val display = remember(number) { number.removePrefix("tel:") }
Text(
text = display,
color = MaterialTheme.colorScheme.primary,
textDecoration = if (underline) TextDecoration.Underline else null,
modifier =
modifier.pointerHoverIcon(PointerIcon.Hand).clickable {
runCatching { uriHandler.openUri("tel:$display") }
},
)
}
@@ -18,7 +18,7 @@
* 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.components
package com.vitorpamplona.amethyst.commons.ui.components
import androidx.compose.foundation.combinedClickable
import androidx.compose.material3.MaterialTheme
@@ -18,7 +18,7 @@
* 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.components
package com.vitorpamplona.amethyst.commons.ui.components
import androidx.compose.foundation.combinedClickable
import androidx.compose.material3.MaterialTheme
@@ -18,30 +18,15 @@
* 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.components
package com.vitorpamplona.amethyst.commons.ui.components
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import com.vitorpamplona.amethyst.commons.ui.components.ClickableTextPrimary
import com.vitorpamplona.amethyst.commons.ui.theme.newItemBubbleModifier
/** The small dot that marks a row with unread items. */
@Composable
fun ClickablePhone(phone: String) {
val context = LocalContext.current
ClickableTextPrimary(
text = phone,
onClick = { context.dial(phone) },
)
}
fun Context.dial(phone: String) {
try {
val intent = Intent(Intent.ACTION_DIAL, Uri.fromParts("tel", phone, null))
startActivity(intent)
} catch (t: Throwable) {
// TODO: Handle potential exceptions
}
fun NewItemsBubble() {
Box(MaterialTheme.colorScheme.newItemBubbleModifier)
}
@@ -18,7 +18,7 @@
* 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.components
package com.vitorpamplona.amethyst.commons.ui.components
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -24,10 +24,10 @@ import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.feeds.FeedState
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -59,11 +59,14 @@ import kotlinx.coroutines.flow.map
* with an instant (non-animated) scroll so the prepend appears as
* in-place growth rather than a visible jump-then-scroll.
*
* Commons port of `amethyst/.../WatchScrollToTop.kt` so Desktop and any
* other multiplatform front-end can use the same auto-stick behavior.
* Uses plain `collectAsState` instead of the Android-only
* `collectAsStateWithLifecycle` — equivalent here because the effect's
* lifecycle is already bound to composition via `LaunchedEffect`.
* Most callers should not invoke this directly: `SaveableFeedContentState`,
* `SaveableGridFeedContentState`, and the analogous wrappers in the app's
* `ui/screen/FeedView.kt` already apply auto-stick to every feed they
* own. Invoke the explicit overload only when the listState is
* constructed outside one of those wrappers, or when the key that
* should trigger the snap is not the default `items.list[0].idHex`
* (e.g. notifications, chats, or feeds keyed on something other than a
* Note's hex id).
*/
@Composable
fun StickToTopOnPrepend(
@@ -142,7 +145,7 @@ private fun rememberFirstItemIdHex(feedContentState: FeedContentState): String?
}
}
}
val key by flow.collectAsState(initial = null)
val key by flow.collectAsStateWithLifecycle(initialValue = null)
return key
}
@@ -0,0 +1,75 @@
/*
* 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.commons.ui.feeds
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.pager.PagerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
@Composable
fun WatchScrollToTop(
feedContentState: FeedContentState,
listState: LazyListState,
) {
val scrollToTop by feedContentState.scrollToTop.collectAsStateWithLifecycle()
LaunchedEffect(scrollToTop) {
if (scrollToTop > 0 && feedContentState.scrollToTopPending) {
listState.scrollToItem(index = 0)
feedContentState.sentToTop()
}
}
}
@Composable
fun WatchScrollToTop(
feedContentState: FeedContentState,
listState: LazyGridState,
) {
val scrollToTop by feedContentState.scrollToTop.collectAsStateWithLifecycle()
LaunchedEffect(scrollToTop) {
if (scrollToTop > 0 && feedContentState.scrollToTopPending) {
listState.scrollToItem(index = 0)
feedContentState.sentToTop()
}
}
}
@Composable
fun WatchScrollToTop(
videoFeedContentState: FeedContentState,
pagerState: PagerState,
) {
val scrollToTop by videoFeedContentState.scrollToTop.collectAsStateWithLifecycle()
LaunchedEffect(scrollToTop) {
if (scrollToTop > 0 && videoFeedContentState.scrollToTopPending) {
pagerState.scrollToPage(page = 0)
videoFeedContentState.sentToTop()
}
}
}
@@ -0,0 +1,78 @@
/*
* 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.commons.ui.layouts
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.commons.ui.theme.Height4dpModifier
import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ChatHeaderLayout(
channelPicture: @Composable () -> Unit,
firstRow: @Composable RowScope.() -> Unit,
secondRow: @Composable RowScope.() -> Unit,
onClick: () -> Unit,
onLongClick: (() -> Unit)? = null,
) {
Row(
modifier =
Modifier
.combinedClickable(onClick = onClick, onLongClick = onLongClick)
.padding(10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Box(Size55Modifier) { channelPicture() }
Spacer(modifier = DoubleHorzSpacer)
Column(
modifier = Modifier.fillMaxWidth(),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
firstRow()
}
Spacer(modifier = Height4dpModifier)
Row(
verticalAlignment = Alignment.CenterVertically,
) {
secondRow()
}
}
}
}
@@ -0,0 +1,77 @@
/*
* 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.commons.ui.layouts
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.vitorpamplona.amethyst.commons.ui.theme.DoubleHorzSpacer
@Composable
fun LeftPictureLayout(
onImage: @Composable () -> Unit,
onTitleRow: @Composable RowScope.() -> Unit,
onDescription: @Composable () -> Unit,
onBottomRow: @Composable RowScope.() -> Unit,
imageFraction: Float = 0.25f,
) {
Row(Modifier.aspectRatio(ratio = 1 / imageFraction)) {
Column(
modifier = Modifier.fillMaxWidth(imageFraction).aspectRatio(ratio = 1f),
) {
onImage()
}
Spacer(modifier = DoubleHorzSpacer)
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.SpaceBetween,
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
onTitleRow()
}
Row(
modifier = Modifier.fillMaxWidth().weight(1f),
) {
onDescription()
}
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
onBottomRow()
}
}
}
}
@@ -18,31 +18,22 @@
* 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.listItem
package com.vitorpamplona.amethyst.commons.ui.layouts.listItem
import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemColors
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.FirstBaseline
import androidx.compose.ui.layout.IntrinsicMeasurable
import androidx.compose.ui.layout.IntrinsicMeasureScope
@@ -55,117 +46,20 @@ import androidx.compose.ui.layout.MultiContentMeasurePolicy
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.offset
import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.profile_banner
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.commons.ui.theme.Height4dpModifier
import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.ui.layouts.ChatHeaderLayout
import com.vitorpamplona.amethyst.ui.layouts.listItem.ListTokens.ListItemContainerElevation
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo
import com.vitorpamplona.amethyst.ui.painterRes
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.NewItemsBubble
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.amethyst.commons.ui.layouts.listItem.ListTokens.ListItemContainerElevation
import kotlin.jvm.JvmInline
import kotlin.math.max
/**
* This is a copy of Material3's ListItemLayout.kt file, with the only change being the padding change from 16.dp to 10.dp
*/
private const val PREVIEW_AUTHOR = "This is my author"
private const val PREVIEW_MESSAGE = "This is a message from this person"
@Composable
@Preview
fun ChannelNamePreview() {
ThemeComparisonColumn {
Column {
ChatHeaderLayout(
channelPicture = {
Image(
painter = painterRes(R.drawable.github, 1),
contentDescription = stringRes(id = Res.string.profile_banner),
contentScale = ContentScale.FillWidth,
)
},
firstRow = {
Text(PREVIEW_AUTHOR, Modifier.weight(1f))
TimeAgo(TimeUtils.now())
},
secondRow = {
Text(PREVIEW_MESSAGE, Modifier.weight(1f))
NewItemsBubble()
},
onClick = {},
)
HorizontalDivider(thickness = DividerThickness)
SlimListItem(
headlineContent = {
Row(verticalAlignment = CenterVertically) {
Text(PREVIEW_AUTHOR, Modifier.weight(1f))
TimeAgo(TimeUtils.now())
}
},
supportingContent = {
Row(verticalAlignment = CenterVertically) {
Text(PREVIEW_MESSAGE, Modifier.weight(1f))
Spacer(modifier = Height4dpModifier)
NewItemsBubble()
}
},
leadingContent = {
Image(
painter = painterRes(R.drawable.github, 2),
contentDescription = stringRes(id = Res.string.profile_banner),
contentScale = ContentScale.FillWidth,
modifier = Size55Modifier,
)
},
)
HorizontalDivider(thickness = DividerThickness)
ListItem(
headlineContent = {
Row(verticalAlignment = CenterVertically) {
Text(PREVIEW_AUTHOR, Modifier.weight(1f))
TimeAgo(TimeUtils.now())
}
},
supportingContent = {
Row(verticalAlignment = CenterVertically) {
Text(PREVIEW_MESSAGE, Modifier.weight(1f))
Spacer(modifier = Height4dpModifier)
NewItemsBubble()
}
},
leadingContent = {
Image(
painter = painterRes(R.drawable.github, 2),
contentDescription = stringRes(id = Res.string.profile_banner),
contentScale = ContentScale.FillWidth,
modifier = Size55Modifier,
)
},
)
HorizontalDivider(thickness = DividerThickness)
}
}
}
@Composable
fun SlimListItem(
headlineContent: @Composable () -> Unit,
@@ -688,22 +582,22 @@ object ListTokens {
// Container related defaults
// TODO: Make sure these values stay up to date until replaced with tokens.
@VisibleForTesting internal val ListItemVerticalPadding = 8.dp
internal val ListItemVerticalPadding = 8.dp
@VisibleForTesting internal val ListItemThreeLineVerticalPadding = 12.dp
internal val ListItemThreeLineVerticalPadding = 12.dp
@VisibleForTesting internal val ListItemStartPadding = 10.dp
internal val ListItemStartPadding = 10.dp
@VisibleForTesting internal val ListItemEndPadding = 10.dp
internal val ListItemEndPadding = 10.dp
// Icon related defaults.
// TODO: Make sure these values stay up to date until replaced with tokens.
@VisibleForTesting internal val LeadingContentEndPadding = Modifier.padding(end = 10.dp)
internal val LeadingContentEndPadding = Modifier.padding(end = 10.dp)
// Icon related defaults.
// TODO: Make sure these values stay up to date until replaced with tokens.
@VisibleForTesting internal val SupportingContentTopPadding = Modifier.padding(top = 2.dp)
internal val SupportingContentTopPadding = Modifier.padding(top = 2.dp)
// Trailing related defaults.
// TODO: Make sure these values stay up to date until replaced with tokens.
@VisibleForTesting internal val TrailingContentStartPadding = Modifier.padding(start = 10.dp)
internal val TrailingContentStartPadding = Modifier.padding(start = 10.dp)