Improves the rendering of the channel metadata changes

Redesigns the expandable top bar for NIP-28 chats
This commit is contained in:
Vitor Pamplona
2025-03-25 19:36:39 -04:00
parent a23ad2dde7
commit 8f77ecd891
18 changed files with 780 additions and 202 deletions
@@ -33,9 +33,13 @@ import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
import com.vitorpamplona.quartz.nip19Bech32.toNEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelData
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import com.vitorpamplona.quartz.utils.Hex
@@ -46,17 +50,30 @@ class PublicChatChannel(
idHex: String,
) : Channel(idHex) {
var event: ChannelCreateEvent? = null
var infoTags = EmptyTagList
var info = ChannelData(null, null, null, null)
override fun relays() = info.relays ?: super.relays()
fun toNEvent() = NEvent.create(idHex, event?.pubKey, ChannelCreateEvent.KIND, *relays().toTypedArray())
fun toNostrUri() = "nostr:${toNEvent()}"
fun updateChannelInfo(
creator: User,
channelInfo: ChannelCreateEvent,
updatedAt: Long,
event: ChannelCreateEvent,
) {
this.event = channelInfo
updateChannelInfo(creator, channelInfo.channelInfo(), updatedAt)
this.event = event
this.infoTags = event.tags.toImmutableListOfLists()
updateChannelInfo(creator, event.channelInfo(), event.createdAt)
}
fun updateChannelInfo(
creator: User,
event: ChannelMetadataEvent,
) {
this.infoTags = event.tags.toImmutableListOfLists()
updateChannelInfo(creator, event.channelInfo(), event.createdAt)
}
fun updateChannelInfo(
@@ -116,9 +133,11 @@ class LiveActivitiesChannel(
.filter { it.contains(prefix, true) }
.isNotEmpty()
fun toNAddr() = NAddress.create(address.kind, address.pubKeyHex, address.dTag, relayHintUrl())
fun toNAddr() = NAddress.create(address.kind, address.pubKeyHex, address.dTag, *relays().toTypedArray())
fun toATag() = ATag(address, relayHintUrl())
fun toNostrUri() = "nostr:${toNAddr()}"
}
data class Counter(
@@ -1385,7 +1385,7 @@ object LocalCache {
}
if (oldChannel.creator == null || oldChannel.creator == author) {
if (oldChannel is PublicChatChannel) {
oldChannel.updateChannelInfo(author, event, event.createdAt)
oldChannel.updateChannelInfo(author, event)
}
}
}
@@ -1404,7 +1404,7 @@ object LocalCache {
val author = getOrCreateUser(event.pubKey)
if (event.createdAt > oldChannel.updatedMetadataAt) {
if (oldChannel is PublicChatChannel) {
oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt)
oldChannel.updateChannelInfo(author, event)
}
} else {
// Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()}
@@ -21,7 +21,6 @@
package com.vitorpamplona.amethyst.ui.navigation
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
@@ -30,6 +29,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.IconButton
import androidx.compose.material3.Surface
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
@@ -90,11 +90,7 @@ fun MyExtensibleTopAppBar(
)
if (expanded.value && extendableRow != null) {
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
) {
Surface(modifier = Modifier.fillMaxWidth(), tonalElevation = 10.dp) {
Column { extendableRow() }
}
}
@@ -22,15 +22,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
@Composable
@@ -41,27 +38,19 @@ fun RenderChangeChannelMetadataNote(
nav: INav,
) {
val noteEvent = note.event as? ChannelMetadataEvent ?: return
val channelInfo = remember(noteEvent) { noteEvent.channelInfo() }
val tags =
remember(noteEvent) {
noteEvent.tags.toImmutableListOfLists()
}
val channelInfo = noteEvent.channelInfo()
val text =
note.author?.toBestDisplayName().toString() +
" ${stringRes(R.string.changed_chat_name_to)} '" +
(channelInfo.name ?: "") +
"', ${stringRes(R.string.description_to)} '" +
(channelInfo.about ?: "") +
"' ${stringRes(R.string.and_picture_to)} " +
(channelInfo.picture ?: "")
TranslatableRichTextViewer(
content = text,
canPreview = true,
quotesLeft = 0,
modifier = Modifier,
tags = note.author?.info?.tags ?: EmptyTagList,
backgroundColor = bgColor,
id = note.idHex,
callbackUri = note.toNostrUri(),
accountViewModel = accountViewModel,
nav = nav,
RenderChannelData(
noteEvent.id,
note.toNostrUri(),
channelInfo,
tags,
bgColor,
accountViewModel,
nav,
)
}
@@ -20,19 +20,63 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
import com.vitorpamplona.amethyst.service.Nip11Retriever
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.RelayInformationDialog
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.RelayIconFilter
import com.vitorpamplona.amethyst.ui.theme.Size20dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
import com.vitorpamplona.amethyst.ui.theme.largeProfilePictureModifier
import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists
import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelData
import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter
@Composable
fun RenderCreateChannelNote(
@@ -42,27 +86,272 @@ fun RenderCreateChannelNote(
nav: INav,
) {
val noteEvent = note.event as? ChannelCreateEvent ?: return
val channelInfo = remember { noteEvent.channelInfo() }
val channelInfo = remember(noteEvent) { noteEvent.channelInfo() }
val tags =
remember(noteEvent) {
noteEvent.tags.toImmutableListOfLists()
}
val text =
note.author?.toBestDisplayName().toString() +
" ${stringRes(R.string.created)} " +
(channelInfo.name ?: "") +
" ${stringRes(R.string.with_description_of)} '" +
(channelInfo.about ?: "") +
"' ${stringRes(R.string.and_picture_to)} " +
(channelInfo.picture ?: "")
TranslatableRichTextViewer(
content = text,
canPreview = true,
quotesLeft = 0,
modifier = Modifier,
tags = note.author?.info?.tags ?: EmptyTagList,
backgroundColor = bgColor,
id = note.idHex,
callbackUri = note.toNostrUri(),
accountViewModel = accountViewModel,
nav = nav,
RenderChannelData(
noteEvent.id,
note.toNostrUri(),
channelInfo,
tags,
bgColor,
accountViewModel,
nav,
)
}
@Preview
@Composable
fun RenderChannelDataPreview() {
ThemeComparisonRow {
RenderChannelData(
id = "bbaacc",
uri = "nostr:nevent1...",
channelInfo =
ChannelData(
"My Group",
"Testing About me",
"http://test.com",
listOf("wss://nostr.mom", "wss://nos.lol"),
),
tags = EmptyTagList,
bgColor = remember { mutableStateOf(Color.Transparent) },
accountViewModel = mockAccountViewModel(),
nav = EmptyNav,
)
}
}
@Composable
fun RenderChannelData(
id: HexKey,
uri: String,
channelInfo: ChannelData,
tags: ImmutableListOfLists<String>,
bgColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: INav,
) {
Column {
Row {
TranslatableRichTextViewer(
content = stringRes(R.string.changed_chat_profile_to),
canPreview = true,
quotesLeft = 1,
modifier = Modifier,
tags = tags,
backgroundColor = bgColor,
id = id,
callbackUri = uri,
accountViewModel = accountViewModel,
nav = nav,
)
}
channelInfo.picture?.let {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth().padding(top = 10.dp),
) {
RobohashFallbackAsyncImage(
robot = id,
model = it,
contentDescription = stringRes(R.string.channel_image),
modifier = MaterialTheme.colorScheme.largeProfilePictureModifier,
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
)
}
}
channelInfo.name?.let {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth().padding(top = 10.dp),
) {
CreateTextWithEmoji(
text = it,
tags = tags,
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
)
}
}
channelInfo.about?.let {
Row(
modifier = Modifier.fillMaxWidth().padding(top = 10.dp),
) {
TranslatableRichTextViewer(
content = it,
canPreview = true,
quotesLeft = 1,
modifier = Modifier,
tags = tags,
backgroundColor = bgColor,
id = id,
callbackUri = uri,
accountViewModel = accountViewModel,
nav = nav,
)
}
}
channelInfo.relays?.let {
Text(
stringRes(R.string.public_chat_relays_title) + ": ",
modifier = Modifier.fillMaxWidth().padding(top = 10.dp),
)
it.forEach {
Spacer(StdVertSpacer)
RenderRelayLinePublicChat(
it,
accountViewModel,
nav,
)
}
}
}
}
@Preview
@Composable
fun RenderRelayLinePreview() {
ThemeComparisonRow {
RenderRelayLine(
"wss://nos.lol",
"http://icon.com/icon.ico",
Modifier,
true,
true,
)
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun RenderRelayLinePublicChat(
dirtyUrl: String,
accountViewModel: AccountViewModel,
nav: INav,
) {
@Suppress("ProduceStateDoesNotAssignValue")
val relayInfo by produceState(
initialValue = Nip11CachedRetriever.getFromCache(dirtyUrl),
) {
if (value == null) {
accountViewModel.retrieveRelayDocument(
dirtyUrl,
onInfo = {
value = it
},
onError = { url, errorCode, exceptionMessage ->
},
)
}
}
var openRelayDialog by remember { mutableStateOf(false) }
val info =
remember(dirtyUrl) {
RelayBriefInfoCache.get(RelayUrlFormatter.normalize(dirtyUrl))
}
if (openRelayDialog && relayInfo != null) {
RelayInformationDialog(
onClose = { openRelayDialog = false },
relayInfo = relayInfo!!,
relayBriefInfo = info,
accountViewModel = accountViewModel,
nav = nav,
)
}
val clipboardManager = LocalClipboardManager.current
val clickableModifier =
remember(dirtyUrl) {
Modifier.combinedClickable(
onLongClick = {
clipboardManager.setText(AnnotatedString(dirtyUrl))
},
onClick = {
accountViewModel.retrieveRelayDocument(
dirtyUrl,
onInfo = {
openRelayDialog = true
},
onError = { url, errorCode, exceptionMessage ->
accountViewModel.toastManager.toast(
R.string.unable_to_download_relay_document,
when (errorCode) {
Nip11Retriever.ErrorCode.FAIL_TO_ASSEMBLE_URL ->
R.string.relay_information_document_error_failed_to_assemble_url
Nip11Retriever.ErrorCode.FAIL_TO_REACH_SERVER ->
R.string.relay_information_document_error_failed_to_reach_server
Nip11Retriever.ErrorCode.FAIL_TO_PARSE_RESULT ->
R.string.relay_information_document_error_failed_to_parse_response
Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS ->
R.string.relay_information_document_error_failed_with_http
},
url,
exceptionMessage ?: errorCode.toString(),
)
},
)
},
)
}
RenderRelayLine(
info.displayUrl,
relayInfo?.icon ?: info.favIcon,
clickableModifier,
showPicture = accountViewModel.settings.showProfilePictures.value,
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
)
}
@Composable
fun RenderRelayLine(
url: String,
icon: String?,
modifier: Modifier = Modifier,
showPicture: Boolean = true,
loadRobohash: Boolean = true,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = modifier,
) {
Text(" -")
Spacer(modifier = StdHorzSpacer)
RobohashFallbackAsyncImage(
robot = url,
model = icon,
contentDescription = stringRes(id = R.string.relay_info, url),
colorFilter = RelayIconFilter,
modifier =
Modifier
.size(Size20dp)
.clip(shape = CircleShape),
loadProfilePicture = showPicture,
loadRobohash = loadRobohash,
)
Spacer(modifier = StdHorzSpacer)
Text(
text = url,
)
}
}
@@ -1,120 +0,0 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.note.LikeReaction
import com.vitorpamplona.amethyst.ui.note.ZapReaction
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions.EditButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions.JoinChatButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions.LeaveChatButton
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.quartz.nip01Core.tags.events.isTaggedEvent
@Composable
fun ShortChannelActionOptions(
channel: PublicChatChannel,
accountViewModel: AccountViewModel,
nav: INav,
) {
LoadNote(baseNoteHex = channel.idHex, accountViewModel) {
it?.let {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = RowColSpacing) {
LikeReaction(
baseNote = it,
grayTint = MaterialTheme.colorScheme.onSurface,
accountViewModel = accountViewModel,
nav,
)
ZapReaction(
baseNote = it,
grayTint = MaterialTheme.colorScheme.onSurface,
accountViewModel = accountViewModel,
nav = nav,
)
Spacer(modifier = StdHorzSpacer)
}
}
}
WatchChannelFollows(channel, accountViewModel) { isFollowing ->
if (!isFollowing) {
JoinChatButton(channel, accountViewModel, nav)
}
}
}
@Composable
fun LongChannelActionOptions(
channel: PublicChatChannel,
accountViewModel: AccountViewModel,
nav: INav,
) {
val isMe by
remember(accountViewModel) {
derivedStateOf { channel.creator == accountViewModel.account.userProfile() }
}
if (isMe) {
EditButton(channel, accountViewModel, nav)
}
WatchChannelFollows(channel, accountViewModel) { isFollowing ->
if (isFollowing) {
LeaveChatButton(channel, accountViewModel, nav)
}
}
}
@Composable
private fun WatchChannelFollows(
channel: PublicChatChannel,
accountViewModel: AccountViewModel,
content: @Composable (Boolean) -> Unit,
) {
val isFollowing by
accountViewModel
.userProfile()
.live()
.follows
.map { it.user.latestContactList?.isTaggedEvent(channel.idHex) ?: false }
.distinctUntilChanged()
.observeAsState(
accountViewModel.userProfile().latestContactList?.isTaggedEvent(channel.idHex) ?: false,
)
content(isFollowing)
}
@@ -20,25 +20,35 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
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 com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
@@ -46,21 +56,64 @@ import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton
import com.vitorpamplona.amethyst.ui.note.elements.NormalTimeAgo
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions.EditButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions.LeaveChatButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions.LinkChatButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions.OpenChatButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions.ShareChatButton
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.Size25dp
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.largeProfilePictureModifier
import com.vitorpamplona.quartz.nip01Core.tags.events.isTaggedEvent
@Composable
fun LongPublicChatChannelHeader(
baseChannel: PublicChatChannel,
lineModifier: Modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp),
lineModifier: Modifier = Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 5.dp),
accountViewModel: AccountViewModel,
nav: INav,
) {
val channelState by baseChannel.live.observeAsState()
val channel = channelState?.channel as? PublicChatChannel ?: return
Spacer(StdVertSpacer)
channel.info.picture?.let {
Row(
horizontalArrangement = Arrangement.Center,
modifier = lineModifier,
) {
RobohashFallbackAsyncImage(
robot = baseChannel.idHex,
model = it,
contentDescription = stringRes(R.string.channel_image),
modifier = MaterialTheme.colorScheme.largeProfilePictureModifier,
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
)
}
}
channel.info.name?.let {
Row(
horizontalArrangement = Arrangement.Center,
modifier = lineModifier,
) {
CreateTextWithEmoji(
text = it,
tags = channel.infoTags,
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
)
}
}
Row(horizontalArrangement = Arrangement.Center, modifier = lineModifier) {
LongChannelActionOptions(channel, accountViewModel, nav)
}
Row(lineModifier) {
val summary = remember(channelState) { channel.summary()?.ifBlank { null } }
@@ -73,7 +126,7 @@ fun LongPublicChatChannelHeader(
content = summary ?: stringRes(id = R.string.groups_no_descriptor),
canPreview = false,
quotesLeft = 1,
tags = EmptyTagList,
tags = channel.infoTags,
backgroundColor = background,
id = baseChannel.idHex,
accountViewModel = accountViewModel,
@@ -81,9 +134,6 @@ fun LongPublicChatChannelHeader(
)
}
}
Spacer(DoubleHorzSpacer)
LongChannelActionOptions(channel, accountViewModel, nav)
}
LoadNote(baseNoteHex = channel.idHex, accountViewModel) { loadingNote ->
@@ -120,4 +170,54 @@ fun LongPublicChatChannelHeader(
}
}
}
Spacer(StdVertSpacer)
}
@Composable
fun LongChannelActionOptions(
channel: PublicChatChannel,
accountViewModel: AccountViewModel,
nav: INav,
) {
val isMe by
remember(accountViewModel) {
derivedStateOf { channel.creator == accountViewModel.account.userProfile() }
}
OpenChatButton(channel, accountViewModel, nav)
LinkChatButton(channel, accountViewModel, nav)
ShareChatButton(channel, accountViewModel, nav)
if (isMe) {
EditButton(channel, accountViewModel, nav)
}
WatchChannelFollows(channel, accountViewModel) { isFollowing ->
if (isFollowing) {
LeaveChatButton(channel, accountViewModel, nav)
}
}
}
@Composable
fun WatchChannelFollows(
channel: PublicChatChannel,
accountViewModel: AccountViewModel,
content: @Composable (Boolean) -> Unit,
) {
val isFollowing by
accountViewModel
.userProfile()
.live()
.follows
.map { it.user.latestContactList?.isTaggedEvent(channel.idHex) ?: false }
.distinctUntilChanged()
.observeAsState(
accountViewModel.userProfile().latestContactList?.isTaggedEvent(channel.idHex) ?: false,
)
content(isFollowing)
}
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28
import androidx.compose.runtime.Composable
import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav.popBack
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.navigation.TopBarExtensibleWithBackButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -23,8 +23,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -38,12 +40,18 @@ import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.note.LikeReaction
import com.vitorpamplona.amethyst.ui.note.ZapReaction
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions.JoinChatButton
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing
import com.vitorpamplona.amethyst.ui.theme.Size35dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
@Composable
fun ShortPublicChatChannelHeader(
@@ -94,3 +102,36 @@ fun ShortPublicChatChannelHeader(
}
}
}
@Composable
fun ShortChannelActionOptions(
channel: PublicChatChannel,
accountViewModel: AccountViewModel,
nav: INav,
) {
LoadNote(baseNoteHex = channel.idHex, accountViewModel) {
it?.let {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = RowColSpacing) {
LikeReaction(
baseNote = it,
grayTint = MaterialTheme.colorScheme.onSurface,
accountViewModel = accountViewModel,
nav,
)
ZapReaction(
baseNote = it,
grayTint = MaterialTheme.colorScheme.onSurface,
accountViewModel = accountViewModel,
nav = nav,
)
Spacer(modifier = StdHorzSpacer)
}
}
}
WatchChannelFollows(channel, accountViewModel) { isFollowing ->
if (!isFollowing) {
JoinChatButton(channel, accountViewModel, nav)
}
}
}
@@ -24,7 +24,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.EditNote
import androidx.compose.material3.Button
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -46,7 +46,7 @@ fun EditButton(
accountViewModel: AccountViewModel,
nav: INav,
) {
Button(
FilledTonalButton(
modifier =
Modifier
.padding(horizontal = 3.dp)
@@ -20,7 +20,7 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions
import androidx.compose.material3.Button
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
@@ -38,7 +38,7 @@ fun JoinChatButton(
accountViewModel: AccountViewModel,
nav: INav,
) {
Button(
FilledTonalButton(
modifier = HalfHalfHorzModifier,
onClick = { accountViewModel.follow(channel) },
contentPadding = ButtonPadding,
@@ -20,7 +20,7 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions
import androidx.compose.material3.Button
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
@@ -38,7 +38,7 @@ fun LeaveChatButton(
accountViewModel: AccountViewModel,
nav: INav,
) {
Button(
FilledTonalButton(
modifier = HalfHalfHorzModifier,
onClick = { accountViewModel.unfollow(channel) },
contentPadding = ButtonPadding,
@@ -0,0 +1,89 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions
import android.content.Intent
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.ZeroPadding
@Composable
fun LinkChatButton(
channel: PublicChatChannel,
accountViewModel: AccountViewModel,
nav: INav,
) {
val context = LocalContext.current
FilledTonalButton(
modifier =
Modifier
.padding(horizontal = 3.dp)
.width(50.dp),
onClick = {
val intent = Intent(Intent.ACTION_VIEW, channel.toNostrUri().toUri())
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
ContextCompat.startActivity(context, intent, null)
val sendIntent =
Intent().apply {
action = Intent.ACTION_SEND
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, channel.toNostrUri())
putExtra(Intent.EXTRA_TITLE, stringRes(context, R.string.quick_action_share_browser_link))
}
val shareIntent =
Intent.createChooser(
sendIntent,
stringRes(context, R.string.quick_action_copy_note_id),
)
ContextCompat.startActivity(context, shareIntent, null)
},
contentPadding = ZeroPadding,
) {
Icon(
tint = Color.White,
imageVector = Icons.Default.ContentCopy,
contentDescription = stringRes(R.string.quick_action_copy_note_id),
modifier = Size20Modifier,
)
}
}
@@ -0,0 +1,74 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions
import android.content.Intent
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.OpenInNew
import androidx.compose.material.icons.filled.OpenInNew
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.ZeroPadding
@Composable
fun OpenChatButton(
channel: PublicChatChannel,
accountViewModel: AccountViewModel,
nav: INav,
) {
val context = LocalContext.current
FilledTonalButton(
modifier =
Modifier
.padding(horizontal = 3.dp)
.width(50.dp),
onClick = {
val intent = Intent(Intent.ACTION_VIEW, channel.toNostrUri().toUri())
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
ContextCompat.startActivity(context, intent, null)
},
contentPadding = ZeroPadding,
) {
Icon(
tint = Color.White,
imageVector = Icons.AutoMirrored.Filled.OpenInNew,
contentDescription = stringRes(R.string.quick_actions_open_in_another_app),
modifier = Size20Modifier,
)
}
}
@@ -0,0 +1,86 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions
import android.content.Intent
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.note.njumpLink
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.ZeroPadding
@Composable
fun ShareChatButton(
channel: PublicChatChannel,
accountViewModel: AccountViewModel,
nav: INav,
) {
val context = LocalContext.current
FilledTonalButton(
modifier =
Modifier
.padding(horizontal = 3.dp)
.width(50.dp),
onClick = {
val sendIntent =
Intent().apply {
action = Intent.ACTION_SEND
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, njumpLink(channel.toNEvent()))
putExtra(Intent.EXTRA_TITLE, stringRes(context, R.string.quick_action_share_browser_link))
}
val shareIntent =
Intent.createChooser(
sendIntent,
stringRes(context, R.string.quick_action_share),
)
ContextCompat.startActivity(context, shareIntent, null)
},
contentPadding = ZeroPadding,
) {
Icon(
tint = Color.White,
imageVector = Icons.Default.Share,
contentDescription = stringRes(R.string.quick_action_share),
modifier = Size20Modifier,
)
}
}
@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -28,8 +27,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.IconButton
@@ -43,7 +40,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
@@ -65,6 +61,7 @@ import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Font14SP
import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size35dp
import com.vitorpamplona.amethyst.ui.theme.largeProfilePictureModifier
import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata
@Preview
@@ -135,12 +132,7 @@ fun ShowQRDialog(
robot = user.pubkeyHex,
model = user.profilePicture(),
contentDescription = stringRes(R.string.profile_image),
modifier =
Modifier
.width(120.dp)
.height(120.dp)
.clip(shape = CircleShape)
.border(3.dp, MaterialTheme.colorScheme.onBackground, CircleShape),
modifier = MaterialTheme.colorScheme.largeProfilePictureModifier,
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
)
@@ -30,8 +30,10 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
@@ -273,6 +275,20 @@ val DarkLargeRelayIconModifier =
.size(Size55dp)
.clip(shape = CircleShape)
val darkLargeProfilePictureModifier =
Modifier
.width(120.dp)
.height(120.dp)
.clip(shape = CircleShape)
.border(3.dp, DarkColorPalette.background, CircleShape)
val lightLargeProfilePictureModifier =
Modifier
.width(120.dp)
.height(120.dp)
.clip(shape = CircleShape)
.border(3.dp, LightColorPalette.background, CircleShape)
val RichTextDefaults = RichTextStyle().resolveDefaults()
val MarkDownStyleOnDark =
@@ -447,6 +463,9 @@ val ColorScheme.largeRelayIconModifier: Modifier
val ColorScheme.selectedReactionBoxModifier: Modifier
get() = if (isLight) LightSelectedReactionBoxModifier else DarkSelectedReactionBoxModifier
val ColorScheme.largeProfilePictureModifier: Modifier
get() = if (isLight) lightLargeProfilePictureModifier else darkLargeProfilePictureModifier
val chartLightColors =
VicoTheme(
candlestickCartesianLayerColors =
+3
View File
@@ -213,6 +213,7 @@
<string name="with_description_of">with description of</string>
<string name="and_picture">and picture</string>
<string name="changed_chat_name_to">changed chat name to</string>
<string name="changed_chat_profile_to">New chat profile:</string>
<string name="description_to">description to</string>
<string name="and_picture_to">and picture to</string>
<string name="leave">Leave</string>
@@ -616,6 +617,8 @@
<string name="cashu_redeem_to_cashu">Open in Cashu Wallet</string>
<string name="cashu_copy_token">Copy Token</string>
<string name="quick_actions_open_in_another_app">Open in Another App</string>
<string name="no_lightning_address_set">No Lightning Address set</string>
<string name="copied_token_to_clipboard">Copied token to clipboard</string>