From 8f77ecd891883ae99f99f57329e0a81ab6e0a5c9 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 25 Mar 2025 19:36:39 -0400 Subject: [PATCH] Improves the rendering of the channel metadata changes Redesigns the expandable top bar for NIP-28 chats --- .../vitorpamplona/amethyst/model/Channel.kt | 29 +- .../amethyst/model/LocalCache.kt | 4 +- .../TopBarExtensibleWithBackButton.kt | 8 +- .../types/RenderChangeChannelMetadataNote.kt | 41 +-- .../feed/types/RenderCreateChannelNote.kt | 331 ++++++++++++++++-- .../header/LongChannelActionOptions.kt | 120 ------- .../header/LongPublicChatChannelHeader.kt | 112 +++++- .../header/PublicChatTopBar.kt | 1 + .../header/ShortPublicChatChannelHeader.kt | 41 +++ .../header/actions/EditChatButton.kt | 4 +- .../header/actions/JoinChatButton.kt | 4 +- .../header/actions/LeaveChatButton.kt | 4 +- .../header/actions/LinkChatButton.kt | 89 +++++ .../header/actions/OpenChatButton.kt | 74 ++++ .../header/actions/ShareChatButton.kt | 86 +++++ .../ui/screen/loggedIn/qrcode/ShowQRDialog.kt | 12 +- .../vitorpamplona/amethyst/ui/theme/Theme.kt | 19 + amethyst/src/main/res/values/strings.xml | 3 + 18 files changed, 780 insertions(+), 202 deletions(-) delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongChannelActionOptions.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LinkChatButton.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/OpenChatButton.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/ShareChatButton.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt index a7dd1365f..442e44fb0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt @@ -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( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index be5342272..541159080 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -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()} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/TopBarExtensibleWithBackButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/TopBarExtensibleWithBackButton.kt index 29d165f84..21d59394c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/TopBarExtensibleWithBackButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/TopBarExtensibleWithBackButton.kt @@ -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() } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChangeChannelMetadataNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChangeChannelMetadataNote.kt index 29aa52549..fa786c9cd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChangeChannelMetadataNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChangeChannelMetadataNote.kt @@ -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, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt index ac01d26ac..4cd262587 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt @@ -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, + bgColor: MutableState, + 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, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongChannelActionOptions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongChannelActionOptions.kt deleted file mode 100644 index 2bc588cc0..000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongChannelActionOptions.kt +++ /dev/null @@ -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) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt index 1bd744c6d..eed1fe260 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt @@ -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) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatTopBar.kt index a4a42d252..2f034d451 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatTopBar.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/ShortPublicChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/ShortPublicChatChannelHeader.kt index f7da4d439..ea1461986 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/ShortPublicChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/ShortPublicChatChannelHeader.kt @@ -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) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/EditChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/EditChatButton.kt index a187629aa..9057a8575 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/EditChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/EditChatButton.kt @@ -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) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/JoinChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/JoinChatButton.kt index 35dbf3965..ef226dc94 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/JoinChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/JoinChatButton.kt @@ -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, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LeaveChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LeaveChatButton.kt index c91f1c045..ae3d9ee54 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LeaveChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LeaveChatButton.kt @@ -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, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LinkChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LinkChatButton.kt new file mode 100644 index 000000000..49ab4ce37 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LinkChatButton.kt @@ -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, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/OpenChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/OpenChatButton.kt new file mode 100644 index 000000000..1b493f3a6 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/OpenChatButton.kt @@ -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, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/ShareChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/ShareChatButton.kt new file mode 100644 index 000000000..a3cc291e4 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/ShareChatButton.kt @@ -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, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRDialog.kt index 7d360ad6f..074281646 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRDialog.kt @@ -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, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt index 19e0126ae..00594bab8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt @@ -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 = diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index fb58c530a..463a5b749 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -213,6 +213,7 @@ with description of and picture changed chat name to + New chat profile: description to and picture to Leave @@ -616,6 +617,8 @@ Open in Cashu Wallet Copy Token + Open in Another App + No Lightning Address set Copied token to clipboard