Merge branch 'main' into main

This commit is contained in:
greenart7c3 2024-03-22 08:48:47 -03:00 committed by GitHub
commit 62a114b981
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 507 additions and 508 deletions

View File

@ -22,9 +22,11 @@ package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.commons.data.LargeCache
import com.vitorpamplona.amethyst.service.NostrSingleChannelDataSource import com.vitorpamplona.amethyst.service.NostrSingleChannelDataSource
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.components.BundledUpdate import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
import com.vitorpamplona.amethyst.ui.note.toShortenHex import com.vitorpamplona.amethyst.ui.note.toShortenHex
import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.Hex import com.vitorpamplona.quartz.encoders.Hex
@ -33,7 +35,6 @@ import com.vitorpamplona.quartz.encoders.toNote
import com.vitorpamplona.quartz.events.ChannelCreateEvent import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.LiveActivitiesEvent import com.vitorpamplona.quartz.events.LiveActivitiesEvent
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import java.util.concurrent.ConcurrentHashMap
@Stable @Stable
class PublicChatChannel(idHex: String) : Channel(idHex) { class PublicChatChannel(idHex: String) : Channel(idHex) {
@ -110,7 +111,7 @@ abstract class Channel(val idHex: String) {
var updatedMetadataAt: Long = 0 var updatedMetadataAt: Long = 0
val notes = ConcurrentHashMap<HexKey, Note>() val notes = LargeCache<HexKey, Note>()
open fun id() = Hex.decode(idHex) open fun id() = Hex.decode(idHex)
@ -131,7 +132,7 @@ abstract class Channel(val idHex: String) {
} }
open fun profilePicture(): String? { open fun profilePicture(): String? {
return creator?.profilePicture() return creator?.info?.banner
} }
open fun updateChannelInfo( open fun updateChannelInfo(
@ -145,7 +146,7 @@ abstract class Channel(val idHex: String) {
} }
fun addNote(note: Note) { fun addNote(note: Note) {
notes[note.idHex] = note notes.put(note.idHex, note)
} }
fun removeNote(note: Note) { fun removeNote(note: Note) {
@ -163,18 +164,18 @@ abstract class Channel(val idHex: String) {
fun pruneOldAndHiddenMessages(account: Account): Set<Note> { fun pruneOldAndHiddenMessages(account: Account): Set<Note> {
val important = val important =
notes.values notes.filter { key, it ->
.filter { it.author?.let { it1 -> account.isHidden(it1) } == false } it.author?.let { author -> account.isHidden(author) } == false
.sortedWith(compareBy({ it.createdAt() }, { it.idHex })) }
.reversed() .sortedWith(DefaultFeedOrder)
.take(1000) .take(500)
.toSet() .toSet()
val toBeRemoved = notes.values.filter { it !in important }.toSet() val toBeRemoved = notes.filter { key, it -> it !in important }
toBeRemoved.forEach { notes.remove(it.idHex) } toBeRemoved.forEach { notes.remove(it.idHex) }
return toBeRemoved return toBeRemoved.toSet()
} }
} }

View File

@ -1809,9 +1809,9 @@ object LocalCache {
removeFromCache(childrenToBeRemoved) removeFromCache(childrenToBeRemoved)
if (toBeRemoved.size > 100 || it.value.notes.size > 100) { if (toBeRemoved.size > 100 || it.value.notes.size() > 100) {
println( println(
"PRUNE: ${toBeRemoved.size} messages removed from ${it.value.toBestDisplayName()}. ${it.value.notes.size} kept", "PRUNE: ${toBeRemoved.size} messages removed from ${it.value.toBestDisplayName()}. ${it.value.notes.size()} kept",
) )
} }
} }

View File

@ -171,7 +171,8 @@ open class Note(val idHex: String) {
event is LiveActivitiesEvent event is LiveActivitiesEvent
) { ) {
(event as? ChannelMessageEvent)?.channel() (event as? ChannelMessageEvent)?.channel()
?: (event as? ChannelMetadataEvent)?.channel() ?: (event as? ChannelCreateEvent)?.id ?: (event as? ChannelMetadataEvent)?.channel()
?: (event as? ChannelCreateEvent)?.id
?: (event as? LiveActivitiesChatMessageEvent)?.activity()?.toTag() ?: (event as? LiveActivitiesChatMessageEvent)?.activity()?.toTag()
?: (event as? LiveActivitiesEvent)?.address()?.toTag() ?: (event as? LiveActivitiesEvent)?.address()?.toTag()
} else { } else {

View File

@ -96,7 +96,7 @@ class ParticipantListBuilder {
it.replyTo?.forEach { addFollowsThatDirectlyParticipateOnToSet(it, followingSet, mySet) } it.replyTo?.forEach { addFollowsThatDirectlyParticipateOnToSet(it, followingSet, mySet) }
} }
LocalCache.getChannelIfExists(baseNote.idHex)?.notes?.values?.forEach { LocalCache.getChannelIfExists(baseNote.idHex)?.notes?.forEach { key, it ->
addFollowsThatDirectlyParticipateOnToSet(it, followingSet, mySet) addFollowsThatDirectlyParticipateOnToSet(it, followingSet, mySet)
} }

View File

@ -46,6 +46,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CurrencyBitcoin import androidx.compose.material.icons.filled.CurrencyBitcoin
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
@ -98,6 +99,7 @@ import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.theme.Size5dp
@ -448,6 +450,9 @@ fun ShowUserSuggestionListForEdit(
key = { _, item -> item.pubkeyHex }, key = { _, item -> item.pubkeyHex },
) { _, item -> ) { _, item ->
UserLine(item, accountViewModel) { editPostViewModel.autocompleteWithUser(item) } UserLine(item, accountViewModel) { editPostViewModel.autocompleteWithUser(item) }
HorizontalDivider(
thickness = DividerThickness,
)
} }
} }
} }

View File

@ -357,6 +357,10 @@ private fun RenderSearchResults(
searchBarViewModel.clear() searchBarViewModel.clear()
} }
HorizontalDivider(
thickness = DividerThickness,
)
} }
itemsIndexed( itemsIndexed(
@ -367,6 +371,10 @@ private fun RenderSearchResults(
nav("Channel/${item.idHex}") nav("Channel/${item.idHex}")
searchBarViewModel.clear() searchBarViewModel.clear()
} }
HorizontalDivider(
thickness = DividerThickness,
)
} }
} }
} }
@ -404,39 +412,30 @@ fun UserComposeForChat(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
Column( Row(
modifier = modifier =
Modifier.clickable( Modifier.clickable(
onClick = onClick, onClick = onClick,
).padding(
start = 12.dp,
end = 12.dp,
top = 10.dp,
bottom = 10.dp,
), ),
verticalAlignment = Alignment.CenterVertically,
) { ) {
Row( ClickableUserPicture(baseUser, Size55dp, accountViewModel)
Column(
modifier = modifier =
Modifier.padding( Modifier
start = 12.dp, .padding(start = 10.dp)
end = 12.dp, .weight(1f),
top = 10.dp,
),
verticalAlignment = Alignment.CenterVertically,
) { ) {
ClickableUserPicture(baseUser, Size55dp, accountViewModel) Row(verticalAlignment = Alignment.CenterVertically) { UsernameDisplay(baseUser) }
Column( DisplayUserAboutInfo(baseUser)
modifier =
Modifier
.padding(start = 10.dp)
.weight(1f),
) {
Row(verticalAlignment = Alignment.CenterVertically) { UsernameDisplay(baseUser) }
DisplayUserAboutInfo(baseUser)
}
} }
HorizontalDivider(
modifier = Modifier.padding(top = 10.dp),
thickness = DividerThickness,
)
} }
} }

View File

@ -294,7 +294,6 @@ private fun DisplayOwnerInformation(
UserCompose( UserCompose(
baseUser = it, baseUser = it,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
showDiviser = false,
nav = nav, nav = nav,
) )
} }

View File

@ -31,10 +31,11 @@ class ChannelFeedFilter(val channel: Channel, val account: Account) : AdditiveFe
// returns the last Note of each user. // returns the last Note of each user.
override fun feed(): List<Note> { override fun feed(): List<Note> {
return channel.notes.values return sort(
.filter { account.isAcceptable(it) } channel.notes.filterIntoSet { key, it ->
.sortedWith(compareBy({ it.createdAt() }, { it.idHex })) account.isAcceptable(it)
.reversed() },
)
} }
override fun applyFilter(collection: Set<Note>): Set<Note> { override fun applyFilter(collection: Set<Note>): Set<Note> {

View File

@ -56,15 +56,12 @@ class ChatroomListKnownFeedFilter(val account: Account) : AdditiveFeedFilter<Not
.selectedChatsFollowList() .selectedChatsFollowList()
.mapNotNull { LocalCache.getChannelIfExists(it) } .mapNotNull { LocalCache.getChannelIfExists(it) }
.mapNotNull { it -> .mapNotNull { it ->
it.notes.values it.notes.filter { key, it -> account.isAcceptable(it) && it.event != null }
.filter { account.isAcceptable(it) && it.event != null } .sortedWith(DefaultFeedOrder)
.sortedWith(compareBy({ it.createdAt() }, { it.idHex })) .firstOrNull()
.lastOrNull()
} }
return (privateMessages + publicChannels) return (privateMessages + publicChannels).sortedWith(DefaultFeedOrder)
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
.reversed()
} }
override fun updateListWith( override fun updateListWith(

View File

@ -48,9 +48,8 @@ open class DiscoverLiveFeedFilter(
} }
override fun feed(): List<Note> { override fun feed(): List<Note> {
val allChannelNotes = val allChannelNotes = LocalCache.channels.values.mapNotNull { LocalCache.getNoteIfExists(it.idHex) }
LocalCache.channels.values.mapNotNull { LocalCache.getNoteIfExists(it.idHex) } val allMessageNotes = LocalCache.channels.values.map { it.notes.filter { key, it -> it.event is LiveActivitiesEvent } }.flatten()
val allMessageNotes = LocalCache.channels.values.map { it.notes.values }.flatten()
val notes = innerApplyFilter(allChannelNotes + allMessageNotes) val notes = innerApplyFilter(allChannelNotes + allMessageNotes)

View File

@ -34,6 +34,7 @@ import com.vitorpamplona.quartz.events.GiftWrapEvent
import com.vitorpamplona.quartz.events.GitIssueEvent import com.vitorpamplona.quartz.events.GitIssueEvent
import com.vitorpamplona.quartz.events.GitPatchEvent import com.vitorpamplona.quartz.events.GitPatchEvent
import com.vitorpamplona.quartz.events.HighlightEvent import com.vitorpamplona.quartz.events.HighlightEvent
import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.LnZapRequestEvent import com.vitorpamplona.quartz.events.LnZapRequestEvent
import com.vitorpamplona.quartz.events.MuteListEvent import com.vitorpamplona.quartz.events.MuteListEvent
import com.vitorpamplona.quartz.events.PeopleListEvent import com.vitorpamplona.quartz.events.PeopleListEvent
@ -87,6 +88,7 @@ class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
filterParams: FilterByListParams, filterParams: FilterByListParams,
): Boolean { ): Boolean {
val loggedInUserHex = account.userProfile().pubkeyHex val loggedInUserHex = account.userProfile().pubkeyHex
val loggedInUser = account.userProfile()
return it.event !is ChannelCreateEvent && return it.event !is ChannelCreateEvent &&
it.event !is ChannelMetadataEvent && it.event !is ChannelMetadataEvent &&
@ -94,6 +96,7 @@ class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
it.event !is BadgeDefinitionEvent && it.event !is BadgeDefinitionEvent &&
it.event !is BadgeProfilesEvent && it.event !is BadgeProfilesEvent &&
it.event !is GiftWrapEvent && it.event !is GiftWrapEvent &&
(it.event is LnZapEvent || it.author !== loggedInUser) &&
(filterParams.isGlobal || filterParams.followLists?.users?.contains(it.author?.pubkeyHex) == true) && (filterParams.isGlobal || filterParams.followLists?.users?.contains(it.author?.pubkeyHex) == true) &&
it.event?.isTaggedUser(loggedInUserHex) ?: false && it.event?.isTaggedUser(loggedInUserHex) ?: false &&
(filterParams.isHiddenList || it.author == null || !account.isHidden(it.author!!.pubkeyHex)) && (filterParams.isHiddenList || it.author == null || !account.isHidden(it.author!!.pubkeyHex)) &&

View File

@ -21,9 +21,11 @@
package com.vitorpamplona.amethyst.ui.dal package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.quartz.events.AddressableEvent
import com.vitorpamplona.quartz.events.AudioHeaderEvent import com.vitorpamplona.quartz.events.AudioHeaderEvent
import com.vitorpamplona.quartz.events.AudioTrackEvent import com.vitorpamplona.quartz.events.AudioTrackEvent
import com.vitorpamplona.quartz.events.ClassifiedsEvent import com.vitorpamplona.quartz.events.ClassifiedsEvent
@ -43,7 +45,7 @@ class UserProfileNewThreadFeedFilter(val user: User, val account: Account) :
override fun feed(): List<Note> { override fun feed(): List<Note> {
val notes = val notes =
LocalCache.notes.filterIntoSet { _, it -> LocalCache.notes.filterIntoSet { _, it ->
acceptableEvent(it) it !is AddressableNote && it.event !is AddressableEvent && acceptableEvent(it)
} }
val longFormNotes = val longFormNotes =

View File

@ -48,6 +48,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment.Companion.BottomStart import androidx.compose.ui.Alignment.Companion.BottomStart
import androidx.compose.ui.Alignment.Companion.Center
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Alignment.Companion.TopEnd import androidx.compose.ui.Alignment.Companion.TopEnd
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@ -68,6 +70,7 @@ import com.vitorpamplona.amethyst.model.ParticipantListBuilder
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
import com.vitorpamplona.amethyst.ui.layouts.LeftPictureLayout import com.vitorpamplona.amethyst.ui.layouts.LeftPictureLayout
import com.vitorpamplona.amethyst.ui.note.elements.BannerImage
import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
@ -197,7 +200,7 @@ fun InnerChannelCardWithReactions(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
when (remember { baseNote.event }) { when (baseNote.event) {
is LiveActivitiesEvent -> { is LiveActivitiesEvent -> {
InnerCardRow(baseNote, accountViewModel, nav) InnerCardRow(baseNote, accountViewModel, nav)
} }
@ -255,7 +258,7 @@ private fun RenderNoteRow(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
when (remember { baseNote.event }) { when (baseNote.event) {
is LiveActivitiesEvent -> { is LiveActivitiesEvent -> {
RenderLiveActivityThumb(baseNote, accountViewModel, nav) RenderLiveActivityThumb(baseNote, accountViewModel, nav)
} }
@ -305,29 +308,29 @@ fun RenderClassifiedsThumb(
), ),
) )
RenderClassifiedsThumb(card, baseNote.author) InnerRenderClassifiedsThumb(card, baseNote)
} }
@Preview @Preview
@Composable @Composable
fun RenderClassifiedsThumbPreview() { fun RenderClassifiedsThumbPreview() {
Surface(Modifier.size(200.dp)) { Surface(Modifier.size(200.dp)) {
RenderClassifiedsThumb( InnerRenderClassifiedsThumb(
card = card =
ClassifiedsThumb( ClassifiedsThumb(
image = null, image = null,
title = "Like New", title = "Like New",
price = Price("800000", "SATS", null), price = Price("800000", "SATS", null),
), ),
author = null, note = Note("hex"),
) )
} }
} }
@Composable @Composable
fun RenderClassifiedsThumb( fun InnerRenderClassifiedsThumb(
card: ClassifiedsThumb, card: ClassifiedsThumb,
author: User?, note: Note,
) { ) {
Box( Box(
Modifier.fillMaxWidth().aspectRatio(1f), Modifier.fillMaxWidth().aspectRatio(1f),
@ -340,8 +343,7 @@ fun RenderClassifiedsThumb(
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) )
} } ?: run { DisplayAuthorBanner(note) }
?: run { author?.let { DisplayAuthorBanner(it) } }
Row( Row(
Modifier.fillMaxWidth().background(Color.Black.copy(0.6f)).padding(Size5dp), Modifier.fillMaxWidth().background(Color.Black.copy(0.6f)).padding(Size5dp),
@ -449,8 +451,7 @@ fun RenderLiveActivityThumb(
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize().clip(QuoteBorder), modifier = Modifier.fillMaxSize().clip(QuoteBorder),
) )
} } ?: run { DisplayAuthorBanner(baseNote) }
?: run { baseNote.author?.let { DisplayAuthorBanner(it) } }
Box(Modifier.padding(10.dp)) { Box(Modifier.padding(10.dp)) {
Crossfade(targetState = card.status, label = "RenderLiveActivityThumb") { Crossfade(targetState = card.status, label = "RenderLiveActivityThumb") {
@ -496,12 +497,11 @@ fun RenderLiveActivityThumb(
Spacer(modifier = DoubleVertSpacer) Spacer(modifier = DoubleVertSpacer)
ChannelHeader( ChannelHeader(
channelHex = remember { baseNote.idHex }, channelHex = baseNote.idHex,
showVideo = false, showVideo = false,
showBottomDiviser = false,
showFlag = false, showFlag = false,
sendToChannel = true, sendToChannel = true,
modifier = remember { Modifier.padding(start = 0.dp, end = 0.dp, top = 5.dp, bottom = 5.dp) }, modifier = Modifier,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, nav = nav,
) )
@ -559,8 +559,7 @@ fun RenderCommunitiesThumb(
modifier = Modifier.fillMaxSize().clip(QuoteBorder), modifier = Modifier.fillMaxSize().clip(QuoteBorder),
) )
} }
} } ?: run { DisplayAuthorBanner(baseNote) }
?: run { baseNote.author?.let { DisplayAuthorBanner(it) } }
}, },
onTitleRow = { onTitleRow = {
Text( Text(
@ -780,16 +779,13 @@ fun RenderChannelThumb(
LeftPictureLayout( LeftPictureLayout(
onImage = { onImage = {
cover?.let { cover?.let {
Box(contentAlignment = BottomStart) { AsyncImage(
AsyncImage( model = it,
model = it, contentDescription = null,
contentDescription = null, contentScale = ContentScale.Crop,
contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize().clip(QuoteBorder),
modifier = Modifier.fillMaxSize().clip(QuoteBorder), )
) } ?: run { DisplayAuthorBanner(baseNote) }
}
}
?: run { baseNote.author?.let { DisplayAuthorBanner(it) } }
}, },
onTitleRow = { onTitleRow = {
Text( Text(
@ -829,7 +825,7 @@ fun RenderChannelThumb(
onBottomRow = { onBottomRow = {
if (participantUsers.isNotEmpty()) { if (participantUsers.isNotEmpty()) {
Spacer(modifier = StdVertSpacer) Spacer(modifier = StdVertSpacer)
Row { Gallery(participantUsers, accountViewModel) } Gallery(participantUsers, accountViewModel)
} }
}, },
) )
@ -846,27 +842,20 @@ fun Gallery(
if (users.size > 6) { if (users.size > 6) {
Text( Text(
text = remember(users) { " + " + (showCount(users.size - 6)) }, text = " + " + showCount(users.size - 6),
fontSize = 13.sp, fontSize = 13.sp,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.align(CenterVertically),
) )
} }
} }
} }
@Composable @Composable
fun DisplayAuthorBanner(author: User) { fun DisplayAuthorBanner(note: Note) {
val picture by val authorState by note.live().authorChanges.observeAsState(note.author)
author
.live()
.metadata
.map { it.user.info?.banner?.ifBlank { null } ?: it.user.info?.picture?.ifBlank { null } }
.observeAsState()
AsyncImage( authorState?.let { author ->
model = picture, BannerImage(author, Modifier.fillMaxSize().clip(QuoteBorder))
contentDescription = null, }
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize().clip(QuoteBorder),
)
} }

View File

@ -28,6 +28,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
@ -110,6 +111,7 @@ fun ChatroomMessageCompose(
WatchBlockAndReport( WatchBlockAndReport(
note = baseNote, note = baseNote,
showHiddenWarning = innerQuote, showHiddenWarning = innerQuote,
modifier = Modifier.fillMaxWidth(),
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, nav = nav,
) { canPreview -> ) { canPreview ->

View File

@ -239,7 +239,6 @@ fun AcceptableNote(
ChannelHeader( ChannelHeader(
channelNote = baseNote, channelNote = baseNote,
showVideo = !makeItShort, showVideo = !makeItShort,
showBottomDiviser = true,
sendToChannel = true, sendToChannel = true,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, nav = nav,
@ -273,7 +272,6 @@ fun AcceptableNote(
ChannelHeader( ChannelHeader(
channelNote = baseNote, channelNote = baseNote,
showVideo = !makeItShort, showVideo = !makeItShort,
showBottomDiviser = true,
sendToChannel = true, sendToChannel = true,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, nav = nav,
@ -692,7 +690,7 @@ fun RenderRepost(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
note.replyTo?.lastOrNull()?.let { note.replyTo?.lastOrNull { it.event !is CommunityDefinitionEvent }?.let {
NoteCompose( NoteCompose(
it, it,
modifier = Modifier, modifier = Modifier,
@ -748,23 +746,25 @@ private fun ReplyRow(
ChannelHeader( ChannelHeader(
channelHex = it, channelHex = it,
showVideo = false, showVideo = false,
showBottomDiviser = false,
sendToChannel = true, sendToChannel = true,
modifier = MaterialTheme.colorScheme.replyModifier.padding(10.dp), modifier = MaterialTheme.colorScheme.replyModifier.padding(10.dp),
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, nav = nav,
) )
Spacer(modifier = StdVertSpacer)
} }
if (showReply) { if (showReply) {
val replyingDirectlyTo = val replyingDirectlyTo =
remember(note) { remember(note) {
if (noteEvent is BaseTextNoteEvent) { if (noteEvent is BaseTextNoteEvent) {
val replyingTo = noteEvent.replyingTo() val replyingTo = noteEvent.replyingToAddressOrEvent()
if (replyingTo != null) { if (replyingTo != null) {
note.replyTo?.firstOrNull { val newNote = accountViewModel.getNoteIfExists(replyingTo)
// important to test both ids in case it's a replaceable event. if (newNote != null && newNote.channelHex() == null && newNote.event?.kind() != CommunityDefinitionEvent.KIND) {
it.idHex == replyingTo || it.event?.id() == replyingTo newNote
} else {
note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.KIND }
} }
} else { } else {
note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.KIND } note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.KIND }

View File

@ -120,7 +120,7 @@ val externalLinkForNote = { note: Note ->
} else if (note.event is AudioTrackEvent) { } else if (note.event is AudioTrackEvent) {
"https://zapstr.live/?track=${note.address()?.toNAddr()}" "https://zapstr.live/?track=${note.address()?.toNAddr()}"
} else { } else {
"https://habla.news/a/${note.address()?.toNAddr()}" "https://njump.me/${note.address()?.toNAddr()}"
} }
} else { } else {
if (note.event is FileHeaderEvent) { if (note.event is FileHeaderEvent) {

View File

@ -20,13 +20,13 @@
*/ */
package com.vitorpamplona.amethyst.ui.note package com.vitorpamplona.amethyst.ui.note
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -47,7 +47,7 @@ import com.vitorpamplona.amethyst.model.RelayInfo
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.ButtonPadding import com.vitorpamplona.amethyst.ui.theme.ButtonPadding
import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.StdPadding
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import java.time.Instant import java.time.Instant
import java.time.ZoneId import java.time.ZoneId
@ -62,49 +62,44 @@ fun RelayCompose(
) { ) {
val context = LocalContext.current val context = LocalContext.current
Column { Row(
Row( modifier = StdPadding,
modifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 10.dp), verticalAlignment = Alignment.CenterVertically,
) {
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.Center,
) { ) {
Column( Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
modifier = Modifier.padding(start = 10.dp).weight(1f),
) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
Text(
relay.url.trim().removePrefix("wss://"),
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
val lastTime by
remember(relay.lastEvent) {
derivedStateOf { timeAgo(relay.lastEvent, context = context) }
}
Text(
text = lastTime,
maxLines = 1,
)
}
Text( Text(
"${relay.counter} ${stringResource(R.string.posts_received)}", relay.url.trim().removePrefix("wss://"),
color = MaterialTheme.colorScheme.placeholderText, fontWeight = FontWeight.Bold,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
val lastTime by
remember(relay.lastEvent) {
derivedStateOf { timeAgo(relay.lastEvent, context = context) }
}
Text(
text = lastTime,
maxLines = 1,
)
} }
Column(modifier = Modifier.padding(start = 10.dp)) { Text(
RelayOptions(accountViewModel, relay, onAddRelay, onRemoveRelay) "${relay.counter} ${stringResource(R.string.posts_received)}",
} color = MaterialTheme.colorScheme.placeholderText,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
} }
HorizontalDivider( Column(modifier = Modifier.padding(start = 10.dp)) {
modifier = Modifier.padding(top = 10.dp), RelayOptions(accountViewModel, relay, onAddRelay, onRemoveRelay)
thickness = DividerThickness, }
)
} }
} }

View File

@ -24,7 +24,6 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -32,7 +31,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.amethyst.ui.theme.Size55dp
import com.vitorpamplona.amethyst.ui.theme.StdPadding import com.vitorpamplona.amethyst.ui.theme.StdPadding
@ -40,37 +38,26 @@ import com.vitorpamplona.amethyst.ui.theme.StdPadding
fun UserCompose( fun UserCompose(
baseUser: User, baseUser: User,
overallModifier: Modifier = StdPadding, overallModifier: Modifier = StdPadding,
showDiviser: Boolean = true,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
Column( Row(
modifier = modifier =
Modifier.clickable( overallModifier.clickable(
onClick = { nav("User/${baseUser.pubkeyHex}") }, onClick = { nav("User/${baseUser.pubkeyHex}") },
), ),
verticalAlignment = Alignment.CenterVertically,
) { ) {
Row( UserPicture(baseUser, Size55dp, accountViewModel = accountViewModel, nav = nav)
modifier = overallModifier,
verticalAlignment = Alignment.CenterVertically,
) {
UserPicture(baseUser, Size55dp, accountViewModel = accountViewModel, nav = nav)
Column(modifier = remember { Modifier.padding(start = 10.dp).weight(1f) }) { Column(modifier = remember { Modifier.padding(start = 10.dp).weight(1f) }) {
Row(verticalAlignment = Alignment.CenterVertically) { UsernameDisplay(baseUser) } Row(verticalAlignment = Alignment.CenterVertically) { UsernameDisplay(baseUser) }
AboutDisplay(baseUser) AboutDisplay(baseUser)
}
Column(modifier = remember { Modifier.padding(start = 10.dp) }) {
UserActionOptions(baseUser, accountViewModel)
}
} }
if (showDiviser) { Column(modifier = remember { Modifier.padding(start = 10.dp) }) {
HorizontalDivider( UserActionOptions(baseUser, accountViewModel)
thickness = DividerThickness,
)
} }
} }
} }

View File

@ -23,48 +23,67 @@ package com.vitorpamplona.amethyst.ui.note.elements
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.note.BaseUserPicture
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.amethyst.ui.theme.Size55dp
import com.vitorpamplona.amethyst.ui.theme.authorNotePictureForImageHeader import com.vitorpamplona.amethyst.ui.theme.authorNotePictureForImageHeader
import com.vitorpamplona.amethyst.ui.theme.imageHeaderBannerSize
@Composable @Composable
fun DefaultImageHeader( fun DefaultImageHeader(
note: Note, note: Note,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
) { ) {
Box { val authorState by note.live().authorChanges.observeAsState(note.author)
note.author?.info?.banner?.let {
AsyncImage(
model = it,
contentDescription =
stringResource(
R.string.preview_card_image_for,
it,
),
contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth(),
)
}
?: Image(
painter = painterResource(R.drawable.profile_banner),
contentDescription = stringResource(R.string.profile_banner),
contentScale = ContentScale.FillWidth,
modifier = imageHeaderBannerSize,
)
Box(authorNotePictureForImageHeader.align(Alignment.BottomStart)) { authorState?.let { author ->
NoteAuthorPicture(baseNote = note, accountViewModel = accountViewModel, size = Size55dp) Box {
BannerImage(author)
Box(authorNotePictureForImageHeader.align(Alignment.BottomStart)) {
BaseUserPicture(author, Size55dp, accountViewModel, Modifier)
}
} }
} }
} }
@Composable
fun BannerImage(
author: User,
imageModifier: Modifier = Modifier.fillMaxWidth().heightIn(max = 200.dp),
) {
val currentInfo by author.live().userMetadataInfo.observeAsState()
currentInfo?.banner?.let {
AsyncImage(
model = it,
contentDescription =
stringResource(
R.string.preview_card_image_for,
it,
),
contentScale = ContentScale.Crop,
modifier = imageModifier,
placeholder = painterResource(R.drawable.profile_banner),
)
} ?: run {
Image(
painter = painterResource(R.drawable.profile_banner),
contentDescription = stringResource(R.string.profile_banner),
contentScale = ContentScale.Crop,
modifier = imageModifier,
)
}
}

View File

@ -88,8 +88,9 @@ fun RenderClassifieds(
contentScale = ContentScale.FillWidth, contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
} ?: run {
DefaultImageHeader(note, accountViewModel)
} }
?: DefaultImageHeader(note, accountViewModel)
} }
Row( Row(

View File

@ -31,7 +31,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -68,7 +67,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.JoinCommunityButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.JoinCommunityButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LeaveCommunityButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.LeaveCommunityButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.NormalTimeAgo import com.vitorpamplona.amethyst.ui.screen.loggedIn.NormalTimeAgo
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier
import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size10dp
@ -88,7 +86,6 @@ import java.util.Locale
@Composable @Composable
fun CommunityHeader( fun CommunityHeader(
baseNote: AddressableNote, baseNote: AddressableNote,
showBottomDiviser: Boolean,
sendToCommunity: Boolean, sendToCommunity: Boolean,
modifier: Modifier = StdPadding, modifier: Modifier = StdPadding,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
@ -125,12 +122,6 @@ fun CommunityHeader(
} }
} }
} }
if (showBottomDiviser) {
HorizontalDivider(
thickness = DividerThickness,
)
}
} }
} }

View File

@ -127,8 +127,9 @@ private fun WikiNoteHeader(
contentScale = ContentScale.FillWidth, contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
} ?: run {
DefaultImageHeader(note, accountViewModel)
} }
?: DefaultImageHeader(note, accountViewModel)
} }
title?.let { title?.let {

View File

@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.note.types
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -65,14 +64,14 @@ private fun LongFormHeader(
note: Note, note: Note,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
) { ) {
val image = remember(noteEvent) { noteEvent.image() } val image = noteEvent.image()
val title = remember(noteEvent) { noteEvent.title() } val title = noteEvent.title()
val summary = val summary =
remember(noteEvent) { remember(noteEvent) {
noteEvent.summary()?.ifBlank { null } ?: noteEvent.content.take(200).ifBlank { null } noteEvent.summary()?.ifBlank { null } ?: noteEvent.content.take(200).ifBlank { null }
} }
Row( Column(
modifier = modifier =
Modifier Modifier
.padding(top = Size5dp) .padding(top = Size5dp)
@ -83,51 +82,50 @@ private fun LongFormHeader(
QuoteBorder, QuoteBorder,
), ),
) { ) {
Column { val automaticallyShowUrlPreview =
val automaticallyShowUrlPreview = remember { accountViewModel.settings.showImages.value }
remember { accountViewModel.settings.showUrlPreview.value }
if (automaticallyShowUrlPreview) { if (automaticallyShowUrlPreview) {
image?.let { image?.let {
AsyncImage( AsyncImage(
model = it, model = it,
contentDescription = contentDescription =
stringResource( stringResource(
R.string.preview_card_image_for, R.string.preview_card_image_for,
it, it,
), ),
contentScale = ContentScale.FillWidth, contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
)
}
?: DefaultImageHeader(note, accountViewModel)
}
title?.let {
Text(
text = it,
style = MaterialTheme.typography.bodyLarge,
modifier =
Modifier
.fillMaxWidth()
.padding(start = 10.dp, end = 10.dp, top = 10.dp),
) )
} ?: run {
DefaultImageHeader(note, accountViewModel)
} }
}
summary?.let { title?.let {
Spacer(modifier = StdVertSpacer) Text(
Text( text = it,
text = it, style = MaterialTheme.typography.bodyLarge,
style = MaterialTheme.typography.bodySmall, modifier =
modifier = Modifier
Modifier .fillMaxWidth()
.fillMaxWidth() .padding(start = 10.dp, end = 10.dp, top = 10.dp),
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp), )
color = Color.Gray, }
maxLines = 3,
overflow = TextOverflow.Ellipsis, summary?.let {
) Spacer(modifier = StdVertSpacer)
} Text(
text = it,
style = MaterialTheme.typography.bodySmall,
modifier =
Modifier
.fillMaxWidth()
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp),
color = Color.Gray,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
)
} }
} }
} }

View File

@ -23,11 +23,13 @@ package com.vitorpamplona.amethyst.ui.note.types
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@ -50,6 +52,7 @@ import com.vitorpamplona.amethyst.ui.components.ShowMoreButton
import com.vitorpamplona.amethyst.ui.note.UserCompose import com.vitorpamplona.amethyst.ui.note.UserCompose
import com.vitorpamplona.amethyst.ui.note.getGradient import com.vitorpamplona.amethyst.ui.note.getGradient
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.quartz.events.PeopleListEvent import com.vitorpamplona.quartz.events.PeopleListEvent
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -98,13 +101,16 @@ fun DisplayPeopleList(
Box { Box {
FlowRow(modifier = Modifier.padding(top = 5.dp)) { FlowRow(modifier = Modifier.padding(top = 5.dp)) {
toMembersShow.forEach { user -> toMembersShow.forEach { user ->
Row(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.fillMaxWidth()) {
UserCompose( UserCompose(
user, user,
overallModifier = Modifier,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, nav = nav,
) )
HorizontalDivider(
thickness = DividerThickness,
)
} }
} }
} }

View File

@ -60,7 +60,6 @@ fun RenderPostApproval(
baseNote?.let { baseNote?.let {
CommunityHeader( CommunityHeader(
baseNote = it, baseNote = it,
showBottomDiviser = false,
sendToCommunity = true, sendToCommunity = true,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, nav = nav,

View File

@ -135,8 +135,9 @@ fun VideoDisplay(
contentScale = ContentScale.FillWidth, contentScale = ContentScale.FillWidth,
modifier = MaterialTheme.colorScheme.imageModifier, modifier = MaterialTheme.colorScheme.imageModifier,
) )
} ?: run {
DefaultImageHeader(note, accountViewModel)
} }
?: DefaultImageHeader(note, accountViewModel)
} }
} else { } else {
ZoomableContentView( ZoomableContentView(

View File

@ -37,7 +37,10 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.ViewModelStore import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
@ -120,6 +123,7 @@ fun LoggedInPage(
accountViewModel.serviceManager = activity.serviceManager accountViewModel.serviceManager = activity.serviceManager
if (accountViewModel.account.signer is NostrSignerExternal) { if (accountViewModel.account.signer is NostrSignerExternal) {
val lifeCycleOwner = LocalLifecycleOwner.current
val launcher = val launcher =
rememberLauncherForActivityResult( rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult(), contract = ActivityResultContracts.StartActivityForResult(),
@ -137,7 +141,30 @@ fun LoggedInPage(
}, },
) )
DisposableEffect(accountViewModel, accountViewModel.account, launcher, activity) { DisposableEffect(accountViewModel, accountViewModel.account, launcher, activity, lifeCycleOwner) {
val observer =
LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
accountViewModel.account.signer.launcher.registerLauncher(
launcher = {
try {
activity.prepareToLaunchSigner()
launcher.launch(it)
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e("Signer", "Error opening Signer app", e)
accountViewModel.toast(
R.string.error_opening_external_signer,
R.string.error_opening_external_signer_description,
)
}
},
contentResolver = { Amethyst.instance.contentResolver },
)
}
}
lifeCycleOwner.lifecycle.addObserver(observer)
accountViewModel.account.signer.launcher.registerLauncher( accountViewModel.account.signer.launcher.registerLauncher(
launcher = { launcher = {
try { try {
@ -154,7 +181,11 @@ fun LoggedInPage(
}, },
contentResolver = { Amethyst.instance.contentResolver }, contentResolver = { Amethyst.instance.contentResolver },
) )
onDispose { accountViewModel.account.signer.launcher.clearLauncher() } onDispose {
Log.d("onDispose", "Called onDispose")
accountViewModel.account.signer.launcher.clearLauncher()
lifeCycleOwner.lifecycle.removeObserver(observer)
}
} }
} }

View File

@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.pullrefresh.PullRefreshIndicator import androidx.compose.material3.pullrefresh.PullRefreshIndicator
import androidx.compose.material3.pullrefresh.pullRefresh import androidx.compose.material3.pullrefresh.pullRefresh
import androidx.compose.material3.pullrefresh.rememberPullRefreshState import androidx.compose.material3.pullrefresh.rememberPullRefreshState
@ -47,6 +48,7 @@ import com.vitorpamplona.amethyst.ui.actions.NewRelayListView
import com.vitorpamplona.amethyst.ui.components.BundledUpdate import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import com.vitorpamplona.amethyst.ui.note.RelayCompose import com.vitorpamplona.amethyst.ui.note.RelayCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -169,6 +171,9 @@ fun RelayFeedView(
onAddRelay = { wantsToAddRelay = item.url }, onAddRelay = { wantsToAddRelay = item.url },
onRemoveRelay = { wantsToAddRelay = item.url }, onRemoveRelay = { wantsToAddRelay = item.url },
) )
HorizontalDivider(
thickness = DividerThickness,
)
} }
} }

View File

@ -28,6 +28,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -37,17 +38,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.FeedPadding
@Composable
fun RefreshingFeedStringFeedView(
viewModel: StringFeedViewModel,
enablePullRefresh: Boolean = true,
inner: @Composable (String) -> Unit,
) {
RefresheableBox(viewModel, enablePullRefresh) { StringFeedView(viewModel, inner = inner) }
}
@Composable @Composable
fun StringFeedView( fun StringFeedView(
viewModel: StringFeedViewModel, viewModel: StringFeedViewModel,
@ -112,7 +105,13 @@ private fun FeedLoaded(
) { ) {
item { pre?.let { it() } } item { pre?.let { it() } }
itemsIndexed(state.feed.value, key = { _, item -> item }) { _, item -> inner(item) } itemsIndexed(state.feed.value, key = { _, item -> item }) { _, item ->
inner(item)
HorizontalDivider(
thickness = DividerThickness,
)
}
item { post?.let { it() } } item { post?.let { it() } }
} }

View File

@ -20,13 +20,10 @@
*/ */
package com.vitorpamplona.amethyst.ui.screen package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@ -35,6 +32,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
@ -49,9 +47,6 @@ import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.pullrefresh.PullRefreshIndicator
import androidx.compose.material3.pullrefresh.pullRefresh
import androidx.compose.material3.pullrefresh.rememberPullRefreshState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
@ -80,7 +75,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
@ -148,7 +142,6 @@ import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
import com.vitorpamplona.amethyst.ui.theme.Size24Modifier import com.vitorpamplona.amethyst.ui.theme.Size24Modifier
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdTopPadding
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
@ -196,117 +189,100 @@ fun ThreadFeedView(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
val feedState by viewModel.feedContent.collectAsStateWithLifecycle()
val listState = rememberLazyListState() val listState = rememberLazyListState()
var refreshing by remember { mutableStateOf(false) } RefresheableBox(viewModel) {
val refresh = { RenderFeedState(
refreshing = true viewModel = viewModel,
viewModel.invalidateData() accountViewModel = accountViewModel,
refreshing = false listState = listState,
nav = nav,
routeForLastRead = null,
onLoaded = {
RenderThreadFeed(noteId, it, listState, accountViewModel, nav)
},
)
} }
val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh) }
Box(Modifier.pullRefresh(pullRefreshState)) { @Composable
Column { fun RenderThreadFeed(
Crossfade( noteId: String,
targetState = feedState, state: FeedState.Loaded,
animationSpec = tween(durationMillis = 100), listState: LazyListState,
label = "ThreadViewMainState", accountViewModel: AccountViewModel,
) { state -> nav: (String) -> Unit,
when (state) { ) {
is FeedState.Empty -> { LaunchedEffect(noteId) {
FeedEmpty { refreshing = true } // waits to load the thread to scroll to item.
} delay(100)
is FeedState.FeedError -> { val noteForPosition = state.feed.value.filter { it.idHex == noteId }.firstOrNull()
FeedError(state.errorMessage) { refreshing = true } var position = state.feed.value.indexOf(noteForPosition)
}
is FeedState.Loaded -> {
refreshing = false
LaunchedEffect(noteId) {
launch(Dispatchers.IO) {
// waits to load the thread to scroll to item.
delay(100)
val noteForPosition = state.feed.value.filter { it.idHex == noteId }.firstOrNull()
var position = state.feed.value.indexOf(noteForPosition)
if (position >= 0) { if (position >= 0) {
if (position >= 1 && position < state.feed.value.size - 1) { if (position >= 1 && position < state.feed.value.size - 1) {
position-- // show the replying note position-- // show the replying note
}
withContext(Dispatchers.Main) { listState.scrollToItem(position) }
}
}
}
LazyColumn(
contentPadding = FeedPadding,
state = listState,
) {
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { index, item ->
if (index == 0) {
ProvideTextStyle(TextStyle(fontSize = 18.sp, lineHeight = 1.20.em)) {
NoteMaster(
item,
modifier =
Modifier.drawReplyLevel(
item.replyLevel(),
MaterialTheme.colorScheme.placeholderText,
if (item.idHex == noteId) {
MaterialTheme.colorScheme.lessImportantLink
} else {
MaterialTheme.colorScheme.placeholderText
},
),
accountViewModel = accountViewModel,
nav = nav,
)
}
} else {
val selectedNoteColor = MaterialTheme.colorScheme.selectedNote
val background =
remember {
if (item.idHex == noteId) mutableStateOf(selectedNoteColor) else null
}
NoteCompose(
item,
modifier =
Modifier.drawReplyLevel(
item.replyLevel(),
MaterialTheme.colorScheme.placeholderText,
if (item.idHex == noteId) {
MaterialTheme.colorScheme.lessImportantLink
} else {
MaterialTheme.colorScheme.placeholderText
},
),
parentBackgroundColor = background,
isBoostedNote = false,
unPackReply = false,
quotesLeft = 3,
accountViewModel = accountViewModel,
nav = nav,
)
}
HorizontalDivider(
modifier = StdTopPadding,
thickness = DividerThickness,
)
}
}
}
FeedState.Loading -> {
LoadingFeed()
}
}
} }
}
PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter)) listState.scrollToItem(position)
}
}
LazyColumn(
contentPadding = FeedPadding,
state = listState,
) {
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { index, item ->
if (index == 0) {
ProvideTextStyle(TextStyle(fontSize = 18.sp, lineHeight = 1.20.em)) {
NoteMaster(
item,
modifier =
Modifier.drawReplyLevel(
item.replyLevel(),
MaterialTheme.colorScheme.placeholderText,
if (item.idHex == noteId) {
MaterialTheme.colorScheme.lessImportantLink
} else {
MaterialTheme.colorScheme.placeholderText
},
),
accountViewModel = accountViewModel,
nav = nav,
)
}
} else {
val selectedNoteColor = MaterialTheme.colorScheme.selectedNote
val background =
remember {
if (item.idHex == noteId) mutableStateOf(selectedNoteColor) else null
}
NoteCompose(
item,
modifier =
Modifier.drawReplyLevel(
item.replyLevel(),
MaterialTheme.colorScheme.placeholderText,
if (item.idHex == noteId) {
MaterialTheme.colorScheme.lessImportantLink
} else {
MaterialTheme.colorScheme.placeholderText
},
),
parentBackgroundColor = background,
isBoostedNote = false,
unPackReply = false,
quotesLeft = 3,
accountViewModel = accountViewModel,
nav = nav,
)
}
HorizontalDivider(
thickness = DividerThickness,
)
}
} }
} }
@ -509,7 +485,6 @@ fun NoteMaster(
ChannelHeader( ChannelHeader(
channelHex = note.channelHex()!!, channelHex = note.channelHex()!!,
showVideo = true, showVideo = true,
showBottomDiviser = false,
sendToChannel = true, sendToChannel = true,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, nav = nav,
@ -819,43 +794,41 @@ private fun RenderClassifiedsReaderForThread(
@Composable @Composable
private fun RenderLongFormHeaderForThread(noteEvent: LongTextNoteEvent) { private fun RenderLongFormHeaderForThread(noteEvent: LongTextNoteEvent) {
Row(modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 12.dp)) { Column(modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 12.dp)) {
Column { noteEvent.image()?.let {
noteEvent.image()?.let { AsyncImage(
AsyncImage( model = it,
model = it, contentDescription =
contentDescription = stringResource(
stringResource( R.string.preview_card_image_for,
R.string.preview_card_image_for, it,
it, ),
), contentScale = ContentScale.FillWidth,
contentScale = ContentScale.FillWidth, modifier = Modifier.fillMaxWidth(),
modifier = Modifier.fillMaxWidth(), )
) }
}
noteEvent.title()?.let { noteEvent.title()?.let {
Spacer(modifier = DoubleVertSpacer)
Text(
text = it,
fontSize = 28.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.fillMaxWidth(),
)
}
noteEvent
.summary()
?.ifBlank { null }
?.let {
Spacer(modifier = DoubleVertSpacer) Spacer(modifier = DoubleVertSpacer)
Text( Text(
text = it, text = it,
fontSize = 28.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
color = Color.Gray,
) )
} }
noteEvent
.summary()
?.ifBlank { null }
?.let {
Spacer(modifier = DoubleVertSpacer)
Text(
text = it,
modifier = Modifier.fillMaxWidth(),
color = Color.Gray,
)
}
}
} }
} }

View File

@ -25,11 +25,13 @@ import androidx.compose.animation.core.tween
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.ui.note.UserCompose import com.vitorpamplona.amethyst.ui.note.UserCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.FeedPadding
@Composable @Composable
@ -82,6 +84,9 @@ private fun FeedLoaded(
) { ) {
itemsIndexed(state.feed.value, key = { _, item -> item.pubkeyHex }) { _, item -> itemsIndexed(state.feed.value, key = { _, item -> item.pubkeyHex }) { _, item ->
UserCompose(item, accountViewModel = accountViewModel, nav = nav) UserCompose(item, accountViewModel = accountViewModel, nav = nav)
HorizontalDivider(
thickness = DividerThickness,
)
} }
} }
} }

View File

@ -53,7 +53,6 @@ import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
@ -139,7 +138,6 @@ import com.vitorpamplona.amethyst.ui.screen.RefreshingChatroomFeedView
import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.ButtonPadding import com.vitorpamplona.amethyst.ui.theme.ButtonPadding
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.EditFieldBorder import com.vitorpamplona.amethyst.ui.theme.EditFieldBorder
@ -583,7 +581,6 @@ fun MyTextField(
fun ChannelHeader( fun ChannelHeader(
channelNote: Note, channelNote: Note,
showVideo: Boolean, showVideo: Boolean,
showBottomDiviser: Boolean,
sendToChannel: Boolean, sendToChannel: Boolean,
modifier: Modifier = StdPadding, modifier: Modifier = StdPadding,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
@ -594,8 +591,8 @@ fun ChannelHeader(
ChannelHeader( ChannelHeader(
channelHex = it, channelHex = it,
showVideo = showVideo, showVideo = showVideo,
showBottomDiviser = showBottomDiviser,
sendToChannel = sendToChannel, sendToChannel = sendToChannel,
modifier = modifier,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, nav = nav,
) )
@ -606,7 +603,6 @@ fun ChannelHeader(
fun ChannelHeader( fun ChannelHeader(
channelHex: String, channelHex: String,
showVideo: Boolean, showVideo: Boolean,
showBottomDiviser: Boolean,
showFlag: Boolean = true, showFlag: Boolean = true,
sendToChannel: Boolean = false, sendToChannel: Boolean = false,
modifier: Modifier = StdPadding, modifier: Modifier = StdPadding,
@ -617,7 +613,6 @@ fun ChannelHeader(
ChannelHeader( ChannelHeader(
it, it,
showVideo, showVideo,
showBottomDiviser,
showFlag, showFlag,
sendToChannel, sendToChannel,
modifier, modifier,
@ -631,7 +626,6 @@ fun ChannelHeader(
fun ChannelHeader( fun ChannelHeader(
baseChannel: Channel, baseChannel: Channel,
showVideo: Boolean, showVideo: Boolean,
showBottomDiviser: Boolean,
showFlag: Boolean = true, showFlag: Boolean = true,
sendToChannel: Boolean = false, sendToChannel: Boolean = false,
modifier: Modifier = StdPadding, modifier: Modifier = StdPadding,
@ -667,12 +661,6 @@ fun ChannelHeader(
LongChannelHeader(baseChannel = baseChannel, accountViewModel = accountViewModel, nav = nav) LongChannelHeader(baseChannel = baseChannel, accountViewModel = accountViewModel, nav = nav)
} }
} }
if (showBottomDiviser) {
HorizontalDivider(
thickness = DividerThickness,
)
}
} }
} }

View File

@ -520,6 +520,9 @@ fun ShowUserSuggestionList(
key = { _, item -> item.pubkeyHex }, key = { _, item -> item.pubkeyHex },
) { _, item -> ) { _, item ->
UserLine(item, accountViewModel) { channelScreenModel.autocompleteWithUser(item) } UserLine(item, accountViewModel) { channelScreenModel.autocompleteWithUser(item) }
HorizontalDivider(
thickness = DividerThickness,
)
} }
} }
} }
@ -836,6 +839,9 @@ fun LongRoomHeader(
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, nav = nav,
) )
HorizontalDivider(
thickness = DividerThickness,
)
} }
} }
} }

View File

@ -27,7 +27,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
@ -47,7 +46,6 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.NostrHashtagDataSource import com.vitorpamplona.amethyst.service.NostrHashtagDataSource
import com.vitorpamplona.amethyst.ui.screen.NostrHashtagFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrHashtagFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.StdPadding import com.vitorpamplona.amethyst.ui.theme.StdPadding
@Composable @Composable
@ -142,27 +140,18 @@ fun HashtagHeader(
account: AccountViewModel, account: AccountViewModel,
onClick: () -> Unit = {}, onClick: () -> Unit = {},
) { ) {
Column( Row(
Modifier.fillMaxWidth().clickable { onClick() }, modifier = Modifier.fillMaxWidth().clickable { onClick() }.then(modifier),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) { ) {
Column(modifier = modifier) { Text(
Row( "#$tag",
verticalAlignment = Alignment.CenterVertically, fontWeight = FontWeight.Bold,
horizontalArrangement = Arrangement.Center, modifier = Modifier.weight(1f),
) {
Text(
"#$tag",
fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1f),
)
HashtagActionOptions(tag, account)
}
}
HorizontalDivider(
thickness = DividerThickness,
) )
HashtagActionOptions(tag, account)
} }
} }

View File

@ -312,10 +312,6 @@ fun MutedWordHeader(
MutedWordActionOptions(tag, account) MutedWordActionOptions(tag, account)
} }
} }
HorizontalDivider(
thickness = DividerThickness,
)
} }
} }

View File

@ -120,7 +120,9 @@ fun NotificationScreen(
SummaryBar( SummaryBar(
model = userReactionsStatsModel, model = userReactionsStatsModel,
) )
HorizontalDivider(
thickness = DividerThickness,
)
RefreshableCardView( RefreshableCardView(
viewModel = notifFeedViewModel, viewModel = notifFeedViewModel,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
@ -229,10 +231,6 @@ fun SummaryBar(model: UserReactionsViewModel) {
} }
} }
} }
HorizontalDivider(
thickness = DividerThickness,
)
} }
@Composable @Composable

View File

@ -1547,18 +1547,17 @@ fun TabFollowedTags(
account: AccountViewModel, account: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
Column(Modifier.fillMaxHeight()) { Column(Modifier.fillMaxHeight().padding(vertical = 0.dp)) {
Column( baseUser.latestContactList?.let {
modifier = Modifier.padding(vertical = 0.dp), it.unverifiedFollowTagSet().forEach { hashtag ->
) { HashtagHeader(
baseUser.latestContactList?.let { tag = hashtag,
it.unverifiedFollowTagSet().forEach { hashtag -> account = account,
HashtagHeader( onClick = { nav("Hashtag/$hashtag") },
tag = hashtag, )
account = account, HorizontalDivider(
onClick = { nav("Hashtag/$hashtag") }, thickness = DividerThickness,
) )
}
} }
} }
} }

View File

@ -374,6 +374,11 @@ private fun DisplaySearchResults(
key = { _, item -> "#$item" }, key = { _, item -> "#$item" },
) { _, item -> ) { _, item ->
HashtagLine(item) { nav("Hashtag/$item") } HashtagLine(item) { nav("Hashtag/$item") }
HorizontalDivider(
modifier = Modifier.padding(top = 10.dp),
thickness = DividerThickness,
)
} }
itemsIndexed( itemsIndexed(
@ -381,6 +386,10 @@ private fun DisplaySearchResults(
key = { _, item -> "u" + item.pubkeyHex }, key = { _, item -> "u" + item.pubkeyHex },
) { _, item -> ) { _, item ->
UserCompose(item, accountViewModel = accountViewModel, nav = nav) UserCompose(item, accountViewModel = accountViewModel, nav = nav)
HorizontalDivider(
thickness = DividerThickness,
)
} }
itemsIndexed( itemsIndexed(
@ -432,33 +441,24 @@ fun HashtagLine(
tag: String, tag: String,
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
Column( Row(
modifier = Modifier.fillMaxWidth().clickable(onClick = onClick), modifier =
Modifier.fillMaxWidth().clickable(onClick = onClick).padding(
start = 12.dp,
end = 12.dp,
top = 10.dp,
),
) { ) {
Row( Row(
modifier = verticalAlignment = Alignment.CenterVertically,
Modifier.padding( horizontalArrangement = Arrangement.Center,
start = 12.dp, modifier = Modifier.fillMaxWidth(),
end = 12.dp,
top = 10.dp,
),
) { ) {
Row( Text(
verticalAlignment = Alignment.CenterVertically, "Search hashtag: #$tag",
horizontalArrangement = Arrangement.Center, fontWeight = FontWeight.Bold,
modifier = Modifier.fillMaxWidth(), )
) {
Text(
"Search hashtag: #$tag",
fontWeight = FontWeight.Bold,
)
}
} }
HorizontalDivider(
modifier = Modifier.padding(top = 10.dp),
thickness = DividerThickness,
)
} }
} }
@ -468,31 +468,23 @@ fun UserLine(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
Column( Row(
modifier = Modifier.fillMaxWidth().clickable(onClick = onClick), modifier =
Modifier.fillMaxWidth().clickable(onClick = onClick).padding(
start = 12.dp,
end = 12.dp,
top = 10.dp,
bottom = 10.dp,
),
) { ) {
Row( ClickableUserPicture(baseUser, 55.dp, accountViewModel, Modifier, null)
modifier =
Modifier.padding( Column(
start = 12.dp, modifier = Modifier.padding(start = 10.dp).weight(1f),
end = 12.dp,
top = 10.dp,
),
) { ) {
ClickableUserPicture(baseUser, 55.dp, accountViewModel, Modifier, null) Row(verticalAlignment = Alignment.CenterVertically) { UsernameDisplay(baseUser) }
Column( AboutDisplay(baseUser)
modifier = Modifier.padding(start = 10.dp).weight(1f),
) {
Row(verticalAlignment = Alignment.CenterVertically) { UsernameDisplay(baseUser) }
AboutDisplay(baseUser)
}
} }
HorizontalDivider(
modifier = Modifier.padding(top = 10.dp),
thickness = DividerThickness,
)
} }
} }

View File

@ -20,11 +20,8 @@
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn package com.vitorpamplona.amethyst.ui.screen.loggedIn
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
@ -79,7 +76,5 @@ fun ThreadScreen(
onDispose { lifeCycleOwner.lifecycle.removeObserver(observer) } onDispose { lifeCycleOwner.lifecycle.removeObserver(observer) }
} }
Column(Modifier.fillMaxHeight()) { ThreadFeedView(noteId, feedViewModel, accountViewModel, nav)
Column { ThreadFeedView(noteId, feedViewModel, accountViewModel, nav) }
}
} }

View File

@ -32,6 +32,17 @@ class LargeCache<K, V> {
fun size() = cache.size fun size() = cache.size
fun isEmpty() = cache.isEmpty()
fun containsKey(key: K) = cache.containsKey(key)
fun put(
key: K,
value: V,
) {
cache.put(key, value)
}
fun getOrCreate( fun getOrCreate(
key: K, key: K,
builder: (key: K) -> V, builder: (key: K) -> V,

View File

@ -72,13 +72,29 @@ open class BaseTextNoteEvent(
} }
fun replyingTo(): HexKey? { fun replyingTo(): HexKey? {
val oldStylePositional = tags.lastOrNull { it.size > 1 && it[0] == "e" }?.get(1) val oldStylePositional = tags.lastOrNull { it.size > 1 && it.size <= 3 && it[0] == "e" }?.get(1)
val newStyleReply = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "reply" }?.get(1) val newStyleReply = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "reply" }?.get(1)
val newStyleRoot = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "root" }?.get(1) val newStyleRoot = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "root" }?.get(1)
return newStyleReply ?: newStyleRoot ?: oldStylePositional return newStyleReply ?: newStyleRoot ?: oldStylePositional
} }
fun replyingToAddress(): ATag? {
val oldStylePositional = tags.lastOrNull { it.size > 1 && it.size <= 3 && it[0] == "a" }?.let { ATag.parseAtag(it[1], it[2]) }
val newStyleReply = tags.lastOrNull { it.size > 3 && it[0] == "a" && it[3] == "reply" }?.let { ATag.parseAtag(it[1], it[2]) }
val newStyleRoot = tags.lastOrNull { it.size > 3 && it[0] == "a" && it[3] == "root" }?.let { ATag.parseAtag(it[1], it[2]) }
return newStyleReply ?: newStyleRoot ?: oldStylePositional
}
fun replyingToAddressOrEvent(): String? {
val oldStylePositional = tags.lastOrNull { it.size > 1 && it.size <= 3 && (it[0] == "e" || it[0] == "a") }?.get(1)
val newStyleReply = tags.lastOrNull { it.size > 3 && (it[0] == "e" || it[0] == "a") && it[3] == "reply" }?.get(1)
val newStyleRoot = tags.lastOrNull { it.size > 3 && (it[0] == "e" || it[0] == "a") && it[3] == "root" }?.get(1)
return newStyleReply ?: newStyleRoot ?: oldStylePositional
}
@Transient private var citedUsersCache: Set<String>? = null @Transient private var citedUsersCache: Set<String>? = null
@Transient private var citedNotesCache: Set<String>? = null @Transient private var citedNotesCache: Set<String>? = null