Merge branch 'main' into amber

This commit is contained in:
greenart7c3
2023-08-25 05:27:14 -03:00
committed by GitHub
50 changed files with 1643 additions and 459 deletions
@@ -133,6 +133,7 @@ object ServiceManager {
LocalCache.pruneContactLists(accounts)
LocalCache.pruneRepliesAndReactions(accounts)
LocalCache.prunePastVersionsOfReplaceables()
LocalCache.pruneExpiredEvents()
}
}
}
@@ -1174,6 +1174,25 @@ class Account(
}
}
fun updateStatus(oldStatus: AddressableNote, newStatus: String) {
if (!isWriteable()) return
val oldEvent = oldStatus.event as? StatusEvent ?: return
val event = StatusEvent.update(oldEvent, newStatus, keyPair.privKey!!)
Client.send(event)
LocalCache.consume(event, null)
}
fun createStatus(newStatus: String) {
if (!isWriteable()) return
val event = StatusEvent.create(newStatus, "general", expiration = null, keyPair.privKey!!)
Client.send(event)
LocalCache.consume(event, null)
}
fun removeEmojiPack(usersEmojiList: Note, emojiList: Note) {
if (!isWriteable()) return
@@ -14,7 +14,10 @@ import com.vitorpamplona.quartz.encoders.Nip19
import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.events.*
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -342,6 +345,27 @@ object LocalCache {
private fun consume(event: PinListEvent) { consumeBaseReplaceable(event) }
private fun consume(event: RelaySetEvent) { consumeBaseReplaceable(event) }
private fun consume(event: AudioTrackEvent) { consumeBaseReplaceable(event) }
fun consume(event: StatusEvent, relay: Relay?) {
val version = getOrCreateNote(event.id)
val note = getOrCreateAddressableNote(event.address())
val author = getOrCreateUser(event.pubKey)
if (version.event == null) {
version.loadEvent(event, author, emptyList())
version.moveAllReferencesTo(note)
}
// Already processed this event.
if (note.event?.id() == event.id()) return
if (event.createdAt > (note.createdAt() ?: 0)) {
note.loadEvent(event, author, emptyList())
author.liveSet?.statuses?.invalidateData()
refreshObservers(note)
}
}
fun consume(event: BadgeDefinitionEvent) { consumeBaseReplaceable(event) }
@@ -1088,7 +1112,7 @@ object LocalCache {
}
return notes.values.filter {
(it.event !is GenericRepostEvent && it.event !is RepostEvent && it.event !is CommunityPostApprovalEvent && it.event !is ReactionEvent && it.event !is GiftWrapEvent) &&
(it.event !is GenericRepostEvent && it.event !is RepostEvent && it.event !is CommunityPostApprovalEvent && it.event !is ReactionEvent && it.event !is GiftWrapEvent && it.event !is LnZapEvent && it.event !is LnZapRequestEvent) &&
(
it.event?.content()?.contains(text, true) ?: false ||
it.event?.matchTag1With(text) ?: false ||
@@ -1096,7 +1120,7 @@ object LocalCache {
it.idNote().startsWith(text, true)
)
} + addressables.values.filter {
(it.event !is GenericRepostEvent && it.event !is RepostEvent && it.event !is CommunityPostApprovalEvent && it.event !is ReactionEvent && it.event !is GiftWrapEvent) &&
(it.event !is GenericRepostEvent && it.event !is RepostEvent && it.event !is CommunityPostApprovalEvent && it.event !is ReactionEvent && it.event !is GiftWrapEvent && it.event !is LnZapEvent && it.event !is LnZapRequestEvent) &&
(
it.event?.content()?.contains(text, true) ?: false ||
it.event?.matchTag1With(text) ?: false ||
@@ -1125,6 +1149,18 @@ object LocalCache {
}
}
suspend fun findStatusesForUser(user: User): ImmutableList<AddressableNote> {
checkNotInMainThread()
return addressables.filter {
val noteEvent = it.value.event
(noteEvent is StatusEvent && noteEvent.pubKey == user.pubkeyHex && !noteEvent.isExpired() && noteEvent.content.isNotBlank())
}.values
.sortedWith(compareBy({ it.event?.expiration() ?: it.event?.createdAt() }, { it.idHex }))
.reversed()
.toImmutableList()
}
fun cleanObservers() {
notes.forEach {
it.value.clearLive()
@@ -1268,6 +1304,39 @@ object LocalCache {
}
}
fun pruneExpiredEvents() {
checkNotInMainThread()
val now = TimeUtils.now()
val toBeRemoved = notes.filter {
it.value.event?.isExpired() == true
}.values
val childrenToBeRemoved = mutableListOf<Note>()
toBeRemoved.forEach {
notes.remove(it.idHex)
it.replyTo?.forEach { masterNote ->
masterNote.removeReply(it)
masterNote.removeBoost(it)
masterNote.removeReaction(it)
masterNote.removeZap(it)
masterNote.removeReport(it)
masterNote.clearEOSE() // allows reloading of these events
}
childrenToBeRemoved.addAll(it.removeAllChildNotes())
}
removeChildrenOf(childrenToBeRemoved)
if (toBeRemoved.size > 1) {
println("PRUNE: ${toBeRemoved.size} thread replies removed.")
}
}
fun pruneHiddenMessages(account: Account) {
checkNotInMainThread()
@@ -1423,6 +1492,7 @@ object LocalCache {
is PrivateDmEvent -> consume(event, relay)
is PinListEvent -> consume(event)
is PeopleListEvent -> consume(event)
is PollNoteEvent -> consume(event, relay)
is ReactionEvent -> consume(event)
is RecommendRelayEvent -> consume(event)
is RelaySetEvent -> consume(event)
@@ -1439,8 +1509,9 @@ object LocalCache {
}
consume(event)
}
is StatusEvent -> consume(event, relay)
is TextNoteEvent -> consume(event, relay)
is PollNoteEvent -> consume(event, relay)
else -> {
Log.w("Event Not Supported", event.toJson())
}
@@ -3,6 +3,8 @@ package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.lifecycle.LiveData
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji
@@ -10,6 +12,7 @@ import com.vitorpamplona.amethyst.service.relays.EOSETime
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.actions.updated
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import com.vitorpamplona.amethyst.ui.note.combineWith
import com.vitorpamplona.amethyst.ui.note.toShortenHex
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.Hex
@@ -651,10 +654,18 @@ open class Note(val idHex: String) {
}
}
@Stable
class NoteLiveSet(u: Note) {
// Observers line up here.
val metadata: NoteLiveData = NoteLiveData(u)
val authorChanges = metadata.map {
it.note.author
}
val hasEvent = metadata.map {
it.note.event != null
}.distinctUntilChanged()
val reactions: NoteLiveData = NoteLiveData(u)
val boosts: NoteLiveData = NoteLiveData(u)
val replies: NoteLiveData = NoteLiveData(u)
@@ -662,6 +673,20 @@ class NoteLiveSet(u: Note) {
val relays: NoteLiveData = NoteLiveData(u)
val zaps: NoteLiveData = NoteLiveData(u)
val hasReactions = zaps.combineWith(boosts, reactions) { zapState, boostState, reactionState ->
zapState?.note?.zaps?.isNotEmpty() ?: false ||
boostState?.note?.boosts?.isNotEmpty() ?: false ||
reactionState?.note?.reactions?.isNotEmpty() ?: false
}.distinctUntilChanged()
val replyCount = replies.map {
it.note.replies.size
}.distinctUntilChanged()
val boostCount = boosts.map {
it.note.boosts.size
}.distinctUntilChanged()
fun isInUse(): Boolean {
return metadata.hasObservers() ||
reactions.hasObservers() ||
@@ -3,6 +3,8 @@ package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.lifecycle.LiveData
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.relays.EOSETime
@@ -378,6 +380,7 @@ class User(val pubkeyHex: String) {
}
}
@Stable
class UserLiveSet(u: User) {
// UI Observers line up here.
val follows: UserLiveData = UserLiveData(u)
@@ -389,6 +392,19 @@ class UserLiveSet(u: User) {
val metadata: UserLiveData = UserLiveData(u)
val zaps: UserLiveData = UserLiveData(u)
val bookmarks: UserLiveData = UserLiveData(u)
val statuses: UserLiveData = UserLiveData(u)
val profilePictureChanges = metadata.map {
it.user.profilePicture()
}.distinctUntilChanged()
val nip05Changes = metadata.map {
it.user.nip05()
}.distinctUntilChanged()
val userMetadataInfo = metadata.map {
it.user.info
}.distinctUntilChanged()
fun isInUse(): Boolean {
return follows.hasObservers() ||
@@ -399,6 +415,7 @@ class UserLiveSet(u: User) {
relayInfo.hasObservers() ||
metadata.hasObservers() ||
zaps.hasObservers() ||
statuses.hasObservers() ||
bookmarks.hasObservers()
}
@@ -412,6 +429,7 @@ class UserLiveSet(u: User) {
metadata.destroy()
zaps.destroy()
bookmarks.destroy()
statuses.destroy()
}
}
@@ -63,9 +63,9 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
return TypedFilter(
types = COMMON_FEED_TYPES,
filter = JsonFilter(
kinds = listOf(AdvertisedRelayListEvent.kind),
kinds = listOf(AdvertisedRelayListEvent.kind, StatusEvent.kind),
authors = listOf(account.userProfile().pubkeyHex),
limit = 1
limit = 5
)
)
}
@@ -7,6 +7,7 @@ import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import com.vitorpamplona.quartz.events.MetadataEvent
import com.vitorpamplona.quartz.events.ReportEvent
import com.vitorpamplona.quartz.events.StatusEvent
object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
var usersToWatch = setOf<User>()
@@ -26,6 +27,21 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
}
}
fun createUserStatusFilter(): List<TypedFilter>? {
if (usersToWatch.isEmpty()) return null
return usersToWatch.map {
TypedFilter(
types = COMMON_FEED_TYPES,
filter = JsonFilter(
kinds = listOf(StatusEvent.kind),
authors = listOf(it.pubkeyHex),
since = it.latestEOSEs
)
)
}
}
fun createUserReportFilter(): List<TypedFilter>? {
if (usersToWatch.isEmpty()) return null
@@ -59,8 +75,8 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
val userChannelOnce = requestNewChannel()
override fun updateChannelFilters() {
userChannel.typedFilters = listOfNotNull(createUserFilter()).flatten().ifEmpty { null }
userChannelOnce.typedFilters = listOfNotNull(createUserReportFilter()).flatten().ifEmpty { null }
userChannel.typedFilters = listOfNotNull(createUserReportFilter(), createUserStatusFilter()).flatten().ifEmpty { null }
userChannelOnce.typedFilters = listOfNotNull(createUserFilter()).flatten().ifEmpty { null }
}
fun add(user: User) {
@@ -120,7 +120,7 @@ fun JoinUserOrChannelView(searchBarViewModel: SearchBarViewModel, onClose: () ->
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
CloseButton(onCancel = {
CloseButton(onPress = {
searchBarViewModel.clear()
NostrSearchEventOrUserDataSource.clear()
onClose()
@@ -51,7 +51,7 @@ fun NewChannelView(onClose: () -> Unit, accountViewModel: AccountViewModel, chan
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
CloseButton(onCancel = {
CloseButton(onPress = {
postViewModel.clear()
onClose()
})
@@ -123,7 +123,7 @@ fun NewMediaView(uri: Uri, onClose: () -> Unit, postViewModel: NewMediaModel, ac
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
CloseButton(onCancel = {
CloseButton(onPress = {
postViewModel.cancel()
onClose()
})
@@ -100,6 +100,7 @@ import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size5dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.replyModifier
@@ -188,316 +189,340 @@ fun NewPostView(
decorFitsSystemWindows = false
)
) {
Surface(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
) {
if (showRelaysDialog) {
RelaySelectionDialog(
list = relayList,
onClose = {
showRelaysDialog = false
},
onPost = {
relayList = it
},
accountViewModel = accountViewModel,
nav = nav
)
}
if (showRelaysDialog) {
RelaySelectionDialog(
list = relayList,
onClose = {
showRelaysDialog = false
},
onPost = {
relayList = it
},
accountViewModel = accountViewModel,
nav = nav
)
}
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
) {
Column(
modifier = Modifier
.padding(start = 10.dp, end = 10.dp, top = 10.dp)
.imePadding()
.weight(1f)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
CloseButton(onCancel = {
Scaffold(
topBar = {
TopAppBar(
title = {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(end = 10.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Spacer(modifier = StdHorzSpacer)
Box {
IconButton(
modifier = Modifier.align(Alignment.Center),
onClick = {
showRelaysDialog = true
}
) {
Icon(
painter = painterResource(R.drawable.relays),
contentDescription = null,
modifier = Modifier.height(25.dp),
tint = MaterialTheme.colors.onBackground
)
}
}
PostButton(
onPost = {
scope.launch(Dispatchers.IO) {
postViewModel.sendPost(relayList = relayList)
onClose()
}
},
isActive = postViewModel.canPost()
)
}
},
navigationIcon = {
Spacer(modifier = StdHorzSpacer)
CloseButton(onPress = {
postViewModel.cancel()
onClose()
})
Box {
IconButton(
modifier = Modifier.align(Alignment.Center),
onClick = {
showRelaysDialog = true
}
) {
Icon(
painter = painterResource(R.drawable.relays),
contentDescription = null,
modifier = Modifier.height(25.dp),
tint = MaterialTheme.colors.onBackground
)
}
}
PostButton(
onPost = {
scope.launch(Dispatchers.IO) {
event = postViewModel.sendPost(relayList = relayList, signEvent = account.keyPair.privKey != null)
if (event == null) {
onClose()
}
}
},
isActive = postViewModel.canPost()
)
}
Row(
},
backgroundColor = MaterialTheme.colors.surface,
elevation = 0.dp
)
}
) { pad ->
Surface(
modifier = Modifier
.padding(
start = Size10dp,
top = pad.calculateTopPadding(),
end = Size10dp,
bottom = pad.calculateBottomPadding()
)
.fillMaxSize()
) {
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(top = 10.dp)
.imePadding()
.weight(1f)
) {
Column(
Row(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(scrollState)
.weight(1f)
) {
postViewModel.originalNote?.let {
NoteCompose(
baseNote = it,
makeItShort = true,
unPackReply = false,
isQuotedNote = true,
modifier = MaterialTheme.colors.replyModifier,
accountViewModel = accountViewModel,
nav = nav
)
}
Notifying(postViewModel.mentions?.toImmutableList()) {
postViewModel.removeFromReplyList(it)
}
if (enableMessageInterface) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)
) {
SendDirectMessageTo(postViewModel = postViewModel)
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(scrollState)
) {
postViewModel.originalNote?.let {
Row(Modifier.heightIn(max = 200.dp)) {
NoteCompose(
baseNote = it,
makeItShort = true,
unPackReply = false,
isQuotedNote = true,
modifier = MaterialTheme.colors.replyModifier,
accountViewModel = accountViewModel,
nav = nav
)
Spacer(modifier = StdVertSpacer)
}
}
}
MessageField(postViewModel)
if (postViewModel.wantsPoll) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)
) {
PollField(postViewModel)
Row() {
Notifying(postViewModel.mentions?.toImmutableList()) {
postViewModel.removeFromReplyList(it)
}
}
}
if (postViewModel.wantsToMarkAsSensitive) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(vertical = Size5dp, horizontal = Size10dp)
) {
ContentSensitivityExplainer(postViewModel)
if (enableMessageInterface) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)
) {
SendDirectMessageTo(postViewModel = postViewModel)
}
}
}
if (postViewModel.wantsToAddGeoHash) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(vertical = Size5dp, horizontal = Size10dp)
) {
LocationAsHash(postViewModel)
MessageField(postViewModel)
if (postViewModel.wantsPoll) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)
) {
PollField(postViewModel)
}
}
}
if (postViewModel.wantsForwardZapTo) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)
) {
FowardZapTo(postViewModel)
if (postViewModel.wantsToMarkAsSensitive) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(vertical = Size5dp, horizontal = Size10dp)
) {
ContentSensitivityExplainer(postViewModel)
}
}
}
val url = postViewModel.contentToAddUrl
if (url != null) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)) {
ImageVideoDescription(
url,
account.defaultFileServer,
onAdd = { description, server, sensitiveContent ->
postViewModel.upload(url, description, sensitiveContent, server, context, relayList)
account.changeDefaultFileServer(server)
},
onCancel = {
postViewModel.contentToAddUrl = null
},
onError = {
scope.launch {
postViewModel.imageUploadingError.emit(it)
}
},
accountViewModel = accountViewModel
)
if (postViewModel.wantsToAddGeoHash) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(vertical = Size5dp, horizontal = Size10dp)
) {
LocationAsHash(postViewModel)
}
}
}
val user = postViewModel.account?.userProfile()
val lud16 = user?.info?.lnAddress()
if (postViewModel.wantsForwardZapTo) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)
) {
FowardZapTo(postViewModel)
}
}
if (lud16 != null && postViewModel.wantsInvoice) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)) {
Column(Modifier.fillMaxWidth()) {
InvoiceRequest(
lud16,
user.pubkeyHex,
account,
stringResource(id = R.string.lightning_invoice),
stringResource(id = R.string.lightning_create_and_add_invoice),
onSuccess = {
postViewModel.message = TextFieldValue(postViewModel.message.text + "\n\n" + it)
postViewModel.wantsInvoice = false
val url = postViewModel.contentToAddUrl
if (url != null) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)) {
ImageVideoDescription(
url,
account.defaultFileServer,
onAdd = { description, server, sensitiveContent ->
postViewModel.upload(url, description, sensitiveContent, server, context, relayList)
account.changeDefaultFileServer(server)
},
onClose = {
postViewModel.wantsInvoice = false
}
onCancel = {
postViewModel.contentToAddUrl = null
},
onError = {
scope.launch {
postViewModel.imageUploadingError.emit(it)
}
},
accountViewModel = accountViewModel
)
}
}
}
if (lud16 != null && postViewModel.wantsZapraiser) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)) {
ZapRaiserRequest(
stringResource(id = R.string.zapraiser),
postViewModel
)
}
}
val user = postViewModel.account?.userProfile()
val lud16 = user?.info?.lnAddress()
val myUrlPreview = postViewModel.urlPreview
if (myUrlPreview != null) {
Row(modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)) {
if (isValidURL(myUrlPreview)) {
val removedParamsFromUrl =
myUrlPreview.split("?")[0].lowercase()
if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
AsyncImage(
model = myUrlPreview,
contentDescription = myUrlPreview,
contentScale = ContentScale.FillWidth,
modifier = Modifier
.padding(top = 4.dp)
.fillMaxWidth()
.clip(shape = QuoteBorder)
.border(
1.dp,
MaterialTheme.colors.subtleBorder,
QuoteBorder
)
if (lud16 != null && postViewModel.wantsInvoice) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)) {
Column(Modifier.fillMaxWidth()) {
InvoiceRequest(
lud16,
user.pubkeyHex,
account,
stringResource(id = R.string.lightning_invoice),
stringResource(id = R.string.lightning_create_and_add_invoice),
onSuccess = {
postViewModel.message = TextFieldValue(postViewModel.message.text + "\n\n" + it)
postViewModel.wantsInvoice = false
},
onClose = {
postViewModel.wantsInvoice = false
}
)
} else if (videoExtensions.any { removedParamsFromUrl.endsWith(it) }) {
VideoView(myUrlPreview, roundedCorner = true, accountViewModel = accountViewModel)
} else {
UrlPreview(myUrlPreview, myUrlPreview, accountViewModel)
}
} else if (startsWithNIP19Scheme(myUrlPreview)) {
val bgColor = MaterialTheme.colors.background
val backgroundColor = remember {
mutableStateOf(bgColor)
}
}
}
BechLink(
myUrlPreview,
true,
backgroundColor,
accountViewModel,
nav
if (lud16 != null && postViewModel.wantsZapraiser) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)) {
ZapRaiserRequest(
stringResource(id = R.string.zapraiser),
postViewModel
)
} else if (noProtocolUrlValidator.matcher(myUrlPreview).matches()) {
UrlPreview("https://$myUrlPreview", myUrlPreview, accountViewModel)
}
}
val myUrlPreview = postViewModel.urlPreview
if (myUrlPreview != null) {
Row(modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)) {
if (isValidURL(myUrlPreview)) {
val removedParamsFromUrl =
myUrlPreview.split("?")[0].lowercase()
if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
AsyncImage(
model = myUrlPreview,
contentDescription = myUrlPreview,
contentScale = ContentScale.FillWidth,
modifier = Modifier
.padding(top = 4.dp)
.fillMaxWidth()
.clip(shape = QuoteBorder)
.border(
1.dp,
MaterialTheme.colors.subtleBorder,
QuoteBorder
)
)
} else if (videoExtensions.any { removedParamsFromUrl.endsWith(it) }) {
VideoView(myUrlPreview, roundedCorner = true, accountViewModel = accountViewModel)
} else {
UrlPreview(myUrlPreview, myUrlPreview, accountViewModel)
}
} else if (startsWithNIP19Scheme(myUrlPreview)) {
val bgColor = MaterialTheme.colors.background
val backgroundColor = remember {
mutableStateOf(bgColor)
}
BechLink(
myUrlPreview,
true,
backgroundColor,
accountViewModel,
nav
)
} else if (noProtocolUrlValidator.matcher(myUrlPreview).matches()) {
UrlPreview("https://$myUrlPreview", myUrlPreview, accountViewModel)
}
}
}
}
}
}
val userSuggestions = postViewModel.userSuggestions
if (userSuggestions.isNotEmpty()) {
LazyColumn(
contentPadding = PaddingValues(
top = 10.dp
),
modifier = Modifier.heightIn(0.dp, 300.dp)
) {
itemsIndexed(
userSuggestions,
key = { _, item -> item.pubkeyHex }
) { _, item ->
UserLine(item, accountViewModel) {
postViewModel.autocompleteWithUser(item)
val userSuggestions = postViewModel.userSuggestions
if (userSuggestions.isNotEmpty()) {
LazyColumn(
contentPadding = PaddingValues(
top = 10.dp
),
modifier = Modifier.heightIn(0.dp, 300.dp)
) {
itemsIndexed(
userSuggestions,
key = { _, item -> item.pubkeyHex }
) { _, item ->
UserLine(item, accountViewModel) {
postViewModel.autocompleteWithUser(item)
}
}
}
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
verticalAlignment = Alignment.CenterVertically
) {
UploadFromGallery(
isUploading = postViewModel.isUploadingImage,
tint = MaterialTheme.colors.onBackground,
Row(
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
verticalAlignment = Alignment.CenterVertically
) {
postViewModel.selectImage(it)
}
if (postViewModel.canUsePoll) {
// These should be hashtag recommendations the user selects in the future.
// val hashtag = stringResource(R.string.poll_hashtag)
// postViewModel.includePollHashtagInMessage(postViewModel.wantsPoll, hashtag)
AddPollButton(postViewModel.wantsPoll) {
postViewModel.wantsPoll = !postViewModel.wantsPoll
UploadFromGallery(
isUploading = postViewModel.isUploadingImage,
tint = MaterialTheme.colors.onBackground,
modifier = Modifier
) {
postViewModel.selectImage(it)
}
}
if (postViewModel.canAddInvoice) {
AddLnInvoiceButton(postViewModel.wantsInvoice) {
postViewModel.wantsInvoice = !postViewModel.wantsInvoice
if (postViewModel.canUsePoll) {
// These should be hashtag recommendations the user selects in the future.
// val hashtag = stringResource(R.string.poll_hashtag)
// postViewModel.includePollHashtagInMessage(postViewModel.wantsPoll, hashtag)
AddPollButton(postViewModel.wantsPoll) {
postViewModel.wantsPoll = !postViewModel.wantsPoll
}
}
}
if (postViewModel.canAddZapRaiser) {
AddZapraiserButton(postViewModel.wantsZapraiser) {
postViewModel.wantsZapraiser = !postViewModel.wantsZapraiser
if (postViewModel.canAddInvoice) {
AddLnInvoiceButton(postViewModel.wantsInvoice) {
postViewModel.wantsInvoice = !postViewModel.wantsInvoice
}
}
}
MarkAsSensitive(postViewModel) {
postViewModel.wantsToMarkAsSensitive = !postViewModel.wantsToMarkAsSensitive
}
if (postViewModel.canAddZapRaiser) {
AddZapraiserButton(postViewModel.wantsZapraiser) {
postViewModel.wantsZapraiser = !postViewModel.wantsZapraiser
}
}
AddGeoHash(postViewModel) {
postViewModel.wantsToAddGeoHash = !postViewModel.wantsToAddGeoHash
}
MarkAsSensitive(postViewModel) {
postViewModel.wantsToMarkAsSensitive = !postViewModel.wantsToMarkAsSensitive
}
ForwardZapTo(postViewModel) {
postViewModel.wantsForwardZapTo = !postViewModel.wantsForwardZapTo
AddGeoHash(postViewModel) {
postViewModel.wantsToAddGeoHash = !postViewModel.wantsToAddGeoHash
}
ForwardZapTo(postViewModel) {
postViewModel.wantsForwardZapTo = !postViewModel.wantsForwardZapTo
}
}
}
}
@@ -1183,10 +1208,10 @@ private fun MarkAsSensitive(
}
@Composable
fun CloseButton(onCancel: () -> Unit) {
fun CloseButton(onPress: () -> Unit) {
Button(
onClick = {
onCancel()
onPress()
},
shape = ButtonBorder,
colors = ButtonDefaults
@@ -157,7 +157,7 @@ fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, re
},
navigationIcon = {
Spacer(modifier = StdHorzSpacer)
CloseButton(onCancel = {
CloseButton(onPress = {
postViewModel.clear()
onClose()
})
@@ -97,7 +97,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
CloseButton(onCancel = {
CloseButton(onPress = {
postViewModel.clear()
onClose()
})
@@ -74,7 +74,7 @@ fun RelayInformationDialog(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
CloseButton(onCancel = {
CloseButton(onPress = {
onClose()
})
}
@@ -110,7 +110,7 @@ fun RelaySelectionDialog(
verticalAlignment = Alignment.CenterVertically
) {
CloseButton(
onCancel = {
onPress = {
onClose()
}
)
@@ -30,8 +30,6 @@ import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.em
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.halilibo.richtext.markdown.Markdown
import com.halilibo.richtext.markdown.MarkdownParseOptions
import com.halilibo.richtext.ui.material.MaterialRichText
@@ -421,9 +419,7 @@ private fun ObserveNIP19Event(
@Composable
fun ObserveNote(note: Note, onRefresh: () -> Unit) {
val loadedNoteId by note.live().metadata.map {
it.note.event?.id()
}.distinctUntilChanged().observeAsState(note.event?.id())
val loadedNoteId by note.live().metadata.observeAsState()
LaunchedEffect(key1 = loadedNoteId) {
if (loadedNoteId != null) {
@@ -460,9 +456,7 @@ private fun ObserveNIP19User(
@Composable
private fun ObserveUser(user: User, onRefresh: () -> Unit) {
val loadedUserMetaId by user.live().metadata.map {
it.user.info?.latestMetadata?.id
}.distinctUntilChanged().observeAsState(user.info?.latestMetadata?.id)
val loadedUserMetaId by user.live().metadata.observeAsState()
LaunchedEffect(key1 = loadedUserMetaId) {
if (loadedUserMetaId != null) {
@@ -854,9 +848,7 @@ private fun DisplayUserFromTag(
val route = remember { "User/${baseUser.pubkeyHex}" }
val hex = remember { baseUser.pubkeyDisplayHex() }
val meta by baseUser.live().metadata.map {
it.user.info
}.distinctUntilChanged().observeAsState(baseUser.info)
val meta by baseUser.live().userMetadataInfo.observeAsState(baseUser.info)
Crossfade(targetState = meta) {
Row() {
@@ -663,7 +663,7 @@ fun ZoomableImageDialog(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
CloseButton(onCancel = onDismiss)
CloseButton(onPress = onDismiss)
val myContent = allImages[pagerState.currentPage]
if (myContent is ZoomableUrlContent) {
@@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.AlertDialog
import androidx.compose.material.Divider
@@ -27,13 +28,18 @@ import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.ModalBottomSheetState
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.ScaffoldState
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Send
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
@@ -46,13 +52,15 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage
import com.vitorpamplona.amethyst.BuildConfig
@@ -65,12 +73,16 @@ import com.vitorpamplona.amethyst.service.HttpClient
import com.vitorpamplona.amethyst.ui.actions.NewRelayListView
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
import com.vitorpamplona.amethyst.ui.note.LoadStatuses
import com.vitorpamplona.amethyst.ui.screen.RelayPoolViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountBackupDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ConnectOrbotDialog
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size16dp
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.Size26Modifier
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.events.toImmutableListOfLists
import kotlinx.coroutines.CoroutineScope
@@ -97,6 +109,7 @@ fun DrawerContent(
.padding(horizontal = 25.dp)
.padding(top = 100.dp),
scaffoldState,
accountViewModel,
nav
)
Divider(
@@ -123,6 +136,7 @@ fun ProfileContent(
baseAccountUser: User,
modifier: Modifier = Modifier,
scaffoldState: ScaffoldState,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val coroutineScope = rememberCoroutineScope()
@@ -215,9 +229,14 @@ fun ProfileContent(
)
)
}
Row(Modifier.padding(top = Size10dp)) {
EditStatusBox(baseAccountUser, accountViewModel)
}
Row(
modifier = Modifier
.padding(top = 15.dp)
.padding(top = Size10dp)
.clickable(onClick = {
nav(route)
coroutineScope.launch {
@@ -231,6 +250,132 @@ fun ProfileContent(
}
}
@Composable
private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewModel) {
val scope = rememberCoroutineScope()
val focusManager = LocalFocusManager.current
LoadStatuses(user = baseAccountUser) { statuses ->
if (statuses.isEmpty()) {
val currentStatus = remember {
mutableStateOf("")
}
val hasChanged by remember {
derivedStateOf {
currentStatus.value != ""
}
}
OutlinedTextField(
value = currentStatus.value,
onValueChange = { currentStatus.value = it },
label = { Text(text = stringResource(R.string.status_update)) },
modifier = Modifier.fillMaxWidth(),
placeholder = {
Text(
text = stringResource(R.string.status_update),
color = MaterialTheme.colors.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences
),
singleLine = true,
trailingIcon = {
if (hasChanged) {
UserStatusSendButton() {
scope.launch(Dispatchers.IO) {
accountViewModel.createStatus(currentStatus.value)
focusManager.clearFocus(true)
}
}
}
}
)
} else {
statuses.forEach {
val originalStatus by it.live().metadata.map {
it.note.event?.content() ?: ""
}.observeAsState(it.event?.content() ?: "")
val thisStatus = remember {
mutableStateOf(originalStatus)
}
val hasChanged by remember {
derivedStateOf {
thisStatus.value != originalStatus
}
}
OutlinedTextField(
value = thisStatus.value,
onValueChange = { thisStatus.value = it },
label = { Text(text = stringResource(R.string.status_update)) },
modifier = Modifier.fillMaxWidth(),
placeholder = {
Text(
text = stringResource(R.string.status_update),
color = MaterialTheme.colors.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences
),
singleLine = true,
trailingIcon = {
if (hasChanged) {
UserStatusSendButton() {
scope.launch(Dispatchers.IO) {
accountViewModel.updateStatus(it, thisStatus.value)
focusManager.clearFocus(true)
}
}
} else {
UserStatusDeleteButton() {
scope.launch(Dispatchers.IO) {
accountViewModel.updateStatus(it, "")
accountViewModel.delete(it)
focusManager.clearFocus(true)
}
}
}
}
)
}
}
}
}
@Composable
fun UserStatusSendButton(onClick: () -> Unit) {
IconButton(
modifier = Size26Modifier,
onClick = onClick
) {
Icon(
imageVector = Icons.Default.Send,
null,
modifier = Size20Modifier,
tint = MaterialTheme.colors.placeholderText
)
}
}
@Composable
fun UserStatusDeleteButton(onClick: () -> Unit) {
IconButton(
modifier = Size26Modifier,
onClick = onClick
) {
Icon(
imageVector = Icons.Default.Delete,
null,
modifier = Size20Modifier,
tint = MaterialTheme.colors.placeholderText
)
}
}
@Composable
private fun FollowingAndFollowerCounts(baseAccountUser: User) {
var followingCount by remember { mutableStateOf("--") }
@@ -484,7 +629,7 @@ private fun RelayStatus(
relayViewModel: RelayPoolViewModel
) {
val connectedRelaysText by relayViewModel.connectionStatus.observeAsState("--/--")
val isConnected by relayViewModel.isConnected.distinctUntilChanged().observeAsState(false)
val isConnected by relayViewModel.isConnected.observeAsState(false)
RenderRelayStatus(connectedRelaysText, isConnected)
}
@@ -95,7 +95,7 @@ fun AddBountyAmountDialog(bounty: Note, accountViewModel: AccountViewModel, onCl
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
CloseButton(onCancel = {
CloseButton(onPress = {
postViewModel.cancel()
onClose()
})
@@ -93,24 +93,10 @@ fun ChannelCardCompose(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val isBlank by baseNote.live().metadata.map {
it.note.event == null
}.distinctUntilChanged().observeAsState(baseNote.event == null)
val hasEvent by baseNote.live().hasEvent.observeAsState(baseNote.event != null)
Crossfade(targetState = isBlank) {
Crossfade(targetState = hasEvent) {
if (it) {
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
BlankNote(
remember {
modifier.combinedClickable(
onClick = { },
onLongClick = showPopup
)
},
false
)
}
} else {
if (forceEventKind == null || baseNote.event?.kind() == forceEventKind) {
CheckHiddenChannelCardCompose(
baseNote,
@@ -122,6 +108,18 @@ fun ChannelCardCompose(
nav
)
}
} else {
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
BlankNote(
remember {
modifier.combinedClickable(
onClick = { },
onLongClick = showPopup
)
},
false
)
}
}
}
}
@@ -77,14 +77,12 @@ fun ChatroomHeaderCompose(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val isBlank by baseNote.live().metadata.map {
it.note.event == null
}.observeAsState(baseNote.event == null)
val hasEvent by baseNote.live().hasEvent.observeAsState(baseNote.event != null)
if (isBlank) {
BlankNote(Modifier)
} else {
if (hasEvent) {
ChatroomComposeChannelOrUser(baseNote, accountViewModel, nav)
} else {
BlankNote(Modifier)
}
}
@@ -90,12 +90,20 @@ fun ChatroomMessageCompose(
nav: (String) -> Unit,
onWantsToReply: (Note) -> Unit
) {
val isBlank by baseNote.live().metadata.map {
it.note.event == null
}.observeAsState(baseNote.event == null)
val hasEvent by baseNote.live().hasEvent.observeAsState(baseNote.event != null)
Crossfade(targetState = isBlank) {
Crossfade(targetState = hasEvent) {
if (it) {
CheckHiddenChatMessage(
baseNote,
routeForLastRead,
innerQuote,
parentBackgroundColor,
accountViewModel,
nav,
onWantsToReply
)
} else {
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
BlankNote(
remember {
@@ -106,16 +114,6 @@ fun ChatroomMessageCompose(
}
)
}
} else {
CheckHiddenChatMessage(
baseNote,
routeForLastRead,
innerQuote,
parentBackgroundColor,
accountViewModel,
nav,
onWantsToReply
)
}
}
}
@@ -41,8 +41,6 @@ import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
@@ -579,9 +577,7 @@ private fun WatchNoteAuthor(
baseNote: Note,
onContent: @Composable (User?) -> Unit
) {
val author by baseNote.live().metadata.map {
it.note.author
}.observeAsState(baseNote.author)
val author by baseNote.live().authorChanges.observeAsState(baseNote.author)
onContent(author)
}
@@ -591,9 +587,7 @@ private fun WatchUserMetadata(
author: User,
onNewMetadata: @Composable (String?) -> Unit
) {
val userProfile by author.live().metadata.map {
it.user.profilePicture()
}.distinctUntilChanged().observeAsState(author.profilePicture())
val userProfile by author.live().profilePictureChanges.observeAsState(author.profilePicture())
onNewMetadata(userProfile)
}
@@ -5,38 +5,58 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.OpenInNew
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.map
import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.Nip05Verifier
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.ui.note.LoadStatuses
import com.vitorpamplona.amethyst.ui.note.NIP05CheckingIcon
import com.vitorpamplona.amethyst.ui.note.NIP05FailedVerification
import com.vitorpamplona.amethyst.ui.note.NIP05VerifiedIcon
import com.vitorpamplona.amethyst.ui.note.routeFor
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.Font14SP
import com.vitorpamplona.amethyst.ui.theme.NIP05IconSize
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
import com.vitorpamplona.amethyst.ui.theme.Size16Modifier
import com.vitorpamplona.amethyst.ui.theme.Size18Modifier
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
import com.vitorpamplona.amethyst.ui.theme.nip05
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.events.AddressableEvent
import com.vitorpamplona.quartz.events.UserMetadata
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.seconds
@Composable
fun nip05VerificationAsAState(userMetadata: UserMetadata, pubkeyHex: String): MutableState<Boolean?> {
@@ -93,47 +113,193 @@ fun nip05VerificationAsAState(userMetadata: UserMetadata, pubkeyHex: String): Mu
}
@Composable
fun ObserveDisplayNip05Status(baseNote: Note, columnModifier: Modifier = Modifier) {
val noteState by baseNote.live().metadata.observeAsState()
val author by remember(noteState) {
derivedStateOf {
noteState?.note?.author
}
}
fun ObserveDisplayNip05Status(
baseNote: Note,
columnModifier: Modifier = Modifier,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val author by baseNote.live().authorChanges.observeAsState()
author?.let {
ObserveDisplayNip05Status(it, columnModifier)
ObserveDisplayNip05Status(it, columnModifier, accountViewModel, nav)
}
}
@Composable
fun ObserveDisplayNip05Status(baseUser: User, columnModifier: Modifier = Modifier) {
val nip05 by baseUser.live().metadata.map {
it.user.nip05()
}.observeAsState(baseUser.nip05())
fun ObserveDisplayNip05Status(
baseUser: User,
columnModifier: Modifier = Modifier,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val nip05 by baseUser.live().nip05Changes.observeAsState(baseUser.nip05())
Crossfade(targetState = nip05, modifier = columnModifier) {
if (it != null) {
DisplayNIP05Line(it, baseUser, columnModifier)
} else {
Text(
text = baseUser.pubkeyDisplayHex(),
color = MaterialTheme.colors.placeholderText,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = columnModifier
)
LoadStatuses(baseUser) { statuses ->
Crossfade(targetState = nip05, modifier = columnModifier, label = "ObserveDisplayNip05StatusCrossfade") {
VerifyAndDisplayNIP05OrStatusLine(it, statuses, baseUser, columnModifier, accountViewModel, nav)
}
}
}
@Composable
private fun DisplayNIP05Line(nip05: String, baseUser: User, columnModifier: Modifier = Modifier) {
private fun VerifyAndDisplayNIP05OrStatusLine(
nip05: String?,
statuses: ImmutableList<AddressableNote>,
baseUser: User,
columnModifier: Modifier = Modifier,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
Column(modifier = columnModifier) {
val nip05Verified = nip05VerificationAsAState(baseUser.info!!, baseUser.pubkeyHex)
Crossfade(targetState = nip05Verified) {
Row(verticalAlignment = Alignment.CenterVertically) {
DisplayNIP05(nip05, it)
Row(verticalAlignment = Alignment.CenterVertically) {
if (nip05 != null) {
val nip05Verified = nip05VerificationAsAState(baseUser.info!!, baseUser.pubkeyHex)
if (nip05Verified.value != true) {
DisplayNIP05(nip05, nip05Verified)
} else if (!statuses.isEmpty()) {
RotateStatuses(statuses, accountViewModel, nav)
} else {
DisplayNIP05(nip05, nip05Verified)
}
} else {
if (!statuses.isEmpty()) {
RotateStatuses(statuses, accountViewModel, nav)
} else {
DisplayUsersNpub(baseUser.pubkeyDisplayHex())
}
}
}
}
}
@Composable
fun RotateStatuses(
statuses: ImmutableList<AddressableNote>,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
var indexToDisplay by remember {
mutableIntStateOf(0)
}
DisplayStatus(statuses[indexToDisplay], accountViewModel, nav)
if (statuses.size > 1) {
LaunchedEffect(Unit) {
while (true) {
delay(10.seconds)
indexToDisplay = ((indexToDisplay + 1) % (statuses.size + 1))
}
}
}
}
@Composable
fun DisplayUsersNpub(npub: String) {
Text(
text = npub,
fontSize = 14.sp,
color = MaterialTheme.colors.placeholderText,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
@Composable
fun DisplayStatus(
addressableNote: AddressableNote,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val noteState by addressableNote.live().metadata.observeAsState()
val content = remember(noteState) { addressableNote.event?.content() ?: "" }
val type = remember(noteState) {
(addressableNote.event as? AddressableEvent)?.dTag() ?: ""
}
val url = remember(noteState) {
addressableNote.event?.firstTaggedUrl()?.ifBlank { null }
}
val nostrATag = remember(noteState) {
addressableNote.event?.firstTaggedAddress()
}
val nostrHexID = remember(noteState) {
addressableNote.event?.firstTaggedEvent()?.ifBlank { null }
}
when (type) {
"music" -> Icon(
painter = painterResource(id = R.drawable.tunestr),
null,
modifier = Size18Modifier,
tint = MaterialTheme.colors.placeholderText
)
else -> {}
}
Text(
text = content,
fontSize = Font14SP,
color = MaterialTheme.colors.placeholderText,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
if (url != null) {
val uri = LocalUriHandler.current
IconButton(
modifier = Size15Modifier,
onClick = { runCatching { uri.openUri(url.trim()) } }
) {
Icon(
imageVector = Icons.Default.OpenInNew,
null,
modifier = Size15Modifier,
tint = MaterialTheme.colors.lessImportantLink
)
}
} else if (nostrATag != null) {
LoadAddressableNote(nostrATag) { note ->
if (note != null) {
IconButton(
modifier = Size15Modifier,
onClick = {
routeFor(
note,
accountViewModel.userProfile()
)?.let { nav(it) }
}
) {
Icon(
imageVector = Icons.Default.OpenInNew,
null,
modifier = Size15Modifier,
tint = MaterialTheme.colors.lessImportantLink
)
}
}
}
} else if (nostrHexID != null) {
LoadNote(baseNoteHex = nostrHexID) {
if (it != null) {
IconButton(
modifier = Size15Modifier,
onClick = {
routeFor(
it,
accountViewModel.userProfile()
)?.let { nav(it) }
}
) {
Icon(
imageVector = Icons.Default.OpenInNew,
null,
modifier = Size15Modifier,
tint = MaterialTheme.colors.lessImportantLink
)
}
}
}
}
@@ -157,6 +323,7 @@ private fun DisplayNIP05(
if (user != "_") {
Text(
text = remember(nip05) { AnnotatedString(user) },
fontSize = Font14SP,
color = MaterialTheme.colors.nip05,
maxLines = 1,
overflow = TextOverflow.Ellipsis
@@ -168,7 +335,7 @@ private fun DisplayNIP05(
ClickableText(
text = remember(nip05) { AnnotatedString(domain) },
onClick = { runCatching { uri.openUri("https://$domain") } },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.nip05),
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.nip05, fontSize = Font14SP),
maxLines = 1,
overflow = TextOverflow.Visible
)
@@ -124,7 +124,6 @@ import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.Font14SP
import com.vitorpamplona.amethyst.ui.theme.HalfPadding
import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer
import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.Size10dp
@@ -224,24 +223,10 @@ fun NoteCompose(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val isBlank by baseNote.live().metadata.map {
it.note.event == null
}.distinctUntilChanged().observeAsState(baseNote.event == null)
val hasEvent by baseNote.live().hasEvent.observeAsState(baseNote.event != null)
Crossfade(targetState = isBlank) {
Crossfade(targetState = hasEvent) {
if (it) {
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
BlankNote(
remember {
modifier.combinedClickable(
onClick = { },
onLongClick = showPopup
)
},
isBoostedNote || isQuotedNote
)
}
} else {
CheckHiddenNoteCompose(
note = baseNote,
routeForLastRead = routeForLastRead,
@@ -256,6 +241,18 @@ fun NoteCompose(
accountViewModel = accountViewModel,
nav = nav
)
} else {
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
BlankNote(
remember {
modifier.combinedClickable(
onClick = { },
onLongClick = showPopup
)
},
isBoostedNote || isQuotedNote
)
}
}
}
}
@@ -465,45 +462,86 @@ fun NormalNote(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
when (baseNote.event) {
is ChannelCreateEvent, is ChannelMetadataEvent -> ChannelHeader(
channelNote = baseNote,
showVideo = !makeItShort,
showBottomDiviser = true,
sendToChannel = true,
accountViewModel = accountViewModel,
nav = nav
)
is CommunityDefinitionEvent -> (baseNote as? AddressableNote)?.let {
CommunityHeader(
baseNote = it,
if (isQuotedNote || isBoostedNote) {
when (baseNote.event) {
is ChannelCreateEvent, is ChannelMetadataEvent -> ChannelHeader(
channelNote = baseNote,
showVideo = !makeItShort,
showBottomDiviser = true,
sendToCommunity = true,
sendToChannel = true,
accountViewModel = accountViewModel,
nav = nav
)
}
is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote)
is FileHeaderEvent -> FileHeaderDisplay(baseNote, isQuotedNote, accountViewModel)
is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, isQuotedNote, accountViewModel)
else ->
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
CheckNewAndRenderNote(
baseNote,
routeForLastRead,
modifier,
isBoostedNote,
isQuotedNote,
unPackReply,
makeItShort,
addMarginTop,
canPreview,
parentBackgroundColor,
accountViewModel,
showPopup,
nav
is CommunityDefinitionEvent -> (baseNote as? AddressableNote)?.let {
CommunityHeader(
baseNote = it,
showBottomDiviser = true,
sendToCommunity = true,
accountViewModel = accountViewModel,
nav = nav
)
}
is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote)
else ->
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
CheckNewAndRenderNote(
baseNote,
routeForLastRead,
modifier,
isBoostedNote,
isQuotedNote,
unPackReply,
makeItShort,
addMarginTop,
canPreview,
parentBackgroundColor,
accountViewModel,
showPopup,
nav
)
}
}
} else {
when (baseNote.event) {
is ChannelCreateEvent, is ChannelMetadataEvent -> ChannelHeader(
channelNote = baseNote,
showVideo = !makeItShort,
showBottomDiviser = true,
sendToChannel = true,
accountViewModel = accountViewModel,
nav = nav
)
is CommunityDefinitionEvent -> (baseNote as? AddressableNote)?.let {
CommunityHeader(
baseNote = it,
showBottomDiviser = true,
sendToCommunity = true,
accountViewModel = accountViewModel,
nav = nav
)
}
is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote)
is FileHeaderEvent -> FileHeaderDisplay(baseNote, false, accountViewModel)
is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, false, accountViewModel)
else ->
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
CheckNewAndRenderNote(
baseNote,
routeForLastRead,
modifier,
isBoostedNote,
isQuotedNote,
unPackReply,
makeItShort,
addMarginTop,
canPreview,
parentBackgroundColor,
accountViewModel,
showPopup,
nav
)
}
}
}
}
@@ -1014,7 +1052,7 @@ private fun NoteBody(
)
}
Spacer(modifier = HalfVertSpacer)
Spacer(modifier = Modifier.height(3.dp))
if (!makeItShort) {
ReplyRow(
@@ -1144,6 +1182,14 @@ private fun RenderNoteRow(
)
}
is FileHeaderEvent -> {
FileHeaderDisplay(baseNote, true, accountViewModel)
}
is FileStorageHeaderEvent -> {
FileStorageHeaderDisplay(baseNote, true, accountViewModel)
}
is CommunityPostApprovalEvent -> {
RenderPostApproval(
baseNote,
@@ -2441,7 +2487,7 @@ fun SecondUserInfoRow(
val noteAuthor = remember { note.author } ?: return
Row(verticalAlignment = CenterVertically, modifier = UserNameMaxRowHeight) {
ObserveDisplayNip05Status(noteAuthor, remember { Modifier.weight(1f) })
ObserveDisplayNip05Status(noteAuthor, remember { Modifier.weight(1f) }, accountViewModel, nav)
val geo = remember { noteEvent.getGeoHash() }
if (geo != null) {
@@ -2460,6 +2506,30 @@ fun SecondUserInfoRow(
}
}
@Composable
fun LoadStatuses(
user: User,
content: @Composable (ImmutableList<AddressableNote>) -> Unit
) {
var statuses: ImmutableList<AddressableNote> by remember {
mutableStateOf(persistentListOf())
}
val userStatus by user.live().statuses.observeAsState()
LaunchedEffect(key1 = userStatus) {
launch(Dispatchers.IO) {
val myUser = userStatus?.user ?: return@launch
val newStatuses = LocalCache.findStatusesForUser(myUser)
if (!equalImmutableLists(statuses, newStatuses)) {
statuses = newStatuses
}
}
}
content(statuses)
}
@Composable
fun DisplayLocation(geohash: String, nav: (String) -> Unit) {
val context = LocalContext.current
@@ -3199,7 +3269,7 @@ private fun RenderBadge(
}
@Composable
fun FileHeaderDisplay(note: Note, isQuotedNote: Boolean, accountViewModel: AccountViewModel) {
fun FileHeaderDisplay(note: Note, roundedCorner: Boolean, accountViewModel: AccountViewModel) {
val event = (note.event as? FileHeaderEvent) ?: return
val fullUrl = event.url() ?: return
@@ -3235,18 +3305,18 @@ fun FileHeaderDisplay(note: Note, isQuotedNote: Boolean, accountViewModel: Accou
}
SensitivityWarning(note = note, accountViewModel = accountViewModel) {
ZoomableContentView(content = content, roundedCorner = isQuotedNote, accountViewModel = accountViewModel)
ZoomableContentView(content = content, roundedCorner = roundedCorner, accountViewModel = accountViewModel)
}
}
@Composable
fun FileStorageHeaderDisplay(baseNote: Note, isQuotedNote: Boolean, accountViewModel: AccountViewModel) {
fun FileStorageHeaderDisplay(baseNote: Note, roundedCorner: Boolean, accountViewModel: AccountViewModel) {
val eventHeader = (baseNote.event as? FileStorageHeaderEvent) ?: return
val dataEventId = eventHeader.dataEventId() ?: return
LoadNote(baseNoteHex = dataEventId) { contentNote ->
if (contentNote != null) {
ObserverAndRenderNIP95(baseNote, contentNote, isQuotedNote, accountViewModel)
ObserverAndRenderNIP95(baseNote, contentNote, roundedCorner, accountViewModel)
}
}
}
@@ -3255,7 +3325,7 @@ fun FileStorageHeaderDisplay(baseNote: Note, isQuotedNote: Boolean, accountViewM
private fun ObserverAndRenderNIP95(
header: Note,
content: Note,
isQuotedNote: Boolean,
roundedCorner: Boolean,
accountViewModel: AccountViewModel
) {
val eventHeader = (header.event as? FileStorageHeaderEvent) ?: return
@@ -3302,7 +3372,7 @@ private fun ObserverAndRenderNIP95(
Crossfade(targetState = content) {
if (it != null) {
SensitivityWarning(note = header, accountViewModel = accountViewModel) {
ZoomableContentView(content = it, roundedCorner = isQuotedNote, accountViewModel = accountViewModel)
ZoomableContentView(content = it, roundedCorner = roundedCorner, accountViewModel = accountViewModel)
}
}
}
@@ -419,14 +419,7 @@ private fun ReactionDetailGallery(
val defaultBackgroundColor = MaterialTheme.colors.background
val backgroundColor = remember { mutableStateOf<Color>(defaultBackgroundColor) }
val hasReactions by baseNote.live().zaps.combineWith(
baseNote.live().boosts,
baseNote.live().reactions
) { zapState, boostState, reactionState ->
zapState?.note?.zaps?.isNotEmpty() ?: false ||
boostState?.note?.boosts?.isNotEmpty() ?: false ||
reactionState?.note?.reactions?.isNotEmpty() ?: false
}.distinctUntilChanged().observeAsState(
val hasReactions by baseNote.live().hasReactions.observeAsState(
baseNote.zaps.isNotEmpty() || baseNote.boosts.isNotEmpty() || baseNote.reactions.isNotEmpty()
)
@@ -603,9 +596,7 @@ fun ReplyReaction(
@Composable
fun ReplyCounter(baseNote: Note, textColor: Color) {
val repliesState by baseNote.live().replies.map {
it.note.replies.size
}.observeAsState(baseNote.replies.size)
val repliesState by baseNote.live().replyCount.observeAsState(baseNote.replies.size)
SlidingAnimationCount(repliesState, textColor)
}
@@ -775,9 +766,7 @@ fun BoostIcon(baseNote: Note, iconSize: Dp = Size20dp, grayTint: Color, accountV
@Composable
fun BoostText(baseNote: Note, grayTint: Color) {
val boostState by baseNote.live().boosts.map {
it.note.boosts.size
}.distinctUntilChanged().observeAsState(baseNote.boosts.size)
val boostState by baseNote.live().boostCount.observeAsState(baseNote.boosts.size)
SlidingAnimationCount(boostState, grayTint)
}
@@ -155,7 +155,7 @@ fun UpdateReactionTypeDialog(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
CloseButton(onCancel = {
CloseButton(onPress = {
postViewModel.cancel()
onClose()
})
@@ -250,7 +250,7 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, nip47uri: String? = null, account
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
CloseButton(onCancel = {
CloseButton(onPress = {
postViewModel.cancel()
onClose()
})
@@ -57,6 +57,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import java.math.BigDecimal
@@ -187,10 +188,10 @@ class UserReactionsViewModel(val account: Account) : ViewModel() {
private var takenIntoAccount = setOf<HexKey>()
private val sdf = DateTimeFormatter.ofPattern("yyyy-MM-dd") // SimpleDateFormat()
val todaysReplyCount = _replies.map { showCount(it[today()]) }
val todaysBoostCount = _boosts.map { showCount(it[today()]) }
val todaysReactionCount = _reactions.map { showCount(it[today()]) }
val todaysZapAmount = _zaps.map { showAmountAxis(it[today()]) }
val todaysReplyCount = _replies.map { showCount(it[today()]) }.distinctUntilChanged()
val todaysBoostCount = _boosts.map { showCount(it[today()]) }.distinctUntilChanged()
val todaysReactionCount = _reactions.map { showCount(it[today()]) }.distinctUntilChanged()
val todaysZapAmount = _zaps.map { showAmountAxis(it[today()]) }.distinctUntilChanged()
fun formatDate(createAt: Long): String {
return sdf.format(
@@ -94,7 +94,7 @@ fun ZapCustomDialog(onClose: () -> Unit, accountViewModel: AccountViewModel, bas
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
CloseButton(onCancel = {
CloseButton(onPress = {
postViewModel.cancel()
onClose()
})
@@ -61,7 +61,7 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) {
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
CloseButton(onCancel = onClose)
CloseButton(onPress = onClose)
}
Column(
@@ -16,5 +16,5 @@ class RelayPoolViewModel : ViewModel() {
val isConnected = RelayPool.live.map {
it.relays.connectedRelays() > 0
}
}.distinctUntilChanged()
}
@@ -328,7 +328,7 @@ fun NoteMaster(
}
Row(verticalAlignment = Alignment.CenterVertically) {
ObserveDisplayNip05Status(baseNote, remember { Modifier.weight(1f) })
ObserveDisplayNip05Status(baseNote, remember { Modifier.weight(1f) }, accountViewModel, nav)
val geo = remember { noteEvent.getGeoHash() }
if (geo != null) {
@@ -70,7 +70,7 @@ fun AccountBackupDialog(account: Account, onClose: () -> Unit) {
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
CloseButton(onCancel = onClose)
CloseButton(onPress = onClose)
}
Column(
@@ -14,6 +14,7 @@ import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AccountState
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.ConnectivityType
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
@@ -384,6 +385,14 @@ class AccountViewModel(val account: Account) : ViewModel() {
}
}
fun createStatus(newStatus: String) {
account.createStatus(newStatus)
}
fun updateStatus(it: AddressableNote, newStatus: String) {
account.updateStatus(it, newStatus)
}
class Factory(val account: Account) : ViewModelProvider.Factory {
override fun <AccountViewModel : ViewModel> create(modelClass: Class<AccountViewModel>): AccountViewModel {
return AccountViewModel(account) as AccountViewModel
@@ -557,7 +557,7 @@ fun ChatroomHeader(
Column(modifier = Modifier.padding(start = 10.dp)) {
UsernameDisplay(baseUser)
ObserveDisplayNip05Status(baseUser)
ObserveDisplayNip05Status(baseUser, accountViewModel = accountViewModel, nav = nav)
}
}
}
@@ -669,7 +669,7 @@ fun NewSubjectView(onClose: () -> Unit, accountViewModel: AccountViewModel, room
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
CloseButton(onCancel = {
CloseButton(onPress = {
onClose()
})
@@ -56,7 +56,7 @@ fun ConnectOrbotDialog(onClose: () -> Unit, onPost: () -> Unit, portNumber: Muta
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
CloseButton(onCancel = {
CloseButton(onPress = {
onClose()
})
@@ -60,6 +60,7 @@ import com.vitorpamplona.amethyst.ui.note.ReplyReaction
import com.vitorpamplona.amethyst.ui.note.ViewCountReaction
import com.vitorpamplona.amethyst.ui.note.WatchForReports
import com.vitorpamplona.amethyst.ui.note.ZapReaction
import com.vitorpamplona.amethyst.ui.note.routeFor
import com.vitorpamplona.amethyst.ui.screen.FeedEmpty
import com.vitorpamplona.amethyst.ui.screen.FeedError
import com.vitorpamplona.amethyst.ui.screen.FeedState
@@ -373,7 +374,9 @@ private fun RenderAuthorInformation(
Row(verticalAlignment = Alignment.CenterVertically) {
ObserveDisplayNip05Status(
remember { note.author!! },
remember { Modifier.weight(1f) }
remember { Modifier.weight(1f) },
accountViewModel,
nav = nav
)
}
Row(
@@ -451,8 +454,14 @@ fun ReactionsColumn(baseNote: Note, accountViewModel: AccountViewModel, nav: (St
Spacer(modifier = Modifier.height(8.dp))
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(bottom = 75.dp, end = 20.dp)) {
val scope = rememberCoroutineScope()
ReplyReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, accountViewModel, iconSize = 40.dp) {
wantsToReplyTo = baseNote
scope.launch {
routeFor(
baseNote,
accountViewModel.userProfile()
)?.let { nav(it) }
}
}
BoostReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, accountViewModel, iconSize = 40.dp) {
wantsToQuote = baseNote
@@ -126,7 +126,7 @@ val ChatHeadlineBorders = Modifier.padding(start = 12.dp, end = 12.dp, top = 10.
val VolumeBottomIconSize = Modifier.size(70.dp).padding(10.dp)
val PinBottomIconSize = Modifier.size(70.dp).padding(10.dp)
val NIP05IconSize = Modifier.size(14.dp).padding(top = 1.dp, start = 1.dp, end = 1.dp)
val NIP05IconSize = Modifier.size(13.dp).padding(top = 1.dp, start = 1.dp, end = 1.dp)
val EditFieldModifier = Modifier
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp, top = 5.dp)