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.core.HexKey
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address 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.NAddress
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
import com.vitorpamplona.quartz.nip19Bech32.toNEvent import com.vitorpamplona.quartz.nip19Bech32.toNEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent 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.nip28PublicChat.base.ChannelData
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.Hex
@@ -46,17 +50,30 @@ class PublicChatChannel(
idHex: String, idHex: String,
) : Channel(idHex) { ) : Channel(idHex) {
var event: ChannelCreateEvent? = null var event: ChannelCreateEvent? = null
var infoTags = EmptyTagList
var info = ChannelData(null, null, null, null) var info = ChannelData(null, null, null, null)
override fun relays() = info.relays ?: super.relays() override fun relays() = info.relays ?: super.relays()
fun toNEvent() = NEvent.create(idHex, event?.pubKey, ChannelCreateEvent.KIND, *relays().toTypedArray())
fun toNostrUri() = "nostr:${toNEvent()}"
fun updateChannelInfo( fun updateChannelInfo(
creator: User, creator: User,
channelInfo: ChannelCreateEvent, event: ChannelCreateEvent,
updatedAt: Long,
) { ) {
this.event = channelInfo this.event = event
updateChannelInfo(creator, channelInfo.channelInfo(), updatedAt) 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( fun updateChannelInfo(
@@ -116,9 +133,11 @@ class LiveActivitiesChannel(
.filter { it.contains(prefix, true) } .filter { it.contains(prefix, true) }
.isNotEmpty() .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 toATag() = ATag(address, relayHintUrl())
fun toNostrUri() = "nostr:${toNAddr()}"
} }
data class Counter( data class Counter(
@@ -1385,7 +1385,7 @@ object LocalCache {
} }
if (oldChannel.creator == null || oldChannel.creator == author) { if (oldChannel.creator == null || oldChannel.creator == author) {
if (oldChannel is PublicChatChannel) { 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) val author = getOrCreateUser(event.pubKey)
if (event.createdAt > oldChannel.updatedMetadataAt) { if (event.createdAt > oldChannel.updatedMetadataAt) {
if (oldChannel is PublicChatChannel) { if (oldChannel is PublicChatChannel) {
oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt) oldChannel.updateChannelInfo(author, event)
} }
} else { } else {
// Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()} // Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()}
@@ -21,7 +21,6 @@
package com.vitorpamplona.amethyst.ui.navigation package com.vitorpamplona.amethyst.ui.navigation
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope 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.foundation.layout.width
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.Surface
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -90,11 +90,7 @@ fun MyExtensibleTopAppBar(
) )
if (expanded.value && extendableRow != null) { if (expanded.value && extendableRow != null) {
Row( Surface(modifier = Modifier.fillMaxWidth(), tonalElevation = 10.dp) {
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
) {
Column { extendableRow() } 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.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note 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.navigation.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
@Composable @Composable
@@ -41,27 +38,19 @@ fun RenderChangeChannelMetadataNote(
nav: INav, nav: INav,
) { ) {
val noteEvent = note.event as? ChannelMetadataEvent ?: return val noteEvent = note.event as? ChannelMetadataEvent ?: return
val channelInfo = remember(noteEvent) { noteEvent.channelInfo() }
val tags =
remember(noteEvent) {
noteEvent.tags.toImmutableListOfLists()
}
val channelInfo = noteEvent.channelInfo() RenderChannelData(
val text = noteEvent.id,
note.author?.toBestDisplayName().toString() + note.toNostrUri(),
" ${stringRes(R.string.changed_chat_name_to)} '" + channelInfo,
(channelInfo.name ?: "") + tags,
"', ${stringRes(R.string.description_to)} '" + bgColor,
(channelInfo.about ?: "") + accountViewModel,
"' ${stringRes(R.string.and_picture_to)} " + nav,
(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,
) )
} }
@@ -20,19 +20,63 @@
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types 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.Composable
import androidx.compose.runtime.MutableState 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.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color 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.R
import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.model.Note 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.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav
import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel 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.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.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.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelData
import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter
@Composable @Composable
fun RenderCreateChannelNote( fun RenderCreateChannelNote(
@@ -42,27 +86,272 @@ fun RenderCreateChannelNote(
nav: INav, nav: INav,
) { ) {
val noteEvent = note.event as? ChannelCreateEvent ?: return 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 = RenderChannelData(
note.author?.toBestDisplayName().toString() + noteEvent.id,
" ${stringRes(R.string.created)} " + note.toNostrUri(),
(channelInfo.name ?: "") + channelInfo,
" ${stringRes(R.string.with_description_of)} '" + tags,
(channelInfo.about ?: "") + bgColor,
"' ${stringRes(R.string.and_picture_to)} " + accountViewModel,
(channelInfo.picture ?: "") nav,
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,
) )
} }
@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 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.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp 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.R
import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.model.PublicChatChannel 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.LoadNote
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture 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.MoreOptionsButton
import com.vitorpamplona.amethyst.ui.note.elements.NormalTimeAgo import com.vitorpamplona.amethyst.ui.note.elements.NormalTimeAgo
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel 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.stringRes
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.Size25dp 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 @Composable
fun LongPublicChatChannelHeader( fun LongPublicChatChannelHeader(
baseChannel: PublicChatChannel, 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, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
) { ) {
val channelState by baseChannel.live.observeAsState() val channelState by baseChannel.live.observeAsState()
val channel = channelState?.channel as? PublicChatChannel ?: return 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) { Row(lineModifier) {
val summary = remember(channelState) { channel.summary()?.ifBlank { null } } val summary = remember(channelState) { channel.summary()?.ifBlank { null } }
@@ -73,7 +126,7 @@ fun LongPublicChatChannelHeader(
content = summary ?: stringRes(id = R.string.groups_no_descriptor), content = summary ?: stringRes(id = R.string.groups_no_descriptor),
canPreview = false, canPreview = false,
quotesLeft = 1, quotesLeft = 1,
tags = EmptyTagList, tags = channel.infoTags,
backgroundColor = background, backgroundColor = background,
id = baseChannel.idHex, id = baseChannel.idHex,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
@@ -81,9 +134,6 @@ fun LongPublicChatChannelHeader(
) )
} }
} }
Spacer(DoubleHorzSpacer)
LongChannelActionOptions(channel, accountViewModel, nav)
} }
LoadNote(baseNoteHex = channel.idHex, accountViewModel) { loadingNote -> 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 androidx.compose.runtime.Composable
import com.vitorpamplona.amethyst.model.PublicChatChannel 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.INav
import com.vitorpamplona.amethyst.ui.navigation.TopBarExtensibleWithBackButton import com.vitorpamplona.amethyst.ui.navigation.TopBarExtensibleWithBackButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel 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.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -38,12 +40,18 @@ import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.model.PublicChatChannel 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.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.navigation.INav 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.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.stringRes
import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier 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.Size35dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
@Composable @Composable
fun ShortPublicChatChannelHeader( 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.foundation.layout.width
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.EditNote 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.material3.Icon
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -46,7 +46,7 @@ fun EditButton(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
) { ) {
Button( FilledTonalButton(
modifier = modifier =
Modifier Modifier
.padding(horizontal = 3.dp) .padding(horizontal = 3.dp)
@@ -20,7 +20,7 @@
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions 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.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -38,7 +38,7 @@ fun JoinChatButton(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
) { ) {
Button( FilledTonalButton(
modifier = HalfHalfHorzModifier, modifier = HalfHalfHorzModifier,
onClick = { accountViewModel.follow(channel) }, onClick = { accountViewModel.follow(channel) },
contentPadding = ButtonPadding, contentPadding = ButtonPadding,
@@ -20,7 +20,7 @@
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions 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.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -38,7 +38,7 @@ fun LeaveChatButton(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
) { ) {
Button( FilledTonalButton(
modifier = HalfHalfHorzModifier, modifier = HalfHalfHorzModifier,
onClick = { accountViewModel.unfollow(channel) }, onClick = { accountViewModel.unfollow(channel) },
contentPadding = ButtonPadding, 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 package com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@@ -28,8 +27,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@@ -43,7 +40,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview 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.Font14SP
import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size35dp import com.vitorpamplona.amethyst.ui.theme.Size35dp
import com.vitorpamplona.amethyst.ui.theme.largeProfilePictureModifier
import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata
@Preview @Preview
@@ -135,12 +132,7 @@ fun ShowQRDialog(
robot = user.pubkeyHex, robot = user.pubkeyHex,
model = user.profilePicture(), model = user.profilePicture(),
contentDescription = stringRes(R.string.profile_image), contentDescription = stringRes(R.string.profile_image),
modifier = modifier = MaterialTheme.colorScheme.largeProfilePictureModifier,
Modifier
.width(120.dp)
.height(120.dp)
.clip(shape = CircleShape)
.border(3.dp, MaterialTheme.colorScheme.onBackground, CircleShape),
loadProfilePicture = accountViewModel.settings.showProfilePictures.value, loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, 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.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ColorScheme import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -273,6 +275,20 @@ val DarkLargeRelayIconModifier =
.size(Size55dp) .size(Size55dp)
.clip(shape = CircleShape) .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 RichTextDefaults = RichTextStyle().resolveDefaults()
val MarkDownStyleOnDark = val MarkDownStyleOnDark =
@@ -447,6 +463,9 @@ val ColorScheme.largeRelayIconModifier: Modifier
val ColorScheme.selectedReactionBoxModifier: Modifier val ColorScheme.selectedReactionBoxModifier: Modifier
get() = if (isLight) LightSelectedReactionBoxModifier else DarkSelectedReactionBoxModifier get() = if (isLight) LightSelectedReactionBoxModifier else DarkSelectedReactionBoxModifier
val ColorScheme.largeProfilePictureModifier: Modifier
get() = if (isLight) lightLargeProfilePictureModifier else darkLargeProfilePictureModifier
val chartLightColors = val chartLightColors =
VicoTheme( VicoTheme(
candlestickCartesianLayerColors = candlestickCartesianLayerColors =
+3
View File
@@ -213,6 +213,7 @@
<string name="with_description_of">with description of</string> <string name="with_description_of">with description of</string>
<string name="and_picture">and picture</string> <string name="and_picture">and picture</string>
<string name="changed_chat_name_to">changed chat name to</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="description_to">description to</string>
<string name="and_picture_to">and picture to</string> <string name="and_picture_to">and picture to</string>
<string name="leave">Leave</string> <string name="leave">Leave</string>
@@ -616,6 +617,8 @@
<string name="cashu_redeem_to_cashu">Open in Cashu Wallet</string> <string name="cashu_redeem_to_cashu">Open in Cashu Wallet</string>
<string name="cashu_copy_token">Copy Token</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="no_lightning_address_set">No Lightning Address set</string>
<string name="copied_token_to_clipboard">Copied token to clipboard</string> <string name="copied_token_to_clipboard">Copied token to clipboard</string>