diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 3bde8f1b2..33ced9e5d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -136,6 +136,7 @@ import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEve import com.vitorpamplona.quartz.nip30CustomEmoji.taggedEmojis import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning +import com.vitorpamplona.quartz.nip37Drafts.DraftBuilder import com.vitorpamplona.quartz.nip37Drafts.DraftEvent import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent @@ -2103,6 +2104,27 @@ class Account( Amethyst.instance.client.send(signedEvent, relayList = relayList) } + fun sendNip95Privately( + data: FileStorageEvent, + signedEvent: FileStorageHeaderEvent, + relayList: List, + ) { + val connect = + relayList.map { + val normalizedUrl = RelayUrlFormatter.normalize(it) + RelaySetupInfoToConnect( + normalizedUrl, + shouldUseTorForClean(normalizedUrl), + true, + true, + setOf(FeedType.GLOBAL), + ) + } + + Amethyst.instance.client.sendPrivately(data, relayList = connect) + Amethyst.instance.client.sendPrivately(signedEvent, relayList = connect) + } + fun sendHeader( signedEvent: Event, relayList: List, @@ -2286,6 +2308,29 @@ class Account( } } + fun signAndSendPrivately( + template: EventTemplate, + relayList: List, + ) { + signer.sign(template) { + LocalCache.justConsume(it, null) + Amethyst.instance.client.sendPrivately(it, relayList = convertRelayList(relayList)) + } + } + + fun signAndSend( + template: EventTemplate, + relayList: List, + broadcastNotes: Set, + ) { + signer.sign(template) { + LocalCache.justConsume(it, null) + Amethyst.instance.client.send(it, relayList = relayList) + + broadcastNotes.forEach { it.event?.let { Amethyst.instance.client.send(it, relayList = relayList) } } + } + } + fun signAndSend( draftTag: String?, template: EventTemplate, @@ -2359,6 +2404,16 @@ class Account( signAndSend(draftTag, template, relayList, broadcastNotes) } + fun createAndSendDraft( + draftTag: String, + template: EventTemplate, + ) { + val rumor = signer.assembleRumor(template) + DraftBuilder.encryptAndSign(draftTag, rumor, signer) { draftEvent -> + sendDraftEvent(draftEvent) + } + } + fun deleteDraft(draftTag: String) { val key = DraftEvent.createAddressTag(userProfile().pubkeyHex, draftTag) LocalCache.getAddressableNoteIfExists(key)?.let { note -> @@ -2682,6 +2737,18 @@ class Account( LocalCache.justConsume(draftEvent, null) } + fun convertRelayList(broadcast: List): List = + broadcast.map { + val normalizedUrl = RelayUrlFormatter.normalize(it) + RelaySetupInfoToConnect( + normalizedUrl, + shouldUseTorForClean(normalizedUrl), + true, + true, + setOf(FeedType.GLOBAL), + ) + } + fun broadcastPrivately(signedEvents: NIP17Factory.Result) { val mine = signedEvents.wraps.filter { (it.recipientPubKey() == signer.pubKey) } 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 96c4d015e..f36b2498a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -2412,9 +2412,12 @@ object LocalCache { } is ChannelMessageEvent -> { draft.channelId()?.let { channelId -> - checkGetOrCreateChannel(channelId)?.let { channel -> - channel.addNote(note, null) - } + checkGetOrCreateChannel(channelId)?.addNote(note, null) + } + } + is LiveActivitiesChatMessageEvent -> { + draft.activityAddress()?.let { channelId -> + checkGetOrCreateChannel(channelId.toValue())?.addNote(note, null) } } is TextNoteEvent -> { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/JoinUserOrChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/JoinUserOrChannelView.kt index 8caff509f..5d31fbf92 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/JoinUserOrChannelView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/JoinUserOrChannelView.kt @@ -81,7 +81,7 @@ import com.vitorpamplona.amethyst.ui.note.SearchIcon import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.list.ChannelName +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.ChannelName import com.vitorpamplona.amethyst.ui.screen.loggedIn.search.SearchBarViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewChannelView.kt deleted file mode 100644 index cbda34dba..000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewChannelView.kt +++ /dev/null @@ -1,155 +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.actions - -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.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.LocalTextStyle -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.input.KeyboardCapitalization -import androidx.compose.ui.text.style.TextDirection -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties -import androidx.lifecycle.viewmodel.compose.viewModel -import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.PublicChatChannel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton -import com.vitorpamplona.amethyst.ui.screen.loggedIn.PostButton -import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.placeholderText - -@Composable -fun NewChannelView( - onClose: () -> Unit, - accountViewModel: AccountViewModel, - channel: PublicChatChannel? = null, -) { - val postViewModel: NewChannelViewModel = viewModel() - postViewModel.load(accountViewModel.account, channel) - - Dialog( - onDismissRequest = { onClose() }, - properties = - DialogProperties( - dismissOnClickOutside = false, - ), - ) { - Surface { - Column( - modifier = Modifier.padding(10.dp).verticalScroll(rememberScrollState()), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - CloseButton( - onPress = { - postViewModel.clear() - onClose() - }, - ) - - PostButton( - onPost = { - postViewModel.create() - onClose() - }, - postViewModel.channelName.value.text - .isNotBlank(), - ) - } - - Spacer(modifier = Modifier.height(15.dp)) - - OutlinedTextField( - label = { Text(text = stringRes(R.string.channel_name)) }, - modifier = Modifier.fillMaxWidth(), - value = postViewModel.channelName.value, - onValueChange = { postViewModel.channelName.value = it }, - placeholder = { - Text( - text = stringRes(R.string.my_awesome_group), - color = MaterialTheme.colorScheme.placeholderText, - ) - }, - keyboardOptions = - KeyboardOptions.Default.copy( - capitalization = KeyboardCapitalization.Sentences, - ), - textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content), - ) - - Spacer(modifier = Modifier.height(15.dp)) - - OutlinedTextField( - label = { Text(text = stringRes(R.string.picture_url)) }, - modifier = Modifier.fillMaxWidth(), - value = postViewModel.channelPicture.value, - onValueChange = { postViewModel.channelPicture.value = it }, - placeholder = { - Text( - text = "http://mygroup.com/logo.jpg", - color = MaterialTheme.colorScheme.placeholderText, - ) - }, - ) - - Spacer(modifier = Modifier.height(15.dp)) - - OutlinedTextField( - label = { Text(text = stringRes(R.string.description)) }, - modifier = Modifier.fillMaxWidth().height(100.dp), - value = postViewModel.channelDescription.value, - onValueChange = { postViewModel.channelDescription.value = it }, - placeholder = { - Text( - text = stringRes(R.string.about_us), - color = MaterialTheme.colorScheme.placeholderText, - ) - }, - keyboardOptions = - KeyboardOptions.Default.copy( - capitalization = KeyboardCapitalization.Sentences, - ), - textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content), - maxLines = 10, - ) - } - } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/RelayInformationDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/RelayInformationDialog.kt index a6aec404d..eeb077170 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/RelayInformationDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/RelayInformationDialog.kt @@ -62,7 +62,7 @@ import com.vitorpamplona.amethyst.ui.note.UserCompose import com.vitorpamplona.amethyst.ui.note.timeAgo import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.list.LoadUser +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index 8772d93b2..1a5d99cb7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.components -import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -94,7 +93,7 @@ import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.toShortenHex import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.list.LoadUser +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.theme.CashuCardBorders import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ChatHeaderLayout.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ChatHeaderLayout.kt index 77e7e68c7..804d7f5ad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ChatHeaderLayout.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ChatHeaderLayout.kt @@ -41,7 +41,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.list.NewItemsBubble +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.NewItemsBubble import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 6c84bdabc..45c7228a0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -60,10 +60,10 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoadRedirectScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.NewPostScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarks.BookmarkListScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.list.MessagesScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomByAuthorScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.public.ChannelScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ChannelScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.MessagesScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.CommunityScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.DiscoverScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.drafts.DraftListScreen diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt index ebe9ac545..75c952aea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt @@ -78,11 +78,11 @@ import com.vitorpamplona.amethyst.ui.layouts.LeftPictureLayout import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.note.elements.BannerImage import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.public.ChannelHeader -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.public.EndedFlag -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.public.LiveFlag -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.public.OfflineFlag -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.public.ScheduledFlag +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ChannelHeader +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.EndedFlag +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveFlag +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.OfflineFlag +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.ScheduledFlag import com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.observeAppDefinition import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CheckIfVideoIsOnline import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 64f7b6fa9..38d0257c8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -123,7 +123,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderTorrentComment import com.vitorpamplona.amethyst.ui.note.types.RenderWikiContent import com.vitorpamplona.amethyst.ui.note.types.VideoDisplay import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.public.RenderChannelHeader +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.RenderChannelHeader import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.Font12SP diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt index e0be54e8d..8993b6812 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt @@ -44,7 +44,7 @@ import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.list.LoadUser +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/TimeAgo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/TimeAgo.kt index 3471a1d87..ef9db9c94 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/TimeAgo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/TimeAgo.kt @@ -23,12 +23,18 @@ package com.vitorpamplona.amethyst.ui.note.elements 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.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextOverflow +import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.note.timeAgo +import com.vitorpamplona.amethyst.ui.note.timeAgoShort +import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.placeholderText @Composable @@ -48,3 +54,21 @@ fun TimeAgo(time: Long) { maxLines = 1, ) } + +@Composable +fun NormalTimeAgo( + baseNote: Note, + modifier: Modifier, +) { + val nowStr = stringRes(id = R.string.now) + + val time by + remember(baseNote) { derivedStateOf { timeAgoShort(baseNote.createdAt() ?: 0, nowStr) } } + + Text( + text = time, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = modifier, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChannelMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChannelMessage.kt index ba40ae89f..f6b911186 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChannelMessage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChannelMessage.kt @@ -33,7 +33,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.public.ChannelHeader +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ChannelHeader import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.replyModifier import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessageEncryptedFile.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessageEncryptedFile.kt index 70f58baa6..3a2f8c596 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessageEncryptedFile.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessageEncryptedFile.kt @@ -29,37 +29,21 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent -import com.vitorpamplona.amethyst.commons.richtext.EncryptedMediaUrlImage -import com.vitorpamplona.amethyst.commons.richtext.EncryptedMediaUrlVideo -import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.components.SensitivityWarning -import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer -import com.vitorpamplona.amethyst.ui.components.ZoomableContentView import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.routeFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderEncryptedFile import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.ChatroomHeader -import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.replyModifier -import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable -import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent -import com.vitorpamplona.quartz.nip17Dm.files.encryption.AESGCM -import com.vitorpamplona.quartz.nip31Alts.alt -import kotlinx.collections.immutable.persistentListOf @Composable fun RenderChatMessageEncryptedFile( @@ -99,78 +83,3 @@ fun RenderChatMessageEncryptedFile( } } } - -@Composable -fun RenderEncryptedFile( - note: Note, - backgroundBubbleColor: MutableState, - accountViewModel: AccountViewModel, - nav: INav, -) { - val noteEvent = note.event as? ChatMessageEncryptedFileHeaderEvent ?: return - - val algo = noteEvent.algo() - val key = noteEvent.key() - val nonce = noteEvent.nonce() - val mimeType = noteEvent.mimeType() - - if (algo == AESGCM.NAME && key != null && nonce != null) { - HttpClientManager.addCipherToCache(noteEvent.content, AESGCM(key, nonce), mimeType) - - val content by remember(noteEvent) { - val isImage = mimeType?.startsWith("image/") == true || RichTextParser.isImageUrl(noteEvent.content) - - mutableStateOf( - if (isImage) { - EncryptedMediaUrlImage( - url = noteEvent.content, - description = noteEvent.alt(), - hash = noteEvent.originalHash(), - blurhash = noteEvent.blurhash(), - dim = noteEvent.dimensions(), - uri = note.toNostrUri(), - mimeType = mimeType, - encryptionAlgo = algo, - encryptionKey = key, - encryptionNonce = nonce, - ) - } else { - EncryptedMediaUrlVideo( - url = noteEvent.content, - description = noteEvent.alt(), - hash = noteEvent.originalHash(), - blurhash = noteEvent.blurhash(), - dim = noteEvent.dimensions(), - uri = note.toNostrUri(), - authorName = note.author?.toBestDisplayName(), - mimeType = mimeType, - encryptionAlgo = algo, - encryptionKey = key, - encryptionNonce = nonce, - ) - }, - ) - } - - ZoomableContentView( - content, - persistentListOf(content), - roundedCorner = true, - contentScale = ContentScale.FillWidth, - accountViewModel, - ) - } else { - TranslatableRichTextViewer( - content = stringRes(id = R.string.could_not_decrypt_the_message), - canPreview = true, - quotesLeft = 0, - modifier = Modifier, - tags = EmptyTagList, - backgroundColor = backgroundBubbleColor, - id = note.idHex, - callbackUri = note.toNostrUri(), - accountViewModel = accountViewModel, - nav = nav, - ) - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt index 488c3ee2c..1da46693b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt @@ -28,6 +28,8 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -36,9 +38,11 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -60,12 +64,12 @@ import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.note.ZapReaction import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags 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.public.JoinCommunityButton -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.public.LeaveCommunityButton -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.public.NormalTimeAgo import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.ButtonBorder +import com.vitorpamplona.amethyst.ui.theme.ButtonPadding import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier import com.vitorpamplona.amethyst.ui.theme.RowColSpacing @@ -82,6 +86,8 @@ import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefiniti import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.ModeratorTag import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import java.util.Locale @Composable @@ -359,3 +365,47 @@ fun WatchAddressableNoteFollows( onFollowChanges(state.addresses.contains(note.idHex)) } + +@Composable +fun JoinCommunityButton( + accountViewModel: AccountViewModel, + note: AddressableNote, + nav: INav, +) { + val scope = rememberCoroutineScope() + + Button( + modifier = Modifier.padding(horizontal = 3.dp), + onClick = { scope.launch(Dispatchers.IO) { accountViewModel.account.follow(note) } }, + shape = ButtonBorder, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + ), + contentPadding = ButtonPadding, + ) { + Text(text = stringRes(R.string.join), color = Color.White) + } +} + +@Composable +fun LeaveCommunityButton( + accountViewModel: AccountViewModel, + note: AddressableNote, + nav: INav, +) { + val scope = rememberCoroutineScope() + + Button( + modifier = Modifier.padding(horizontal = 3.dp), + onClick = { scope.launch(Dispatchers.IO) { accountViewModel.account.unfollow(note) } }, + shape = ButtonBorder, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + ), + contentPadding = ButtonPadding, + ) { + Text(text = stringRes(R.string.leave), color = Color.White) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt index 992e4f63c..e53c9e29c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt @@ -58,8 +58,8 @@ import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.ui.note.DisplayAuthorBanner import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.public.LiveFlag -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.public.ScheduledFlag +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveFlag +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.ScheduledFlag import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CheckIfVideoIsOnline import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CrossfadeCheckIfVideoIsOnline import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivityChatMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivityChatMessage.kt index 36d67889c..25e0c0d44 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivityChatMessage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivityChatMessage.kt @@ -33,7 +33,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.public.ChannelHeader +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ChannelHeader import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.replyModifier import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt index 4b47074ed..48f819e1c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt @@ -80,7 +80,7 @@ fun DisplayPeopleList( members.take(3) } - val name by remember { derivedStateOf { "#${noteEvent.dTag()}" } } + val name by remember { derivedStateOf { "#${noteEvent.nameOrTitle() ?: noteEvent.dTag()}" } } Text( text = name, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 1f3ab1d03..6b131680e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -79,6 +79,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata +import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser @@ -860,6 +861,14 @@ class AccountViewModel( account.decryptZapContentAuthor(note, onReady) } + fun follow(channel: Channel) { + viewModelScope.launch(Dispatchers.IO) { account.follow(channel) } + } + + fun unfollow(channel: Channel) { + viewModelScope.launch(Dispatchers.IO) { account.unfollow(channel) } + } + fun follow(user: User) { viewModelScope.launch(Dispatchers.IO) { account.follow(user) } } @@ -1054,6 +1063,8 @@ class AccountViewModel( } } + fun createRumor(template: EventTemplate) = account.signer.assembleRumor(template) + fun retrieveRelayDocument( dirtyUrl: String, onInfo: (Nip11RelayInformation) -> Unit, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/feed/ChatroomFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt similarity index 81% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/feed/ChatroomFeedView.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt index 806fc8395..d1ae97366 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/feed/ChatroomFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt @@ -18,7 +18,7 @@ * 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.privateDM.feed +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.fillMaxSize @@ -28,10 +28,8 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty @@ -40,14 +38,10 @@ import com.vitorpamplona.amethyst.ui.feeds.FeedState import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.note.dateFormatter import com.vitorpamplona.amethyst.ui.screen.FeedViewModel import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.messages.ChatroomMessageCompose -import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.FeedPadding -import com.vitorpamplona.quartz.nip14Subject.subject import com.vitorpamplona.quartz.nip37Drafts.DraftEvent @Composable @@ -64,7 +58,7 @@ fun RefreshingChatroomFeedView( ) { RefresheableBox(viewModel, enablePullRefresh) { SaveableFeedState(viewModel, scrollStateKey) { listState -> - RenderChatroomFeedView( + RenderChatFeedView( viewModel, accountViewModel, listState, @@ -79,7 +73,7 @@ fun RefreshingChatroomFeedView( } @Composable -fun RenderChatroomFeedView( +fun RenderChatFeedView( viewModel: FeedViewModel, accountViewModel: AccountViewModel, listState: LazyListState, @@ -97,7 +91,7 @@ fun RenderChatroomFeedView( is FeedState.Empty -> FeedEmpty { viewModel.invalidateData() } is FeedState.FeedError -> FeedError(state.errorMessage) { viewModel.invalidateData() } is FeedState.Loaded -> - ChatroomFeedLoaded( + ChatFeedLoaded( state, accountViewModel, listState, @@ -112,7 +106,7 @@ fun RenderChatroomFeedView( } @Composable -fun ChatroomFeedLoaded( +fun ChatFeedLoaded( loaded: FeedState.Loaded, accountViewModel: AccountViewModel, listState: LazyListState, @@ -153,31 +147,3 @@ fun ChatroomFeedLoaded( } } } - -@Composable -fun NewDateOrSubjectDivisor( - previous: Note?, - note: Note, -) { - if (previous == null) return - - val never = stringRes(R.string.never) - val today = stringRes(R.string.today) - - val prevDate = remember(previous) { dateFormatter(previous.event?.createdAt, never, today) } - val date = remember(note) { dateFormatter(note.event?.createdAt, never, today) } - - val subject = remember(note) { note.event?.subject() } - - if (prevDate != date) { - if (subject != null) { - ChatDivisor("$date - $subject") - } else { - ChatDivisor(date) - } - } else { - if (subject != null) { - ChatDivisor(subject) - } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/messages/ChatroomMessageCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt similarity index 50% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/messages/ChatroomMessageCompose.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt index 9ab47b27e..c09c891a0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/messages/ChatroomMessageCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt @@ -18,70 +18,48 @@ * 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.privateDM.messages +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed -import androidx.compose.foundation.layout.Box -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.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji -import com.vitorpamplona.amethyst.ui.components.SensitivityWarning -import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.note.DisplayDraftChat -import com.vitorpamplona.amethyst.ui.note.FollowingIcon -import com.vitorpamplona.amethyst.ui.note.InnerUserPicture import com.vitorpamplona.amethyst.ui.note.LikeReaction -import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContentOrNull import com.vitorpamplona.amethyst.ui.note.NoteQuickActionMenu -import com.vitorpamplona.amethyst.ui.note.ObserveDraftEvent import com.vitorpamplona.amethyst.ui.note.RelayBadgesHorizontal import com.vitorpamplona.amethyst.ui.note.ReplyReaction import com.vitorpamplona.amethyst.ui.note.WatchBlockAndReport import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent -import com.vitorpamplona.amethyst.ui.note.WatchUserFollows import com.vitorpamplona.amethyst.ui.note.ZapReaction -import com.vitorpamplona.amethyst.ui.note.timeAgoShort -import com.vitorpamplona.amethyst.ui.note.types.RenderEncryptedFile import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.layouts.ChatBubbleLayout +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderChangeChannelMetadataNote +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderCreateChannelNote +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderDraftEvent +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderEncryptedFile +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderRegularTextNote +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.IncognitoBadge import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer -import com.vitorpamplona.amethyst.ui.theme.Font12SP import com.vitorpamplona.amethyst.ui.theme.RowColSpacing import com.vitorpamplona.amethyst.ui.theme.Size18Modifier -import com.vitorpamplona.amethyst.ui.theme.Size20dp -import com.vitorpamplona.amethyst.ui.theme.Size5Modifier import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer -import com.vitorpamplona.amethyst.ui.theme.chatAuthorBox -import com.vitorpamplona.amethyst.ui.theme.incognitoIconModifier import com.vitorpamplona.amethyst.ui.theme.placeholderText -import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList -import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists -import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable -import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent @@ -224,11 +202,11 @@ fun NormalChatNote( } } }, - ) { backgroundBubbleColor -> + ) { bgColor -> MessageBubbleLines( note, innerQuote, - backgroundBubbleColor, + bgColor, onWantsToReply, onWantsToEditDraft, canPreview, @@ -242,7 +220,7 @@ fun NormalChatNote( private fun MessageBubbleLines( baseNote: Note, innerQuote: Boolean, - backgroundBubbleColor: MutableState, + bgColor: MutableState, onWantsToReply: (Note) -> Unit, onWantsToEditDraft: (Note) -> Unit, canPreview: Boolean, @@ -253,7 +231,7 @@ private fun MessageBubbleLines( RenderReplyRow( note = baseNote, innerQuote = innerQuote, - backgroundBubbleColor = backgroundBubbleColor, + bgColor = bgColor, accountViewModel = accountViewModel, nav = nav, onWantsToReply = onWantsToReply, @@ -267,31 +245,31 @@ private fun MessageBubbleLines( innerQuote = innerQuote, onWantsToReply = onWantsToReply, onWantsToEditDraft = onWantsToEditDraft, - backgroundBubbleColor = backgroundBubbleColor, + bgColor = bgColor, accountViewModel = accountViewModel, nav = nav, ) } @Composable -private fun RenderReplyRow( +fun RenderReplyRow( note: Note, innerQuote: Boolean, - backgroundBubbleColor: MutableState, + bgColor: MutableState, accountViewModel: AccountViewModel, nav: INav, onWantsToReply: (Note) -> Unit, onWantsToEditDraft: (Note) -> Unit, ) { if (!innerQuote && note.replyTo?.lastOrNull() != null) { - RenderReply(note, backgroundBubbleColor, accountViewModel, nav, onWantsToReply, onWantsToEditDraft) + RenderReply(note, bgColor, accountViewModel, nav, onWantsToReply, onWantsToEditDraft) } } @Composable private fun RenderReply( note: Note, - backgroundBubbleColor: MutableState, + bgColor: MutableState, accountViewModel: AccountViewModel, nav: INav, onWantsToReply: (Note) -> Unit, @@ -311,7 +289,7 @@ private fun RenderReply( baseNote = note, routeForLastRead = null, innerQuote = true, - parentBackgroundColor = backgroundBubbleColor, + parentBackgroundColor = bgColor, accountViewModel = accountViewModel, nav = nav, onWantsToReply = onWantsToReply, @@ -322,280 +300,23 @@ private fun RenderReply( } @Composable -private fun NoteRow( +fun NoteRow( note: Note, canPreview: Boolean, innerQuote: Boolean, onWantsToReply: (Note) -> Unit, onWantsToEditDraft: (Note) -> Unit, - backgroundBubbleColor: MutableState, + bgColor: MutableState, accountViewModel: AccountViewModel, nav: INav, ) { Row(verticalAlignment = Alignment.CenterVertically) { when (note.event) { - is ChannelCreateEvent -> RenderCreateChannelNote(note) - is ChannelMetadataEvent -> RenderChangeChannelMetadataNote(note) - is DraftEvent -> - RenderDraftEvent( - note, - canPreview, - innerQuote, - onWantsToReply, - onWantsToEditDraft, - backgroundBubbleColor, - accountViewModel, - nav, - ) - is ChatMessageEncryptedFileHeaderEvent -> - RenderEncryptedFile( - note, - backgroundBubbleColor, - accountViewModel, - nav, - ) - else -> - RenderRegularTextNote( - note, - canPreview, - innerQuote, - backgroundBubbleColor, - accountViewModel, - nav, - ) + is ChannelCreateEvent -> RenderCreateChannelNote(note, bgColor, accountViewModel, nav) + is ChannelMetadataEvent -> RenderChangeChannelMetadataNote(note, bgColor, accountViewModel, nav) + is DraftEvent -> RenderDraftEvent(note, canPreview, innerQuote, onWantsToReply, onWantsToEditDraft, bgColor, accountViewModel, nav) + is ChatMessageEncryptedFileHeaderEvent -> RenderEncryptedFile(note, bgColor, accountViewModel, nav) + else -> RenderRegularTextNote(note, canPreview, innerQuote, bgColor, accountViewModel, nav) } } } - -@Composable -private fun RenderDraftEvent( - note: Note, - canPreview: Boolean, - innerQuote: Boolean, - onWantsToReply: (Note) -> Unit, - onWantsToEditDraft: (Note) -> Unit, - backgroundBubbleColor: MutableState, - accountViewModel: AccountViewModel, - nav: INav, -) { - ObserveDraftEvent(note, accountViewModel) { - Column { - RenderReplyRow( - note = it, - innerQuote = innerQuote, - backgroundBubbleColor = backgroundBubbleColor, - accountViewModel = accountViewModel, - nav = nav, - onWantsToReply = onWantsToReply, - onWantsToEditDraft = onWantsToEditDraft, - ) - - NoteRow( - note = it, - canPreview = canPreview, - innerQuote = innerQuote, - onWantsToReply = onWantsToReply, - onWantsToEditDraft = onWantsToEditDraft, - backgroundBubbleColor = backgroundBubbleColor, - accountViewModel = accountViewModel, - nav = nav, - ) - } - } -} - -@Composable -fun IncognitoBadge(baseNote: Note) { - if (baseNote.event is NIP17Group) { - Icon( - painter = painterResource(id = R.drawable.incognito), - null, - modifier = incognitoIconModifier, - tint = MaterialTheme.colorScheme.placeholderText, - ) - Spacer(modifier = StdHorzSpacer) - } else if (baseNote.event is PrivateDmEvent) { - Icon( - painter = painterResource(id = R.drawable.incognito_off), - null, - modifier = incognitoIconModifier, - tint = MaterialTheme.colorScheme.placeholderText, - ) - Spacer(modifier = StdHorzSpacer) - } -} - -@Composable -fun ChatTimeAgo(baseNote: Note) { - val nowStr = stringRes(id = R.string.now) - val time = remember(baseNote) { timeAgoShort(baseNote.createdAt() ?: 0, nowStr) } - - Text( - text = time, - color = MaterialTheme.colorScheme.placeholderText, - fontSize = Font12SP, - maxLines = 1, - ) -} - -@Composable -private fun RenderRegularTextNote( - note: Note, - canPreview: Boolean, - innerQuote: Boolean, - backgroundBubbleColor: MutableState, - accountViewModel: AccountViewModel, - nav: INav, -) { - LoadDecryptedContentOrNull(note = note, accountViewModel = accountViewModel) { eventContent -> - if (eventContent != null) { - SensitivityWarning( - note = note, - accountViewModel = accountViewModel, - ) { - val tags = remember(note.event) { note.event?.tags?.toImmutableListOfLists() ?: EmptyTagList } - - TranslatableRichTextViewer( - content = eventContent, - canPreview = canPreview, - quotesLeft = if (innerQuote) 0 else 1, - modifier = Modifier, - tags = tags, - backgroundColor = backgroundBubbleColor, - id = note.idHex, - callbackUri = note.toNostrUri(), - accountViewModel = accountViewModel, - nav = nav, - ) - } - } else { - TranslatableRichTextViewer( - content = stringRes(id = R.string.could_not_decrypt_the_message), - canPreview = true, - quotesLeft = 0, - modifier = Modifier, - tags = EmptyTagList, - backgroundColor = backgroundBubbleColor, - id = note.idHex, - callbackUri = note.toNostrUri(), - accountViewModel = accountViewModel, - nav = nav, - ) - } - } -} - -@Composable -private fun RenderChangeChannelMetadataNote(note: Note) { - val noteEvent = note.event as? ChannelMetadataEvent ?: return - - 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 ?: "") + - "'" - - CreateTextWithEmoji( - text = text, - tags = note.author?.info?.tags, - ) -} - -@Composable -private fun RenderCreateChannelNote(note: Note) { - val noteEvent = note.event as? ChannelCreateEvent ?: return - val channelInfo = remember { noteEvent.channelInfo() } - - 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)} '" + - (channelInfo.picture ?: "") + - "'" - - CreateTextWithEmoji( - text = text, - tags = note.author?.info?.tags, - ) -} - -@Composable -private fun DrawAuthorInfo( - baseNote: Note, - accountViewModel: AccountViewModel, - nav: INav, -) { - baseNote.author?.let { - WatchAndDisplayUser(it, accountViewModel, nav) - } -} - -@Composable -fun UserDisplayNameLayout( - picture: @Composable () -> Unit, - name: @Composable () -> Unit, -) { - Box(chatAuthorBox, contentAlignment = Alignment.TopEnd) { - picture() - } - - Spacer(modifier = StdHorzSpacer) - - name() -} - -@Composable -private fun WatchAndDisplayUser( - author: User, - accountViewModel: AccountViewModel, - nav: INav, -) { - val userState by author.live().userMetadataInfo.observeAsState() - - UserDisplayNameLayout( - picture = { - InnerUserPicture( - userHex = author.pubkeyHex, - userPicture = userState?.picture, - userName = userState?.bestName(), - size = Size20dp, - modifier = Modifier, - accountViewModel = accountViewModel, - ) - - WatchUserFollows(author.pubkeyHex, accountViewModel) { newFollowingState -> - if (newFollowingState) { - FollowingIcon(Size5Modifier) - } - } - }, - name = { - if (userState != null) { - DisplayMessageUsername(userState?.bestName() ?: author.pubkeyDisplayHex(), userState?.tags ?: EmptyTagList) - } else { - DisplayMessageUsername(author.pubkeyDisplayHex(), EmptyTagList) - } - }, - ) -} - -@Composable -private fun DisplayMessageUsername( - userDisplayName: String, - userTags: ImmutableListOfLists, -) { - CreateTextWithEmoji( - text = userDisplayName, - tags = userTags, - maxLines = 1, - fontWeight = FontWeight.Bold, - ) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt new file mode 100644 index 000000000..7e25e709e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt @@ -0,0 +1,45 @@ +/** + * 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.feed + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.note.timeAgoShort +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.Font12SP +import com.vitorpamplona.amethyst.ui.theme.placeholderText + +@Composable +fun ChatTimeAgo(baseNote: Note) { + val nowStr = stringRes(id = R.string.now) + val time = remember(baseNote) { timeAgoShort(baseNote.createdAt() ?: 0, nowStr) } + + Text( + text = time, + color = MaterialTheme.colorScheme.placeholderText, + fontSize = Font12SP, + maxLines = 1, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/DrawAuthorInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/DrawAuthorInfo.kt new file mode 100644 index 000000000..0b998c7c1 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/DrawAuthorInfo.kt @@ -0,0 +1,95 @@ +/** + * 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.feed + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.note.FollowingIcon +import com.vitorpamplona.amethyst.ui.note.InnerUserPicture +import com.vitorpamplona.amethyst.ui.note.WatchUserFollows +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.layouts.UserDisplayNameLayout +import com.vitorpamplona.amethyst.ui.theme.Size20dp +import com.vitorpamplona.amethyst.ui.theme.Size5Modifier +import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList + +@Composable +fun DrawAuthorInfo( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: INav, +) { + baseNote.author?.let { + WatchAndDisplayUser(it, accountViewModel, nav) + } +} + +@Composable +private fun WatchAndDisplayUser( + author: User, + accountViewModel: AccountViewModel, + nav: INav, +) { + val userState by author.live().userMetadataInfo.observeAsState() + + UserDisplayNameLayout( + picture = { + InnerUserPicture( + userHex = author.pubkeyHex, + userPicture = userState?.picture, + userName = userState?.bestName(), + size = Size20dp, + modifier = Modifier, + accountViewModel = accountViewModel, + ) + + WatchUserFollows(author.pubkeyHex, accountViewModel) { newFollowingState -> + if (newFollowingState) { + FollowingIcon(Size5Modifier) + } + } + }, + name = { + if (userState != null) { + CreateTextWithEmoji( + text = userState?.bestName() ?: author.pubkeyDisplayHex(), + tags = userState?.tags ?: EmptyTagList, + maxLines = 1, + fontWeight = FontWeight.Bold, + ) + } else { + CreateTextWithEmoji( + text = author.pubkeyDisplayHex(), + tags = EmptyTagList, + maxLines = 1, + fontWeight = FontWeight.Bold, + ) + } + }, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/NewDateOrSubjectDivisor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/NewDateOrSubjectDivisor.kt new file mode 100644 index 000000000..f5d3549bc --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/NewDateOrSubjectDivisor.kt @@ -0,0 +1,58 @@ +/** + * 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.feed + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.note.dateFormatter +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.layouts.ChatDivisor +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.quartz.nip14Subject.subject + +@Composable +fun NewDateOrSubjectDivisor( + previous: Note?, + note: Note, +) { + if (previous == null) return + + val never = stringRes(R.string.never) + val today = stringRes(R.string.today) + + val prevDate = remember(previous) { dateFormatter(previous.event?.createdAt, never, today) } + val date = remember(note) { dateFormatter(note.event?.createdAt, never, today) } + + val subject = remember(note) { note.event?.subject() } + + if (prevDate != date) { + if (subject != null) { + ChatDivisor("$date - $subject") + } else { + ChatDivisor(date) + } + } else { + if (subject != null) { + ChatDivisor(subject) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/messages/ChatBubbleLayout.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/ChatBubbleLayout.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/messages/ChatBubbleLayout.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/ChatBubbleLayout.kt index 56984c4dd..d6f370720 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/messages/ChatBubbleLayout.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/ChatBubbleLayout.kt @@ -18,7 +18,7 @@ * 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.privateDM.messages +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.layouts import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background @@ -82,7 +82,7 @@ fun ChatBubbleLayout( val defaultBackground = MaterialTheme.colorScheme.background val draftColor = MaterialTheme.colorScheme.chatDraftBackground - val backgroundBubbleColor = + val bgColor = remember { if (isLoggedInUser) { if (isDraft) { @@ -135,7 +135,7 @@ fun ChatBubbleLayout( modifier = if (innerQuote) Modifier else ChatBubbleMaxSizeModifier, ) { Surface( - color = backgroundBubbleColor.value, + color = bgColor.value, shape = if (isLoggedInUser) ChatBubbleShapeMe else ChatBubbleShapeThem, modifier = clickableModifier, ) { @@ -150,7 +150,7 @@ fun ChatBubbleLayout( } } - inner(backgroundBubbleColor) + inner(bgColor) if (showDetails.value) { Row( @@ -175,7 +175,7 @@ fun ChatBubbleLayout( @Preview @Composable private fun BubblePreview() { - val backgroundBubbleColor = + val bgColor = remember { mutableStateOf(Color.Transparent) } @@ -188,7 +188,7 @@ private fun BubblePreview() { isComplete = true, hasDetailsToShow = true, drawAuthorInfo = true, - parentBackgroundColor = backgroundBubbleColor, + parentBackgroundColor = bgColor, onClick = { false }, onAuthorClick = {}, actionMenu = { onDismiss -> @@ -212,7 +212,7 @@ private fun BubblePreview() { ) }, detailRow = { Text("Relays and Actions") }, - ) { backgroundBubbleColor -> + ) { bgColor -> Text("This is my note") } @@ -223,7 +223,7 @@ private fun BubblePreview() { isComplete = true, hasDetailsToShow = true, drawAuthorInfo = true, - parentBackgroundColor = backgroundBubbleColor, + parentBackgroundColor = bgColor, onClick = { false }, onAuthorClick = {}, actionMenu = { onDismiss -> @@ -246,7 +246,7 @@ private fun BubblePreview() { ) }, detailRow = { Text("Relays and Actions") }, - ) { backgroundBubbleColor -> + ) { bgColor -> Text("This is a very long long loong note") } @@ -257,7 +257,7 @@ private fun BubblePreview() { isComplete = true, hasDetailsToShow = true, drawAuthorInfo = true, - parentBackgroundColor = backgroundBubbleColor, + parentBackgroundColor = bgColor, onClick = { false }, onAuthorClick = {}, actionMenu = { onDismiss -> @@ -280,7 +280,7 @@ private fun BubblePreview() { ) }, detailRow = { Text("Relays and Actions") }, - ) { backgroundBubbleColor -> + ) { bgColor -> Text("This is a draft note") } @@ -291,7 +291,7 @@ private fun BubblePreview() { isComplete = false, hasDetailsToShow = false, drawAuthorInfo = false, - parentBackgroundColor = backgroundBubbleColor, + parentBackgroundColor = bgColor, onClick = { false }, onAuthorClick = {}, actionMenu = { onDismiss -> @@ -314,7 +314,7 @@ private fun BubblePreview() { ) }, detailRow = { Text("Relays and Actions") }, - ) { backgroundBubbleColor -> + ) { bgColor -> Text("Short note") } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/feed/ChatDivisor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/ChatDivisor.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/feed/ChatDivisor.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/ChatDivisor.kt index fd4d6578b..70458cbdb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/feed/ChatDivisor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/ChatDivisor.kt @@ -18,7 +18,7 @@ * 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.privateDM.feed +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.layouts import androidx.compose.foundation.layout.Row import androidx.compose.material3.HorizontalDivider diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/UserDisplayNameLayout.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/UserDisplayNameLayout.kt new file mode 100644 index 000000000..2e506a9ff --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/UserDisplayNameLayout.kt @@ -0,0 +1,42 @@ +/** + * 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.feed.layouts + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer +import com.vitorpamplona.amethyst.ui.theme.chatAuthorBox + +@Composable +fun UserDisplayNameLayout( + picture: @Composable () -> Unit, + name: @Composable () -> Unit, +) { + Box(chatAuthorBox, contentAlignment = Alignment.TopEnd) { + picture() + } + + Spacer(modifier = StdHorzSpacer) + + name() +} 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 new file mode 100644 index 000000000..29aa52549 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChangeChannelMetadataNote.kt @@ -0,0 +1,67 @@ +/** + * 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.feed.types + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.ui.Modifier +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.nip28PublicChat.admin.ChannelMetadataEvent + +@Composable +fun RenderChangeChannelMetadataNote( + note: Note, + bgColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, +) { + val noteEvent = note.event as? ChannelMetadataEvent ?: return + + 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, + ) +} 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 new file mode 100644 index 000000000..ac01d26ac --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt @@ -0,0 +1,68 @@ +/** + * 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.feed.types + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +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.nip28PublicChat.admin.ChannelCreateEvent + +@Composable +fun RenderCreateChannelNote( + note: Note, + bgColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, +) { + val noteEvent = note.event as? ChannelCreateEvent ?: return + val channelInfo = remember { noteEvent.channelInfo() } + + 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, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderDraftEvent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderDraftEvent.kt new file mode 100644 index 000000000..1e4dc8b7b --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderDraftEvent.kt @@ -0,0 +1,70 @@ +/** + * 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.feed.types + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.ui.graphics.Color +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.note.ObserveDraftEvent +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.NoteRow +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RenderReplyRow +import com.vitorpamplona.amethyst.ui.theme.RowColSpacing5dp + +@Composable +fun RenderDraftEvent( + note: Note, + canPreview: Boolean, + innerQuote: Boolean, + onWantsToReply: (Note) -> Unit, + onWantsToEditDraft: (Note) -> Unit, + backgroundBubbleColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, +) { + ObserveDraftEvent(note, accountViewModel) { + Column(verticalArrangement = RowColSpacing5dp) { + RenderReplyRow( + note = it, + innerQuote = innerQuote, + bgColor = backgroundBubbleColor, + accountViewModel = accountViewModel, + nav = nav, + onWantsToReply = onWantsToReply, + onWantsToEditDraft = onWantsToEditDraft, + ) + + NoteRow( + note = it, + canPreview = canPreview, + innerQuote = innerQuote, + onWantsToReply = onWantsToReply, + onWantsToEditDraft = onWantsToEditDraft, + bgColor = backgroundBubbleColor, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderEncryptedFile.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderEncryptedFile.kt new file mode 100644 index 000000000..32fdcb8c2 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderEncryptedFile.kt @@ -0,0 +1,122 @@ +/** + * 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.feed.types + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent +import com.vitorpamplona.amethyst.commons.richtext.EncryptedMediaUrlImage +import com.vitorpamplona.amethyst.commons.richtext.EncryptedMediaUrlVideo +import com.vitorpamplona.amethyst.commons.richtext.RichTextParser +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager +import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer +import com.vitorpamplona.amethyst.ui.components.ZoomableContentView +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.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent +import com.vitorpamplona.quartz.nip17Dm.files.encryption.AESGCM +import com.vitorpamplona.quartz.nip31Alts.alt +import kotlinx.collections.immutable.persistentListOf + +@Composable +fun RenderEncryptedFile( + note: Note, + bgColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, +) { + val noteEvent = note.event as? ChatMessageEncryptedFileHeaderEvent ?: return + + val algo = noteEvent.algo() + val key = noteEvent.key() + val nonce = noteEvent.nonce() + val mimeType = noteEvent.mimeType() + + if (algo == AESGCM.NAME && key != null && nonce != null) { + HttpClientManager.addCipherToCache(noteEvent.content, AESGCM(key, nonce), mimeType) + + val content by remember(noteEvent) { + val isImage = mimeType?.startsWith("image/") == true || RichTextParser.isImageUrl(noteEvent.content) + + mutableStateOf( + if (isImage) { + EncryptedMediaUrlImage( + url = noteEvent.content, + description = noteEvent.alt(), + hash = noteEvent.originalHash(), + blurhash = noteEvent.blurhash(), + dim = noteEvent.dimensions(), + uri = note.toNostrUri(), + mimeType = mimeType, + encryptionAlgo = algo, + encryptionKey = key, + encryptionNonce = nonce, + ) + } else { + EncryptedMediaUrlVideo( + url = noteEvent.content, + description = noteEvent.alt(), + hash = noteEvent.originalHash(), + blurhash = noteEvent.blurhash(), + dim = noteEvent.dimensions(), + uri = note.toNostrUri(), + authorName = note.author?.toBestDisplayName(), + mimeType = mimeType, + encryptionAlgo = algo, + encryptionKey = key, + encryptionNonce = nonce, + ) + }, + ) + } + + ZoomableContentView( + content, + persistentListOf(content), + roundedCorner = true, + contentScale = ContentScale.FillWidth, + accountViewModel, + ) + } else { + TranslatableRichTextViewer( + content = stringRes(id = R.string.could_not_decrypt_the_message), + canPreview = true, + quotesLeft = 0, + modifier = Modifier, + tags = EmptyTagList, + backgroundColor = bgColor, + id = note.idHex, + callbackUri = note.toNostrUri(), + accountViewModel = accountViewModel, + nav = nav, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt new file mode 100644 index 000000000..e39fd1e93 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt @@ -0,0 +1,84 @@ +/** + * 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.feed.types + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.components.SensitivityWarning +import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContentOrNull +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 + +@Composable +fun RenderRegularTextNote( + note: Note, + canPreview: Boolean, + innerQuote: Boolean, + bgColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, +) { + LoadDecryptedContentOrNull(note = note, accountViewModel = accountViewModel) { eventContent -> + if (eventContent != null) { + SensitivityWarning( + note = note, + accountViewModel = accountViewModel, + ) { + val tags = remember(note.event) { note.event?.tags?.toImmutableListOfLists() ?: EmptyTagList } + + TranslatableRichTextViewer( + content = eventContent, + canPreview = canPreview, + quotesLeft = if (innerQuote) 0 else 1, + modifier = Modifier, + tags = tags, + backgroundColor = bgColor, + id = note.idHex, + callbackUri = note.toNostrUri(), + accountViewModel = accountViewModel, + nav = nav, + ) + } + } else { + TranslatableRichTextViewer( + content = stringRes(id = R.string.could_not_decrypt_the_message), + canPreview = true, + quotesLeft = 0, + modifier = Modifier, + tags = EmptyTagList, + backgroundColor = bgColor, + id = note.idHex, + callbackUri = note.toNostrUri(), + accountViewModel = accountViewModel, + nav = nav, + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt index 3a38db622..cf0f03f5b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -42,9 +41,10 @@ import com.vitorpamplona.amethyst.ui.note.elements.ObserveRelayListForDMs import com.vitorpamplona.amethyst.ui.note.elements.ObserveRelayListForDMsAndDisplayIfNotFound import com.vitorpamplona.amethyst.ui.screen.NostrChatroomFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.feed.RefreshingChatroomFeedView +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.ChatNewMessageViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.PrivateMessageEditFieldRow +import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import kotlinx.coroutines.launch @@ -176,7 +176,7 @@ fun ChatroomViewUI( ) } - Spacer(modifier = Modifier.height(10.dp)) + Spacer(modifier = DoubleVertSpacer) val scope = rememberCoroutineScope() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/IncognitoBadge.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/IncognitoBadge.kt new file mode 100644 index 000000000..f98392295 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/IncognitoBadge.kt @@ -0,0 +1,55 @@ +/** + * 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.privateDM + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.painterResource +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer +import com.vitorpamplona.amethyst.ui.theme.incognitoIconModifier +import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group + +@Composable +fun IncognitoBadge(baseNote: Note) { + if (baseNote.event is NIP17Group) { + Icon( + painter = painterResource(id = R.drawable.incognito), + null, + modifier = incognitoIconModifier, + tint = MaterialTheme.colorScheme.placeholderText, + ) + Spacer(modifier = StdHorzSpacer) + } else if (baseNote.event is PrivateDmEvent) { + Icon( + painter = painterResource(id = R.drawable.incognito_off), + null, + modifier = incognitoIconModifier, + tint = MaterialTheme.colorScheme.placeholderText, + ) + Spacer(modifier = StdHorzSpacer) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/ChatroomHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/ChatroomHeader.kt index 10ea0292f..56698cb97 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/ChatroomHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/ChatroomHeader.kt @@ -36,7 +36,7 @@ import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.list.LoadUser +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser import com.vitorpamplona.amethyst.ui.theme.Size34dp import com.vitorpamplona.amethyst.ui.theme.StdPadding import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RenderRoomTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RenderRoomTopBar.kt index 27b2a9b59..2df854f7b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RenderRoomTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RenderRoomTopBar.kt @@ -55,7 +55,7 @@ import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures import com.vitorpamplona.amethyst.ui.note.UserCompose import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.list.LoadUser +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RoomNameOnlyDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RoomNameOnlyDisplay.kt index 668f6c48a..7b971502d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RoomNameOnlyDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RoomNameOnlyDisplay.kt @@ -36,7 +36,7 @@ import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.list.LoadUser +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import kotlin.math.min diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index 3d00cec2a..4b65993ed 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -57,6 +57,7 @@ import com.vitorpamplona.quartz.nip14Subject.subject import com.vitorpamplona.quartz.nip17Dm.base.BaseDMGroupEvent import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group +import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent import com.vitorpamplona.quartz.nip18Reposts.quotes.quotes import com.vitorpamplona.quartz.nip19Bech32.toNpub @@ -179,6 +180,12 @@ open class ChatNewMessageViewModel : ViewModel() { open fun reply(replyNote: Note) { replyTo.value = replyNote + saveDraft() + } + + fun clearReply() { + replyTo.value = null + saveDraft() } open fun quote(quote: Note) { @@ -224,8 +231,6 @@ open class ChatNewMessageViewModel : ViewModel() { } private fun loadFromDraft(draft: Note) { - Log.d("draft", draft.event!!.toJson()) - val draftEvent = draft.event ?: return val accountViewModel = accountViewModel ?: return @@ -264,9 +269,29 @@ open class ChatNewMessageViewModel : ViewModel() { TextFieldValue( draftEvent.groupMembers().mapNotNull { runCatching { Hex.decode(it).toNpub() }.getOrNull() }.joinToString(", ") { "@$it" }, ) + + val replyId = + when (draftEvent) { + is ChatMessageEvent -> draftEvent.replyTo().lastOrNull() + is ChatMessageEncryptedFileHeaderEvent -> draftEvent.replyTo().lastOrNull() + else -> null + } + + if (replyId != null) { + accountViewModel.checkGetOrCreateNote(replyId) { + replyTo.value = it + } + } } else if (draftEvent is PrivateDmEvent) { val recepientNpub = draftEvent.verifiedRecipientPubKey()?.let { Hex.decode(it).toNpub() } toUsers = TextFieldValue("@$recepientNpub") + + val replyId = draftEvent.replyTo() + if (replyId != null) { + accountViewModel.checkGetOrCreateNote(replyId) { + replyTo.value = it + } + } } message = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt index 6c4e3dba4..a27d6d035 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt @@ -42,12 +42,15 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField +import com.vitorpamplona.amethyst.ui.navigation.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.note.IncognitoIconOff import com.vitorpamplona.amethyst.ui.note.IncognitoIconOn @@ -58,11 +61,14 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.ChatFileUploadDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ThinSendButton +import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.EditFieldBorder +import com.vitorpamplona.amethyst.ui.theme.EditFieldLeadingIconModifier import com.vitorpamplona.amethyst.ui.theme.EditFieldModifier import com.vitorpamplona.amethyst.ui.theme.EditFieldTrailingIconModifier import com.vitorpamplona.amethyst.ui.theme.Size30Modifier +import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.ui.theme.placeholderText import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collectLatest @@ -70,6 +76,23 @@ import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch +@Preview +@Composable +fun PrivateMessageEditFieldRow() { + val channelScreenModel: ChatNewMessageViewModel = viewModel() + val accountViewModel = mockAccountViewModel() + channelScreenModel.init(accountViewModel) + + ThemeComparisonRow { + PrivateMessageEditFieldRow( + channelScreenModel = channelScreenModel, + accountViewModel = accountViewModel, + onSendNewMessage = {}, + nav = EmptyNav, + ) + } +} + @Composable fun PrivateMessageEditFieldRow( channelScreenModel: ChatNewMessageViewModel, @@ -77,7 +100,11 @@ fun PrivateMessageEditFieldRow( onSendNewMessage: () -> Unit, nav: INav, ) { - channelScreenModel.replyTo.value?.let { DisplayReplyingToNote(it, accountViewModel, nav) { channelScreenModel.replyTo.value = null } } + channelScreenModel.replyTo.value?.let { + DisplayReplyingToNote(it, accountViewModel, nav) { + channelScreenModel.clearReply() + } + } LaunchedEffect(key1 = channelScreenModel.draftTag) { launch(Dispatchers.IO) { @@ -148,8 +175,7 @@ fun PrivateMessageEditFieldRow( }, trailingIcon = { ThinSendButton( - isActive = - channelScreenModel.message.text.isNotBlank() && !channelScreenModel.isUploadingImage, + isActive = channelScreenModel.message.text.isNotBlank() && !channelScreenModel.isUploadingImage, modifier = EditFieldTrailingIconModifier, ) { channelScreenModel.sendPost(onSendNewMessage) @@ -163,10 +189,7 @@ fun PrivateMessageEditFieldRow( SelectFromGallery( isUploading = channelScreenModel.isUploadingImage, tint = MaterialTheme.colorScheme.placeholderText, - modifier = - Modifier - .size(30.dp) - .padding(start = 2.dp), + modifier = EditFieldLeadingIconModifier, onImageChosen = channelScreenModel::pickedMedia, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadDialog.kt index 127aed8c7..8e72986a4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadDialog.kt @@ -208,8 +208,8 @@ private fun ImageVideoPostChat( title = R.string.add_sensitive_content_label, description = R.string.add_sensitive_content_description, modifier = Modifier.fillMaxWidth().padding(top = 8.dp), - checked = fileUploadState.sensitiveContent, - onCheckedChange = { fileUploadState.sensitiveContent = it }, + checked = fileUploadState.contentWarning, + onCheckedChange = fileUploadState::updateContentWarning, ) SettingsRow(R.string.file_server, R.string.file_server_description) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadState.kt index 9bc20bb7d..3919a2cc5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadState.kt @@ -40,7 +40,11 @@ class ChatFileUploadState( var selectedServer by mutableStateOf(defaultServer) var caption by mutableStateOf("") - var sensitiveContent by mutableStateOf(false) + + var contentWarning by mutableStateOf(false) + private set + + var contentWarningReason by mutableStateOf(null) // Images and Videos var multiOrchestrator by mutableStateOf(null) @@ -72,4 +76,13 @@ class ChatFileUploadState( fun canPost(): Boolean = !isUploadingImage && multiOrchestrator != null fun hasPickedMedia() = multiOrchestrator != null + + fun updateContentWarning(value: Boolean) { + contentWarning = value + if (value) { + contentWarningReason = "" + } else { + contentWarningReason = null + } + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt index f70c67e5a..ea44e754a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt @@ -59,7 +59,7 @@ class ChatFileUploader( orchestrator.uploadEncrypted( scope, viewState.caption, - if (viewState.sensitiveContent) "" else null, + viewState.contentWarningReason, MediaCompressor.intToCompressorQuality(viewState.mediaQualitySlider), cipher, viewState.selectedServer, @@ -88,9 +88,7 @@ class ChatFileUploader( alt(viewState.caption) } - if (viewState.sensitiveContent) { - contentWarning("") - } + viewState.contentWarningReason?.let { contentWarning(it) } } account.sendNIP17EncryptedFile(template) @@ -125,7 +123,7 @@ class ChatFileUploader( orchestrator.upload( scope, viewState.caption, - if (viewState.sensitiveContent) "" else null, + viewState.contentWarningReason, MediaCompressor.intToCompressorQuality(viewState.mediaQualitySlider), viewState.selectedServer, account, @@ -136,7 +134,7 @@ class ChatFileUploader( results.successful.forEach { if (it.result is UploadOrchestrator.OrchestratorResult.ServerResult) { val iMetaAttachments = IMetaAttachments() - iMetaAttachments.add(it.result, viewState.caption, if (viewState.sensitiveContent) "" else null) + iMetaAttachments.add(it.result, viewState.caption, viewState.contentWarningReason) account.sendPrivateMessage( message = it.result.url, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/public/ChannelScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/public/ChannelScreen.kt deleted file mode 100644 index b0a4faf2e..000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/public/ChannelScreen.kt +++ /dev/null @@ -1,1275 +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.public - -import androidx.compose.foundation.background -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.Spacer -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.EditNote -import androidx.compose.material.icons.filled.Share -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.Icon -import androidx.compose.material3.LocalTextStyle -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TextFieldDefaults -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -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.layout.ContentScale -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.KeyboardCapitalization -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.style.TextDirection -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver -import androidx.lifecycle.distinctUntilChanged -import androidx.lifecycle.map -import androidx.lifecycle.viewmodel.compose.viewModel -import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Channel -import com.vitorpamplona.amethyst.model.FeatureSetType -import com.vitorpamplona.amethyst.model.LiveActivitiesChannel -import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.PublicChatChannel -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.NostrChannelDataSource -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.ui.actions.NewChannelView -import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger -import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel -import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery -import com.vitorpamplona.amethyst.ui.components.LoadNote -import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage -import com.vitorpamplona.amethyst.ui.components.SensitivityWarning -import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField -import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer -import com.vitorpamplona.amethyst.ui.components.ZoomableContentView -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.TopBarExtensibleWithBackButton -import com.vitorpamplona.amethyst.ui.navigation.routeFor -import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture -import com.vitorpamplona.amethyst.ui.note.LikeReaction -import com.vitorpamplona.amethyst.ui.note.LoadChannel -import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture -import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay -import com.vitorpamplona.amethyst.ui.note.ShowEmojiSuggestionList -import com.vitorpamplona.amethyst.ui.note.ShowUserSuggestionList -import com.vitorpamplona.amethyst.ui.note.UserPicture -import com.vitorpamplona.amethyst.ui.note.UsernameDisplay -import com.vitorpamplona.amethyst.ui.note.ZapReaction -import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags -import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton -import com.vitorpamplona.amethyst.ui.note.timeAgoShort -import com.vitorpamplona.amethyst.ui.screen.NostrChannelFeedViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.DisappearingScaffold -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.feed.RefreshingChatroomFeedView -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ThinSendButton -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CrossfadeCheckIfVideoIsOnline -import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists -import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.ButtonBorder -import com.vitorpamplona.amethyst.ui.theme.ButtonPadding -import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer -import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer -import com.vitorpamplona.amethyst.ui.theme.EditFieldBorder -import com.vitorpamplona.amethyst.ui.theme.EditFieldLeadingIconModifier -import com.vitorpamplona.amethyst.ui.theme.EditFieldModifier -import com.vitorpamplona.amethyst.ui.theme.EditFieldTrailingIconModifier -import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier -import com.vitorpamplona.amethyst.ui.theme.RowColSpacing -import com.vitorpamplona.amethyst.ui.theme.Size10dp -import com.vitorpamplona.amethyst.ui.theme.Size25dp -import com.vitorpamplona.amethyst.ui.theme.Size34dp -import com.vitorpamplona.amethyst.ui.theme.Size35dp -import com.vitorpamplona.amethyst.ui.theme.SmallBorder -import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer -import com.vitorpamplona.amethyst.ui.theme.StdPadding -import com.vitorpamplona.amethyst.ui.theme.ZeroPadding -import com.vitorpamplona.amethyst.ui.theme.innerPostModifier -import com.vitorpamplona.amethyst.ui.theme.liveStreamTag -import com.vitorpamplona.amethyst.ui.theme.placeholderText -import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle -import com.vitorpamplona.quartz.nip01Core.tags.events.ETag -import com.vitorpamplona.quartz.nip01Core.tags.events.isTaggedEvent -import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags -import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags -import com.vitorpamplona.quartz.nip01Core.tags.references.references -import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList -import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists -import com.vitorpamplona.quartz.nip10Notes.content.findHashtags -import com.vitorpamplona.quartz.nip10Notes.content.findNostrUris -import com.vitorpamplona.quartz.nip10Notes.content.findURLs -import com.vitorpamplona.quartz.nip18Reposts.quotes.quotes -import com.vitorpamplona.quartz.nip28PublicChat.base.notify -import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent -import com.vitorpamplona.quartz.nip30CustomEmoji.emojis -import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent -import com.vitorpamplona.quartz.nip53LiveActivities.chat.notify -import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag -import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.StatusTag -import com.vitorpamplona.quartz.nip92IMeta.imetas -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.launch -import java.text.DateFormat -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale - -@Composable -fun ChannelScreen( - channelId: String?, - accountViewModel: AccountViewModel, - nav: INav, -) { - if (channelId == null) return - - DisappearingScaffold( - isInvertedLayout = true, - topBar = { - ChannelTopBar(channelId, accountViewModel, nav) - }, - accountViewModel = accountViewModel, - ) { - Column(Modifier.padding(it)) { - Channel(channelId, accountViewModel, nav) - } - } -} - -@Composable -fun Channel( - channelId: String?, - accountViewModel: AccountViewModel, - nav: INav, -) { - if (channelId == null) return - - LoadChannel(channelId, accountViewModel) { - PrepareChannelViewModels( - baseChannel = it, - accountViewModel = accountViewModel, - nav = nav, - ) - } -} - -@Composable -fun PrepareChannelViewModels( - baseChannel: Channel, - accountViewModel: AccountViewModel, - nav: INav, -) { - val feedViewModel: NostrChannelFeedViewModel = - viewModel( - key = baseChannel.idHex + "ChannelFeedViewModel", - factory = - NostrChannelFeedViewModel.Factory( - baseChannel, - accountViewModel.account, - ), - ) - - val channelScreenModel: NewPostViewModel = viewModel() - channelScreenModel.accountViewModel = accountViewModel - channelScreenModel.account = accountViewModel.account - - ChannelScreen( - channel = baseChannel, - feedViewModel = feedViewModel, - newPostModel = channelScreenModel, - accountViewModel = accountViewModel, - nav = nav, - ) -} - -@Composable -fun ChannelScreen( - channel: Channel, - feedViewModel: NostrChannelFeedViewModel, - newPostModel: NewPostViewModel, - accountViewModel: AccountViewModel, - nav: INav, -) { - NostrChannelDataSource.loadMessagesBetween(accountViewModel.account, channel) - - val lifeCycleOwner = LocalLifecycleOwner.current - - DisposableEffect(accountViewModel) { - NostrChannelDataSource.loadMessagesBetween(accountViewModel.account, channel) - NostrChannelDataSource.start() - feedViewModel.invalidateData(true) - - onDispose { - NostrChannelDataSource.clear() - NostrChannelDataSource.stop() - } - } - - DisposableEffect(lifeCycleOwner) { - val observer = - LifecycleEventObserver { _, event -> - if (event == Lifecycle.Event.ON_RESUME) { - println("Channel Start") - NostrChannelDataSource.start() - feedViewModel.invalidateData(true) - } - if (event == Lifecycle.Event.ON_PAUSE) { - println("Channel Stop") - - NostrChannelDataSource.clear() - NostrChannelDataSource.stop() - } - } - - lifeCycleOwner.lifecycle.addObserver(observer) - onDispose { lifeCycleOwner.lifecycle.removeObserver(observer) } - } - - Column(Modifier.fillMaxHeight()) { - val replyTo = remember { mutableStateOf(null) } - - Column( - modifier = - remember { - Modifier - .fillMaxHeight() - .padding(vertical = 0.dp) - .weight(1f, true) - }, - ) { - if (channel is LiveActivitiesChannel) { - ShowVideoStreaming(channel, accountViewModel) - } - RefreshingChatroomFeedView( - viewModel = feedViewModel, - accountViewModel = accountViewModel, - nav = nav, - routeForLastRead = "Channel/${channel.idHex}", - avoidDraft = newPostModel.draftTag, - onWantsToReply = { replyTo.value = it }, - onWantsToEditDraft = { - newPostModel.load(accountViewModel, null, null, null, null, it) - }, - ) - } - - Spacer(modifier = DoubleVertSpacer) - - replyTo.value?.let { DisplayReplyingToNote(it, accountViewModel, nav) { replyTo.value = null } } - - val scope = rememberCoroutineScope() - - LaunchedEffect(Unit) { - launch(Dispatchers.IO) { - newPostModel.draftTextChanges - .receiveAsFlow() - .debounce(1000) - .collectLatest { - innerSendPost(replyTo, channel, newPostModel, accountViewModel, newPostModel.draftTag) - } - } - } - - // LAST ROW - EditFieldRow(newPostModel, accountViewModel = accountViewModel) { - scope.launch(Dispatchers.IO) { - innerSendPost(replyTo, channel, newPostModel, accountViewModel, null) - newPostModel.message = TextFieldValue("") - replyTo.value = null - accountViewModel.deleteDraft(newPostModel.draftTag) - feedViewModel.sendToTop() - } - } - } -} - -@Composable -private fun ChannelTopBar( - id: String, - accountViewModel: AccountViewModel, - nav: INav, -) { - LoadChannel(baseChannelHex = id, accountViewModel) { baseChannel -> - TopBarExtensibleWithBackButton( - title = { - ShortChannelHeader( - baseChannel = baseChannel, - accountViewModel = accountViewModel, - nav = nav, - showFlag = true, - ) - }, - extendableRow = { - LongChannelHeader(baseChannel = baseChannel, accountViewModel = accountViewModel, nav = nav) - }, - popBack = nav::popBack, - ) - } -} - -private suspend fun innerSendPost( - replyTo: MutableState, - channel: Channel, - newPostModel: NewPostViewModel, - accountViewModel: AccountViewModel, - draftTag: String?, -) { - val tagger = - NewMessageTagger( - message = newPostModel.message.text, - pTags = listOfNotNull(replyTo.value?.author), - eTags = listOfNotNull(replyTo.value), - channelHex = channel.idHex, - dao = accountViewModel, - ) - tagger.run() - - val urls = findURLs(tagger.message) - val usedAttachments = newPostModel.iMetaAttachments.filter { it.url in urls.toSet() } - val emojis = newPostModel.findEmoji(newPostModel.message.text, accountViewModel.account.myEmojis.value) - - val channelRelays = channel.relays() - - if (channel is PublicChatChannel) { - val replyingToEvent = replyTo.value?.toEventHint() - val channelEvent = channel.event - - val template = - if (replyingToEvent != null) { - ChannelMessageEvent.reply(tagger.message, replyingToEvent) { - notify(replyingToEvent.toPTag()) - - hashtags(findHashtags(tagger.message)) - references(findURLs(tagger.message)) - quotes(findNostrUris(tagger.message)) - - emojis(emojis) - imetas(usedAttachments) - } - } else if (channelEvent != null) { - val hint = EventHintBundle(channelEvent, channelRelays.firstOrNull()) - ChannelMessageEvent.message(tagger.message, hint) { - hashtags(findHashtags(tagger.message)) - references(findURLs(tagger.message)) - quotes(findNostrUris(tagger.message)) - - emojis(emojis) - imetas(usedAttachments) - } - } else { - ChannelMessageEvent.message(tagger.message, ETag(channel.idHex, channelRelays.firstOrNull())) { - hashtags(findHashtags(tagger.message)) - references(findURLs(tagger.message)) - quotes(findNostrUris(tagger.message)) - - emojis(emojis) - imetas(usedAttachments) - } - } - - val broadcast = tagger.directMentionsNotes + (tagger.eTags ?: emptyList()) - - accountViewModel.account.signAndSendWithList(draftTag, template, channelRelays, broadcast) - } else if (channel is LiveActivitiesChannel) { - val replyingToEvent = replyTo.value?.toEventHint() - val activity = channel.info - - val template = - if (replyingToEvent != null) { - LiveActivitiesChatMessageEvent.reply(tagger.message, replyingToEvent) { - notify(replyingToEvent.toPTag()) - - hashtags(findHashtags(tagger.message)) - references(findURLs(tagger.message)) - quotes(findNostrUris(tagger.message)) - - emojis(emojis) - imetas(usedAttachments) - } - } else if (activity != null) { - val hint = EventHintBundle(activity, channelRelays.firstOrNull() ?: replyingToEvent?.relay) - - LiveActivitiesChatMessageEvent.message(tagger.message, hint) { - hashtags(findHashtags(tagger.message)) - references(findURLs(tagger.message)) - quotes(findNostrUris(tagger.message)) - - emojis(emojis) - imetas(usedAttachments) - } - } else { - LiveActivitiesChatMessageEvent.message(tagger.message, channel.toATag()) { - hashtags(findHashtags(tagger.message)) - references(findURLs(tagger.message)) - quotes(findNostrUris(tagger.message)) - - emojis(emojis) - imetas(usedAttachments) - } - } - - val broadcast = tagger.directMentionsNotes + (tagger.eTags ?: emptyList()) - - accountViewModel.account.signAndSendWithList(draftTag, template, channelRelays, broadcast) - } -} - -@Composable -fun EditFieldRow( - channelScreenModel: NewPostViewModel, - accountViewModel: AccountViewModel, - onSendNewMessage: () -> Unit, -) { - Column( - modifier = EditFieldModifier, - ) { - val context = LocalContext.current - - ShowUserSuggestionList( - channelScreenModel.userSuggestions, - channelScreenModel::autocompleteWithUser, - accountViewModel, - ) - - ShowEmojiSuggestionList( - channelScreenModel.emojiSuggestions, - channelScreenModel::autocompleteWithEmoji, - channelScreenModel::autocompleteWithEmojiUrl, - accountViewModel, - ) - - ThinPaddingTextField( - value = channelScreenModel.message, - onValueChange = { channelScreenModel.updateMessage(it) }, - keyboardOptions = - KeyboardOptions.Default.copy( - capitalization = KeyboardCapitalization.Sentences, - ), - shape = EditFieldBorder, - modifier = Modifier.fillMaxWidth(), - placeholder = { - Text( - text = stringRes(R.string.reply_here), - color = MaterialTheme.colorScheme.placeholderText, - ) - }, - textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content), - trailingIcon = { - ThinSendButton( - isActive = - channelScreenModel.message.text.isNotBlank() && !channelScreenModel.isUploadingImage, - modifier = EditFieldTrailingIconModifier, - ) { - onSendNewMessage() - } - }, - leadingIcon = { - SelectFromGallery( - isUploading = channelScreenModel.isUploadingImage, - tint = MaterialTheme.colorScheme.placeholderText, - modifier = EditFieldLeadingIconModifier, - ) { - channelScreenModel.selectImage(it) - channelScreenModel.upload( - alt = null, - contentWarningReason = null, - // Use MEDIUM quality - mediaQuality = MediaCompressor.compressorQualityToInt(CompressorQuality.MEDIUM), - server = accountViewModel.account.settings.defaultFileServer, - onError = accountViewModel::toast, - context = context, - ) - } - }, - colors = - TextFieldDefaults.colors( - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - ), - visualTransformation = UrlUserTagTransformation(MaterialTheme.colorScheme.primary), - ) - } -} - -@Composable -fun RenderChannelHeader( - channelNote: Note, - showVideo: Boolean, - sendToChannel: Boolean, - modifier: Modifier = StdPadding, - accountViewModel: AccountViewModel, - nav: INav, -) { - ChannelHeader( - channelNote = channelNote, - showVideo = showVideo, - sendToChannel = sendToChannel, - modifier = MaterialTheme.colorScheme.innerPostModifier.padding(Size10dp), - accountViewModel = accountViewModel, - nav = nav, - ) -} - -@Composable -fun ChannelHeader( - channelNote: Note, - showVideo: Boolean, - sendToChannel: Boolean, - modifier: Modifier = StdPadding, - accountViewModel: AccountViewModel, - nav: INav, -) { - val channelHex by remember { derivedStateOf { channelNote.channelHex() } } - channelHex?.let { - ChannelHeader( - channelHex = it, - showVideo = showVideo, - sendToChannel = sendToChannel, - modifier = modifier, - accountViewModel = accountViewModel, - nav = nav, - ) - } -} - -@Composable -fun ChannelHeader( - channelHex: String, - showVideo: Boolean, - showFlag: Boolean = true, - sendToChannel: Boolean = false, - modifier: Modifier = StdPadding, - accountViewModel: AccountViewModel, - nav: INav, -) { - LoadChannel(channelHex, accountViewModel) { - ChannelHeader( - it, - showVideo, - showFlag, - sendToChannel, - modifier, - accountViewModel, - nav, - ) - } -} - -@Composable -fun ChannelHeader( - baseChannel: Channel, - showVideo: Boolean, - showFlag: Boolean = true, - sendToChannel: Boolean = false, - modifier: Modifier = StdPadding, - accountViewModel: AccountViewModel, - nav: INav, -) { - Column(Modifier.fillMaxWidth()) { - if (showVideo && baseChannel is LiveActivitiesChannel) { - ShowVideoStreaming(baseChannel, accountViewModel) - } - - val expanded = remember { mutableStateOf(false) } - - Column( - verticalArrangement = Arrangement.Center, - modifier = - modifier.clickable { - if (sendToChannel) { - nav.nav(routeFor(baseChannel)) - } else { - expanded.value = !expanded.value - } - }, - ) { - ShortChannelHeader( - baseChannel = baseChannel, - accountViewModel = accountViewModel, - nav = nav, - showFlag = showFlag, - ) - - if (expanded.value) { - LongChannelHeader(baseChannel = baseChannel, accountViewModel = accountViewModel, nav = nav) - } - } - } -} - -@Composable -fun ShowVideoStreaming( - baseChannel: LiveActivitiesChannel, - accountViewModel: AccountViewModel, -) { - baseChannel.info?.let { - SensitivityWarning( - event = it, - accountViewModel = accountViewModel, - ) { - val streamingInfo by - baseChannel.live - .map { - val activity = it.channel as? LiveActivitiesChannel - activity?.info - }.distinctUntilChanged() - .observeAsState(baseChannel.info) - - streamingInfo?.let { event -> - val url = remember(streamingInfo) { event.streaming() } - - url?.let { - CrossfadeCheckIfVideoIsOnline(url, accountViewModel) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center, - modifier = - remember { - Modifier - .fillMaxWidth() - .heightIn(min = 50.dp, max = 300.dp) - }, - ) { - val zoomableUrlVideo = - remember(streamingInfo) { - MediaUrlVideo( - url = url, - description = baseChannel.toBestDisplayName(), - artworkUri = event.image(), - authorName = baseChannel.creatorName(), - uri = baseChannel.toNAddr(), - ) - } - - ZoomableContentView( - content = zoomableUrlVideo, - roundedCorner = false, - contentScale = ContentScale.FillWidth, - accountViewModel = accountViewModel, - ) - } - } - } - } - } - } -} - -@Composable -fun ShortChannelHeader( - baseChannel: Channel, - accountViewModel: AccountViewModel, - nav: INav, - showFlag: Boolean, -) { - val channelState by baseChannel.live.observeAsState() - val channel = channelState?.channel ?: return - - Row(verticalAlignment = Alignment.CenterVertically) { - if (channel is LiveActivitiesChannel) { - channel.creator?.let { - UserPicture( - user = it, - size = Size34dp, - accountViewModel = accountViewModel, - nav = nav, - ) - } - } else { - channel.profilePicture()?.let { - RobohashFallbackAsyncImage( - robot = channel.idHex, - model = it, - contentDescription = stringRes(R.string.profile_image), - contentScale = ContentScale.Crop, - modifier = HeaderPictureModifier, - loadProfilePicture = accountViewModel.settings.showProfilePictures.value, - loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, - ) - } - } - - Column( - modifier = - Modifier - .padding(start = 10.dp) - .height(35.dp) - .weight(1f), - verticalArrangement = Arrangement.Center, - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = remember(channelState) { channel.toBestDisplayName() }, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - } - - Row( - modifier = - Modifier - .height(Size35dp) - .padding(start = 5.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - if (channel is PublicChatChannel) { - ShortChannelActionOptions(channel, accountViewModel, nav) - } - if (channel is LiveActivitiesChannel) { - LiveChannelActionOptions(channel, showFlag, accountViewModel, nav) - } - } - } -} - -@Composable -fun LongChannelHeader( - baseChannel: Channel, - lineModifier: Modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp), - accountViewModel: AccountViewModel, - nav: INav, -) { - val channelState by baseChannel.live.observeAsState() - val channel = channelState?.channel ?: return - - Row( - lineModifier, - ) { - val summary = remember(channelState) { channel.summary()?.ifBlank { null } } - - Column( - Modifier.weight(1f), - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - val defaultBackground = MaterialTheme.colorScheme.background - val background = remember { mutableStateOf(defaultBackground) } - - val tags = - remember(channelState) { - if (baseChannel is LiveActivitiesChannel) { - baseChannel.info?.tags?.toImmutableListOfLists() ?: EmptyTagList - } else { - EmptyTagList - } - } - - TranslatableRichTextViewer( - content = summary ?: stringRes(id = R.string.groups_no_descriptor), - canPreview = false, - quotesLeft = 1, - tags = tags, - backgroundColor = background, - id = baseChannel.idHex, - accountViewModel = accountViewModel, - nav = nav, - ) - } - - if (baseChannel is LiveActivitiesChannel && summary != null) { - baseChannel.info?.let { - if (it.hasHashtags()) { - DisplayUncitedHashtags(it, summary, nav) - } - } - } - } - - Column { - if (channel is PublicChatChannel) { - Row { - Spacer(DoubleHorzSpacer) - LongChannelActionOptions(channel, accountViewModel, nav) - } - } - } - } - - LoadNote(baseNoteHex = channel.idHex, accountViewModel) { loadingNote -> - loadingNote?.let { note -> - Row( - lineModifier, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringRes(id = R.string.owner), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.width(75.dp), - ) - Spacer(DoubleHorzSpacer) - NoteAuthorPicture(note, nav, accountViewModel, Size25dp) - Spacer(DoubleHorzSpacer) - NoteUsernameDisplay(note, Modifier.weight(1f), accountViewModel = accountViewModel) - } - - Row( - lineModifier, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringRes(id = R.string.created_at), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.width(75.dp), - ) - Spacer(DoubleHorzSpacer) - NormalTimeAgo(note, remember { Modifier.weight(1f) }) - MoreOptionsButton(note, null, accountViewModel, nav) - } - } - } - - if (channel is LiveActivitiesChannel) { - var participantUsers by remember(baseChannel) { - mutableStateOf>>( - persistentListOf(), - ) - } - - LaunchedEffect(key1 = channelState) { - launch(Dispatchers.IO) { - val newParticipantUsers = - channel.info - ?.participants() - ?.mapNotNull { part -> - LocalCache.checkGetOrCreateUser(part.pubKey)?.let { Pair(part, it) } - }?.toImmutableList() - - if ( - newParticipantUsers != null && !equalImmutableLists(newParticipantUsers, participantUsers) - ) { - participantUsers = newParticipantUsers - } - } - } - - participantUsers.forEach { - Row( - lineModifier.clickable { nav.nav("User/${it.second.pubkeyHex}") }, - verticalAlignment = Alignment.CenterVertically, - ) { - it.first.role?.let { it1 -> - Text( - text = it1.capitalize(Locale.ROOT), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.width(55.dp), - ) - } - Spacer(DoubleHorzSpacer) - ClickableUserPicture(it.second, Size25dp, accountViewModel) - Spacer(DoubleHorzSpacer) - UsernameDisplay(it.second, Modifier.weight(1f), accountViewModel = accountViewModel) - } - } - } -} - -@Composable -fun NormalTimeAgo( - baseNote: Note, - modifier: Modifier, -) { - val nowStr = stringRes(id = R.string.now) - - val time by - remember(baseNote) { derivedStateOf { timeAgoShort(baseNote.createdAt() ?: 0, nowStr) } } - - Text( - text = time, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = modifier, - ) -} - -@Composable -private 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(accountViewModel, channel, 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) -} - -@Composable -private fun LongChannelActionOptions( - channel: PublicChatChannel, - accountViewModel: AccountViewModel, - nav: INav, -) { - val isMe by - remember(accountViewModel) { - derivedStateOf { channel.creator == accountViewModel.account.userProfile() } - } - - if (isMe) { - EditButton(accountViewModel, channel) - } - - WatchChannelFollows(channel, accountViewModel) { isFollowing -> - if (isFollowing) { - LeaveChatButton(accountViewModel, channel, nav) - } - } -} - -@Composable -private fun LiveChannelActionOptions( - channel: LiveActivitiesChannel, - showFlag: Boolean = true, - accountViewModel: AccountViewModel, - nav: INav, -) { - val isLive by remember(channel) { derivedStateOf { channel.info?.status() == StatusTag.STATUS.LIVE.code } } - - val note = remember(channel.idHex) { LocalCache.getNoteIfExists(channel.idHex) } - - note?.let { - if (showFlag && isLive) { - LiveFlag() - Spacer(modifier = StdHorzSpacer) - } - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = RowColSpacing, - ) { - LikeReaction( - baseNote = it, - grayTint = MaterialTheme.colorScheme.onSurface, - accountViewModel = accountViewModel, - nav, - ) - } - Spacer(modifier = StdHorzSpacer) - ZapReaction( - baseNote = it, - grayTint = MaterialTheme.colorScheme.onSurface, - accountViewModel = accountViewModel, - nav = nav, - ) - } -} - -@Composable -fun LiveFlag() { - Text( - text = stringRes(id = R.string.live_stream_live_tag), - color = Color.White, - fontWeight = FontWeight.Bold, - fontSize = 16.sp, - modifier = - remember { - Modifier - .clip(SmallBorder) - .background(Color.Red) - .padding(horizontal = 5.dp) - }, - ) -} - -@Composable -fun EndedFlag() { - Text( - text = stringRes(id = R.string.live_stream_ended_tag), - color = Color.White, - fontWeight = FontWeight.Bold, - modifier = - remember { - Modifier - .clip(SmallBorder) - .background(Color.Black) - .padding(horizontal = 5.dp) - }, - ) -} - -@Composable -fun OfflineFlag() { - Text( - text = stringRes(id = R.string.live_stream_offline_tag), - color = Color.White, - fontWeight = FontWeight.Bold, - modifier = - remember { - Modifier - .clip(SmallBorder) - .background(Color.Black) - .padding(horizontal = 5.dp) - }, - ) -} - -@Composable -fun ScheduledFlag(starts: Long?) { - val startsIn = - starts?.let { - SimpleDateFormat - .getDateTimeInstance( - DateFormat.SHORT, - DateFormat.SHORT, - ).format(Date(starts * 1000)) - } - - Text( - text = startsIn ?: stringRes(id = R.string.live_stream_planned_tag), - color = Color.White, - fontWeight = FontWeight.Bold, - modifier = liveStreamTag, - ) -} - -@Composable -private fun NoteCopyButton(note: Channel) { - var popupExpanded by remember { mutableStateOf(false) } - - Button( - modifier = - Modifier - .padding(horizontal = 3.dp) - .width(50.dp), - onClick = { popupExpanded = true }, - shape = ButtonBorder, - colors = - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.placeholderText, - ), - ) { - Icon( - tint = Color.White, - imageVector = Icons.Default.Share, - contentDescription = stringRes(R.string.copies_the_note_id_to_the_clipboard_for_sharing), - ) - - DropdownMenu( - expanded = popupExpanded, - onDismissRequest = { popupExpanded = false }, - ) { - val clipboardManager = LocalClipboardManager.current - - DropdownMenuItem( - text = { Text(stringRes(R.string.copy_channel_id_note_to_the_clipboard)) }, - onClick = { - clipboardManager.setText(AnnotatedString("nostr:" + note.idNote())) - popupExpanded = false - }, - ) - } - } -} - -@Composable -private fun EditButton( - accountViewModel: AccountViewModel, - channel: PublicChatChannel, -) { - var wantsToPost by remember { mutableStateOf(false) } - - if (wantsToPost) { - NewChannelView({ wantsToPost = false }, accountViewModel, channel) - } - - Button( - modifier = - Modifier - .padding(horizontal = 3.dp) - .width(50.dp), - onClick = { wantsToPost = true }, - contentPadding = ZeroPadding, - ) { - Icon( - tint = Color.White, - imageVector = Icons.Default.EditNote, - contentDescription = stringRes(R.string.edits_the_channel_metadata), - ) - } -} - -@Composable -fun JoinChatButton( - accountViewModel: AccountViewModel, - channel: Channel, - nav: INav, -) { - val scope = rememberCoroutineScope() - - Button( - modifier = Modifier.padding(horizontal = 3.dp), - onClick = { scope.launch(Dispatchers.IO) { accountViewModel.account.follow(channel) } }, - contentPadding = ButtonPadding, - ) { - Text(text = stringRes(R.string.join), color = Color.White) - } -} - -@Composable -fun LeaveChatButton( - accountViewModel: AccountViewModel, - channel: Channel, - nav: INav, -) { - val scope = rememberCoroutineScope() - - Button( - modifier = Modifier.padding(horizontal = 3.dp), - onClick = { scope.launch(Dispatchers.IO) { accountViewModel.account.unfollow(channel) } }, - contentPadding = ButtonPadding, - ) { - Text(text = stringRes(R.string.leave), color = Color.White) - } -} - -@Composable -fun JoinCommunityButton( - accountViewModel: AccountViewModel, - note: AddressableNote, - nav: INav, -) { - val scope = rememberCoroutineScope() - - Button( - modifier = Modifier.padding(horizontal = 3.dp), - onClick = { scope.launch(Dispatchers.IO) { accountViewModel.account.follow(note) } }, - shape = ButtonBorder, - colors = - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - ), - contentPadding = ButtonPadding, - ) { - Text(text = stringRes(R.string.join), color = Color.White) - } -} - -@Composable -fun LeaveCommunityButton( - accountViewModel: AccountViewModel, - note: AddressableNote, - nav: INav, -) { - val scope = rememberCoroutineScope() - - Button( - modifier = Modifier.padding(horizontal = 3.dp), - onClick = { scope.launch(Dispatchers.IO) { accountViewModel.account.unfollow(note) } }, - shape = ButtonBorder, - colors = - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - ), - contentPadding = ButtonPadding, - ) { - Text(text = stringRes(R.string.leave), color = Color.White) - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ChannelHeader.kt new file mode 100644 index 000000000..8143f1b4a --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ChannelHeader.kt @@ -0,0 +1,91 @@ +/** + * 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 + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.vitorpamplona.amethyst.model.LiveActivitiesChannel +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.note.LoadChannel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.PublicChatChannelHeader +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveActivitiesChannelHeader +import com.vitorpamplona.amethyst.ui.theme.Size10dp +import com.vitorpamplona.amethyst.ui.theme.StdPadding +import com.vitorpamplona.amethyst.ui.theme.innerPostModifier + +@Composable +fun RenderChannelHeader( + channelNote: Note, + showVideo: Boolean, + sendToChannel: Boolean, + accountViewModel: AccountViewModel, + nav: INav, +) { + channelNote.channelHex()?.let { + ChannelHeader( + channelHex = it, + showVideo = showVideo, + sendToChannel = sendToChannel, + modifier = MaterialTheme.colorScheme.innerPostModifier.padding(Size10dp), + accountViewModel = accountViewModel, + nav = nav, + ) + } +} + +@Composable +fun ChannelHeader( + channelHex: String, + showVideo: Boolean, + showFlag: Boolean = true, + sendToChannel: Boolean = false, + modifier: Modifier = StdPadding, + accountViewModel: AccountViewModel, + nav: INav, +) { + LoadChannel(channelHex, accountViewModel) { + when (it) { + is LiveActivitiesChannel -> + LiveActivitiesChannelHeader( + it, + showVideo, + showFlag, + sendToChannel, + modifier, + accountViewModel, + nav, + ) + is PublicChatChannel -> + PublicChatChannelHeader( + it, + sendToChannel, + modifier, + accountViewModel, + nav, + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ChannelScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ChannelScreen.kt new file mode 100644 index 000000000..99f1c3b11 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ChannelScreen.kt @@ -0,0 +1,60 @@ +/** + * 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 + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.vitorpamplona.amethyst.model.LiveActivitiesChannel +import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.note.LoadChannel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.DisappearingScaffold +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.PublicChatTopBar +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.header.LiveActivityTopBar + +@Composable +fun ChannelScreen( + channelId: String?, + accountViewModel: AccountViewModel, + nav: INav, +) { + if (channelId == null) return + + DisappearingScaffold( + isInvertedLayout = true, + topBar = { + LoadChannel(channelId, accountViewModel) { + when (it) { + is PublicChatChannel -> PublicChatTopBar(it, accountViewModel, nav) + is LiveActivitiesChannel -> LiveActivityTopBar(it, accountViewModel, nav) + } + } + }, + accountViewModel = accountViewModel, + ) { + Column(Modifier.padding(it)) { + ChannelView(channelId, accountViewModel, nav) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ChannelView.kt new file mode 100644 index 000000000..a5c77336a --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ChannelView.kt @@ -0,0 +1,181 @@ +/** + * 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 + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.unit.dp +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.model.LiveActivitiesChannel +import com.vitorpamplona.amethyst.service.NostrChannelDataSource +import com.vitorpamplona.amethyst.service.NostrChannelDataSource.channel +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.note.LoadChannel +import com.vitorpamplona.amethyst.ui.screen.NostrChannelFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.ShowVideoStreaming +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.ChannelNewMessageViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.EditFieldRow +import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer +import kotlinx.coroutines.launch + +@Composable +fun ChannelView( + channelId: String?, + accountViewModel: AccountViewModel, + nav: INav, +) { + if (channelId == null) return + + LoadChannel(channelId, accountViewModel) { + PrepareChannelViewModels( + baseChannel = it, + accountViewModel = accountViewModel, + nav = nav, + ) + } +} + +@Composable +fun PrepareChannelViewModels( + baseChannel: Channel, + accountViewModel: AccountViewModel, + nav: INav, +) { + val feedViewModel: NostrChannelFeedViewModel = + viewModel( + key = baseChannel.idHex + "ChannelFeedViewModel", + factory = + NostrChannelFeedViewModel.Factory( + baseChannel, + accountViewModel.account, + ), + ) + + val channelScreenModel: ChannelNewMessageViewModel = viewModel() + channelScreenModel.init(accountViewModel) + channelScreenModel.load(baseChannel) + + ChannelView( + channel = baseChannel, + feedViewModel = feedViewModel, + newPostModel = channelScreenModel, + accountViewModel = accountViewModel, + nav = nav, + ) +} + +@Composable +fun ChannelView( + channel: Channel, + feedViewModel: NostrChannelFeedViewModel, + newPostModel: ChannelNewMessageViewModel, + accountViewModel: AccountViewModel, + nav: INav, +) { + NostrChannelDataSource.loadMessagesBetween(accountViewModel.account, channel) + + val lifeCycleOwner = LocalLifecycleOwner.current + + DisposableEffect(accountViewModel) { + NostrChannelDataSource.loadMessagesBetween(accountViewModel.account, channel) + NostrChannelDataSource.start() + feedViewModel.invalidateData(true) + + onDispose { + NostrChannelDataSource.clear() + NostrChannelDataSource.stop() + } + } + + DisposableEffect(lifeCycleOwner) { + val observer = + LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_RESUME) { + println("Channel Start") + NostrChannelDataSource.start() + feedViewModel.invalidateData(true) + } + if (event == Lifecycle.Event.ON_PAUSE) { + println("Channel Stop") + + NostrChannelDataSource.clear() + NostrChannelDataSource.stop() + } + } + + lifeCycleOwner.lifecycle.addObserver(observer) + onDispose { lifeCycleOwner.lifecycle.removeObserver(observer) } + } + + Column(Modifier.fillMaxHeight()) { + Column( + modifier = + remember { + Modifier + .fillMaxHeight() + .padding(vertical = 0.dp) + .weight(1f, true) + }, + ) { + if (channel is LiveActivitiesChannel) { + ShowVideoStreaming(channel, accountViewModel) + } + RefreshingChatroomFeedView( + viewModel = feedViewModel, + accountViewModel = accountViewModel, + nav = nav, + routeForLastRead = "Channel/${channel.idHex}", + avoidDraft = newPostModel.draftTag, + onWantsToReply = newPostModel::reply, + onWantsToEditDraft = newPostModel::editFromDraft, + ) + } + + Spacer(modifier = DoubleVertSpacer) + + val scope = rememberCoroutineScope() + + // LAST ROW + EditFieldRow( + newPostModel, + accountViewModel, + onSendNewMessage = { + scope.launch { + feedViewModel.sendToTop() + } + }, + nav, + ) + } +} 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 new file mode 100644 index 000000000..84a146658 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongChannelActionOptions.kt @@ -0,0 +1,120 @@ +/** + * 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) + } + + 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 new file mode 100644 index 000000000..1bd744c6d --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt @@ -0,0 +1,123 @@ +/** + * 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.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.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.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.ui.components.LoadNote +import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture +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.stringRes +import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer +import com.vitorpamplona.amethyst.ui.theme.Size25dp +import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList + +@Composable +fun LongPublicChatChannelHeader( + baseChannel: PublicChatChannel, + lineModifier: Modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp), + accountViewModel: AccountViewModel, + nav: INav, +) { + val channelState by baseChannel.live.observeAsState() + val channel = channelState?.channel as? PublicChatChannel ?: return + + Row(lineModifier) { + val summary = remember(channelState) { channel.summary()?.ifBlank { null } } + + Column(Modifier.weight(1f)) { + Row(verticalAlignment = Alignment.CenterVertically) { + val defaultBackground = MaterialTheme.colorScheme.background + val background = remember { mutableStateOf(defaultBackground) } + + TranslatableRichTextViewer( + content = summary ?: stringRes(id = R.string.groups_no_descriptor), + canPreview = false, + quotesLeft = 1, + tags = EmptyTagList, + backgroundColor = background, + id = baseChannel.idHex, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } + + Spacer(DoubleHorzSpacer) + LongChannelActionOptions(channel, accountViewModel, nav) + } + + LoadNote(baseNoteHex = channel.idHex, accountViewModel) { loadingNote -> + loadingNote?.let { note -> + Row( + lineModifier, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringRes(id = R.string.owner), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.width(75.dp), + ) + Spacer(DoubleHorzSpacer) + NoteAuthorPicture(note, nav, accountViewModel, Size25dp) + Spacer(DoubleHorzSpacer) + NoteUsernameDisplay(note, Modifier.weight(1f), accountViewModel = accountViewModel) + } + + Row( + lineModifier, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringRes(id = R.string.created_at), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.width(75.dp), + ) + Spacer(DoubleHorzSpacer) + NormalTimeAgo(note, remember { Modifier.weight(1f) }) + MoreOptionsButton(note, null, accountViewModel, nav) + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatChannelHeader.kt new file mode 100644 index 000000000..534593289 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatChannelHeader.kt @@ -0,0 +1,70 @@ +/** + * 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.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.StdPadding + +@Composable +fun PublicChatChannelHeader( + baseChannel: PublicChatChannel, + sendToChannel: Boolean = false, + modifier: Modifier = StdPadding, + accountViewModel: AccountViewModel, + nav: INav, +) { + Column(Modifier.fillMaxWidth()) { + val expanded = remember { mutableStateOf(false) } + + Column( + verticalArrangement = Arrangement.Center, + modifier = + modifier.clickable { + if (sendToChannel) { + nav.nav(routeFor(baseChannel)) + } else { + expanded.value = !expanded.value + } + }, + ) { + ShortPublicChatChannelHeader( + baseChannel = baseChannel, + accountViewModel = accountViewModel, + nav = nav, + ) + + if (expanded.value) { + LongPublicChatChannelHeader(baseChannel = baseChannel, accountViewModel = accountViewModel, nav = nav) + } + } + } +} 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 new file mode 100644 index 000000000..a4a42d252 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatTopBar.kt @@ -0,0 +1,48 @@ +/** + * 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.runtime.Composable +import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.TopBarExtensibleWithBackButton +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel + +@Composable +fun PublicChatTopBar( + baseChannel: PublicChatChannel, + accountViewModel: AccountViewModel, + nav: INav, +) { + TopBarExtensibleWithBackButton( + title = { + ShortPublicChatChannelHeader( + baseChannel = baseChannel, + accountViewModel = accountViewModel, + nav = nav, + ) + }, + extendableRow = { + LongPublicChatChannelHeader(baseChannel = baseChannel, accountViewModel = accountViewModel, nav = nav) + }, + popBack = nav::popBack, + ) +} 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 new file mode 100644 index 000000000..f7da4d439 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/ShortPublicChatChannelHeader.kt @@ -0,0 +1,96 @@ +/** + * 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.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.style.TextOverflow +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.RobohashFallbackAsyncImage +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.HeaderPictureModifier +import com.vitorpamplona.amethyst.ui.theme.Size35dp + +@Composable +fun ShortPublicChatChannelHeader( + baseChannel: PublicChatChannel, + accountViewModel: AccountViewModel, + nav: INav, +) { + val channelState by baseChannel.live.observeAsState() + val channel = channelState?.channel as? PublicChatChannel ?: return + + Row(verticalAlignment = Alignment.CenterVertically) { + channel.profilePicture()?.let { + RobohashFallbackAsyncImage( + robot = channel.idHex, + model = it, + contentDescription = stringRes(R.string.profile_image), + contentScale = ContentScale.Crop, + modifier = HeaderPictureModifier, + loadProfilePicture = accountViewModel.settings.showProfilePictures.value, + loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, + ) + } + + Column( + Modifier + .padding(start = 10.dp) + .height(35.dp) + .weight(1f), + verticalArrangement = Arrangement.Center, + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = remember(channelState) { channel.toBestDisplayName() }, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + + Row( + modifier = + Modifier + .height(Size35dp) + .padding(start = 5.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + ShortChannelActionOptions(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 new file mode 100644 index 000000000..2897570b8 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/EditChatButton.kt @@ -0,0 +1,69 @@ +/** + * 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 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.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.metadata.ChannelMetadataDialog +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.ZeroPadding + +@Composable +fun EditButton( + channel: PublicChatChannel, + accountViewModel: AccountViewModel, +) { + var wantsToPost by remember { mutableStateOf(false) } + + if (wantsToPost) { + ChannelMetadataDialog({ wantsToPost = false }, accountViewModel, channel) + } + + Button( + modifier = + Modifier + .padding(horizontal = 3.dp) + .width(50.dp), + onClick = { wantsToPost = true }, + contentPadding = ZeroPadding, + ) { + Icon( + tint = Color.White, + imageVector = Icons.Default.EditNote, + contentDescription = stringRes(R.string.edits_the_channel_metadata), + ) + } +} 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 new file mode 100644 index 000000000..35dbf3965 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/JoinChatButton.kt @@ -0,0 +1,48 @@ +/** + * 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 androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +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.ButtonPadding +import com.vitorpamplona.amethyst.ui.theme.HalfHalfHorzModifier + +@Composable +fun JoinChatButton( + channel: PublicChatChannel, + accountViewModel: AccountViewModel, + nav: INav, +) { + Button( + modifier = HalfHalfHorzModifier, + onClick = { accountViewModel.follow(channel) }, + contentPadding = ButtonPadding, + ) { + Text(text = stringRes(R.string.join), color = Color.White) + } +} 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 new file mode 100644 index 000000000..c91f1c045 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LeaveChatButton.kt @@ -0,0 +1,48 @@ +/** + * 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 androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +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.ButtonPadding +import com.vitorpamplona.amethyst.ui.theme.HalfHalfHorzModifier + +@Composable +fun LeaveChatButton( + channel: PublicChatChannel, + accountViewModel: AccountViewModel, + nav: INav, +) { + Button( + modifier = HalfHalfHorzModifier, + onClick = { accountViewModel.unfollow(channel) }, + contentPadding = ButtonPadding, + ) { + Text(text = stringRes(R.string.leave), color = Color.White) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataDialog.kt new file mode 100644 index 000000000..3cc03879f --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataDialog.kt @@ -0,0 +1,169 @@ +/** + * 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.metadata + +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.style.TextDirection +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton +import com.vitorpamplona.amethyst.ui.screen.loggedIn.PostButton +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.placeholderText + +@Composable +fun ChannelMetadataDialog( + onClose: () -> Unit, + accountViewModel: AccountViewModel, + channel: PublicChatChannel? = null, +) { + val postViewModel: ChannelMetadataViewModel = viewModel() + postViewModel.load(accountViewModel.account, channel) + + Dialog( + onDismissRequest = { onClose() }, + properties = + DialogProperties( + dismissOnClickOutside = false, + ), + ) { + DialogContent(postViewModel, onClose) + } +} + +@Composable +private fun DialogContent( + postViewModel: ChannelMetadataViewModel, + onClose: () -> Unit, +) { + Surface { + Column( + modifier = + Modifier + .padding(10.dp) + .verticalScroll(rememberScrollState()), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + CloseButton( + onPress = { + postViewModel.clear() + onClose() + }, + ) + + PostButton( + onPost = { + postViewModel.create() + onClose() + }, + postViewModel.channelName.value.text + .isNotBlank(), + ) + } + + Spacer(modifier = Modifier.height(15.dp)) + + OutlinedTextField( + label = { Text(text = stringRes(R.string.channel_name)) }, + modifier = Modifier.fillMaxWidth(), + value = postViewModel.channelName.value, + onValueChange = { postViewModel.channelName.value = it }, + placeholder = { + Text( + text = stringRes(R.string.my_awesome_group), + color = MaterialTheme.colorScheme.placeholderText, + ) + }, + keyboardOptions = + KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.Sentences, + ), + textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content), + ) + + Spacer(modifier = Modifier.height(15.dp)) + + OutlinedTextField( + label = { Text(text = stringRes(R.string.picture_url)) }, + modifier = Modifier.fillMaxWidth(), + value = postViewModel.channelPicture.value, + onValueChange = { postViewModel.channelPicture.value = it }, + placeholder = { + Text( + text = "http://mygroup.com/logo.jpg", + color = MaterialTheme.colorScheme.placeholderText, + ) + }, + ) + + Spacer(modifier = Modifier.height(15.dp)) + + OutlinedTextField( + label = { Text(text = stringRes(R.string.description)) }, + modifier = + Modifier + .fillMaxWidth() + .height(100.dp), + value = postViewModel.channelDescription.value, + onValueChange = { postViewModel.channelDescription.value = it }, + placeholder = { + Text( + text = stringRes(R.string.about_us), + color = MaterialTheme.colorScheme.placeholderText, + ) + }, + keyboardOptions = + KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.Sentences, + ), + textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content), + maxLines = 10, + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewChannelViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewChannelViewModel.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt index de37917fd..b16d9317c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewChannelViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt @@ -18,7 +18,7 @@ * 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.actions +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.metadata import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.text.input.TextFieldValue @@ -33,7 +33,7 @@ import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -class NewChannelViewModel : ViewModel() { +class ChannelMetadataViewModel : ViewModel() { private var account: Account? = null private var originalChannel: PublicChatChannel? = null diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivitiesChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivitiesChannelHeader.kt new file mode 100644 index 000000000..bd87cee07 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivitiesChannelHeader.kt @@ -0,0 +1,77 @@ +/** + * 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.nip53LiveActivities + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import com.vitorpamplona.amethyst.model.LiveActivitiesChannel +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.StdPadding + +@Composable +fun LiveActivitiesChannelHeader( + baseChannel: LiveActivitiesChannel, + showVideo: Boolean, + showFlag: Boolean = true, + sendToChannel: Boolean = false, + modifier: Modifier = StdPadding, + accountViewModel: AccountViewModel, + nav: INav, +) { + Column(Modifier.fillMaxWidth()) { + if (showVideo) { + ShowVideoStreaming(baseChannel, accountViewModel) + } + + val expanded = remember { mutableStateOf(false) } + + Column( + verticalArrangement = Arrangement.Center, + modifier = + modifier.clickable { + if (sendToChannel) { + nav.nav(routeFor(baseChannel)) + } else { + expanded.value = !expanded.value + } + }, + ) { + ShortLiveActivityChannelHeader( + baseChannel = baseChannel, + accountViewModel = accountViewModel, + nav = nav, + showFlag = showFlag, + ) + + if (expanded.value) { + LongLiveActivityChannelHeader(baseChannel, accountViewModel = accountViewModel, nav = nav) + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveChannelActionOptions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveChannelActionOptions.kt new file mode 100644 index 000000000..6f4ef4ddf --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveChannelActionOptions.kt @@ -0,0 +1,77 @@ +/** + * 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.nip53LiveActivities + +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.remember +import androidx.compose.ui.Alignment +import com.vitorpamplona.amethyst.model.LiveActivitiesChannel +import com.vitorpamplona.amethyst.model.LocalCache +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.theme.RowColSpacing +import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.StatusTag + +@Composable +fun LiveChannelActionOptions( + channel: LiveActivitiesChannel, + showFlag: Boolean = true, + accountViewModel: AccountViewModel, + nav: INav, +) { + val isLive by remember(channel) { derivedStateOf { channel.info?.status() == StatusTag.STATUS.LIVE.code } } + + val note = remember(channel.idHex) { LocalCache.getNoteIfExists(channel.idHex) } + + note?.let { + if (showFlag && isLive) { + LiveFlag() + Spacer(modifier = StdHorzSpacer) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = RowColSpacing, + ) { + LikeReaction( + baseNote = it, + grayTint = MaterialTheme.colorScheme.onSurface, + accountViewModel = accountViewModel, + nav, + ) + } + Spacer(modifier = StdHorzSpacer) + ZapReaction( + baseNote = it, + grayTint = MaterialTheme.colorScheme.onSurface, + accountViewModel = accountViewModel, + nav = nav, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LongLiveActivityChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LongLiveActivityChannelHeader.kt new file mode 100644 index 000000000..43978417a --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LongLiveActivityChannelHeader.kt @@ -0,0 +1,195 @@ +/** + * 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.nip53LiveActivities + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.LiveActivitiesChannel +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.components.LoadNote +import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture +import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture +import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay +import com.vitorpamplona.amethyst.ui.note.UsernameDisplay +import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags +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.notifications.equalImmutableLists +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer +import com.vitorpamplona.amethyst.ui.theme.Size25dp +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags +import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList +import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.util.Locale + +@Composable +fun LongLiveActivityChannelHeader( + baseChannel: LiveActivitiesChannel, + lineModifier: Modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp), + accountViewModel: AccountViewModel, + nav: INav, +) { + val channelState by baseChannel.live.observeAsState() + val channel = channelState?.channel as? LiveActivitiesChannel ?: return + + Row( + lineModifier, + ) { + val summary = remember(channelState) { channel.summary()?.ifBlank { null } } + + Column( + Modifier.weight(1f), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + val defaultBackground = MaterialTheme.colorScheme.background + val background = remember { mutableStateOf(defaultBackground) } + + val tags = remember(channelState) { baseChannel.info?.tags?.toImmutableListOfLists() ?: EmptyTagList } + + TranslatableRichTextViewer( + content = summary ?: stringRes(id = R.string.groups_no_descriptor), + canPreview = false, + quotesLeft = 1, + tags = tags, + backgroundColor = background, + id = baseChannel.idHex, + accountViewModel = accountViewModel, + nav = nav, + ) + } + + if (summary != null) { + baseChannel.info?.let { + if (it.hasHashtags()) { + DisplayUncitedHashtags(it, summary, nav) + } + } + } + } + } + + LoadNote(baseNoteHex = channel.idHex, accountViewModel) { loadingNote -> + loadingNote?.let { note -> + Row( + lineModifier, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringRes(id = R.string.owner), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.width(75.dp), + ) + Spacer(DoubleHorzSpacer) + NoteAuthorPicture(note, nav, accountViewModel, Size25dp) + Spacer(DoubleHorzSpacer) + NoteUsernameDisplay(note, Modifier.weight(1f), accountViewModel = accountViewModel) + } + + Row( + lineModifier, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringRes(id = R.string.created_at), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.width(75.dp), + ) + Spacer(DoubleHorzSpacer) + NormalTimeAgo(note, remember { Modifier.weight(1f) }) + MoreOptionsButton(note, null, accountViewModel, nav) + } + } + } + + var participantUsers by remember(baseChannel) { + mutableStateOf>>( + persistentListOf(), + ) + } + + LaunchedEffect(key1 = channelState) { + launch(Dispatchers.IO) { + val newParticipantUsers = + channel.info + ?.participants() + ?.mapNotNull { part -> + LocalCache.checkGetOrCreateUser(part.pubKey)?.let { Pair(part, it) } + }?.toImmutableList() + + if ( + newParticipantUsers != null && !equalImmutableLists(newParticipantUsers, participantUsers) + ) { + participantUsers = newParticipantUsers + } + } + } + + participantUsers.forEach { + Row( + lineModifier.clickable { nav.nav("User/${it.second.pubkeyHex}") }, + verticalAlignment = Alignment.CenterVertically, + ) { + it.first.role?.let { it1 -> + Text( + text = it1.capitalize(Locale.ROOT), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.width(55.dp), + ) + } + Spacer(DoubleHorzSpacer) + ClickableUserPicture(it.second, Size25dp, accountViewModel) + Spacer(DoubleHorzSpacer) + UsernameDisplay(it.second, Modifier.weight(1f), accountViewModel = accountViewModel) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShortLiveActivityChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShortLiveActivityChannelHeader.kt new file mode 100644 index 000000000..c1767758d --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShortLiveActivityChannelHeader.kt @@ -0,0 +1,91 @@ +/** + * 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.nip53LiveActivities + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.model.LiveActivitiesChannel +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.note.UserPicture +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.Size34dp +import com.vitorpamplona.amethyst.ui.theme.Size35dp + +@Composable +fun ShortLiveActivityChannelHeader( + baseChannel: LiveActivitiesChannel, + accountViewModel: AccountViewModel, + nav: INav, + showFlag: Boolean, +) { + val channelState by baseChannel.live.observeAsState() + val channel = channelState?.channel as? LiveActivitiesChannel ?: return + + Row(verticalAlignment = Alignment.CenterVertically) { + channel.creator?.let { + UserPicture( + user = it, + size = Size34dp, + accountViewModel = accountViewModel, + nav = nav, + ) + } + + Column( + modifier = + Modifier + .padding(start = 10.dp) + .height(35.dp) + .weight(1f), + verticalArrangement = Arrangement.Center, + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = remember(channelState) { channel.toBestDisplayName() }, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + + Row( + modifier = + Modifier + .height(Size35dp) + .padding(start = 5.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + LiveChannelActionOptions(channel, showFlag, accountViewModel, nav) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShowVideoStreaming.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShowVideoStreaming.kt new file mode 100644 index 000000000..e8c3d106d --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShowVideoStreaming.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.nip53LiveActivities + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.layout.ContentScale +import androidx.lifecycle.distinctUntilChanged +import androidx.lifecycle.map +import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo +import com.vitorpamplona.amethyst.model.LiveActivitiesChannel +import com.vitorpamplona.amethyst.ui.components.SensitivityWarning +import com.vitorpamplona.amethyst.ui.components.ZoomableContentView +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CrossfadeCheckIfVideoIsOnline +import com.vitorpamplona.amethyst.ui.theme.StreamingHeaderModifier + +@Composable +fun ShowVideoStreaming( + baseChannel: LiveActivitiesChannel, + accountViewModel: AccountViewModel, +) { + baseChannel.info?.let { + SensitivityWarning( + event = it, + accountViewModel = accountViewModel, + ) { + val streamingInfoEvent by + baseChannel.live + .map { + (it.channel as? LiveActivitiesChannel)?.info + }.distinctUntilChanged() + .observeAsState(baseChannel.info) + + streamingInfoEvent?.let { event -> + event.streaming()?.let { url -> + CrossfadeCheckIfVideoIsOnline(url, accountViewModel) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = StreamingHeaderModifier, + ) { + val zoomableUrlVideo = + remember(streamingInfoEvent) { + MediaUrlVideo( + url = url, + description = baseChannel.toBestDisplayName(), + artworkUri = event.image(), + authorName = baseChannel.creatorName(), + uri = baseChannel.toNAddr(), + ) + } + + ZoomableContentView( + content = zoomableUrlVideo, + roundedCorner = false, + contentScale = ContentScale.FillWidth, + accountViewModel = accountViewModel, + ) + } + } + } + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/StreamingStatusFlags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/StreamingStatusFlags.kt new file mode 100644 index 000000000..e2957d7fd --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/StreamingStatusFlags.kt @@ -0,0 +1,108 @@ +/** + * 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.nip53LiveActivities + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.SmallBorder +import com.vitorpamplona.amethyst.ui.theme.liveStreamTag +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.Date + +@Composable +fun LiveFlag() { + Text( + text = stringRes(id = R.string.live_stream_live_tag), + color = Color.White, + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + modifier = + remember { + Modifier + .clip(SmallBorder) + .background(Color.Red) + .padding(horizontal = 5.dp) + }, + ) +} + +@Composable +fun EndedFlag() { + Text( + text = stringRes(id = R.string.live_stream_ended_tag), + color = Color.White, + fontWeight = FontWeight.Bold, + modifier = + remember { + Modifier + .clip(SmallBorder) + .background(Color.Black) + .padding(horizontal = 5.dp) + }, + ) +} + +@Composable +fun OfflineFlag() { + Text( + text = stringRes(id = R.string.live_stream_offline_tag), + color = Color.White, + fontWeight = FontWeight.Bold, + modifier = + remember { + Modifier + .clip(SmallBorder) + .background(Color.Black) + .padding(horizontal = 5.dp) + }, + ) +} + +@Composable +fun ScheduledFlag(starts: Long?) { + val startsIn = + starts?.let { + SimpleDateFormat + .getDateTimeInstance( + DateFormat.SHORT, + DateFormat.SHORT, + ).format(Date(starts * 1000)) + } + + Text( + text = startsIn ?: stringRes(id = R.string.live_stream_planned_tag), + color = Color.White, + fontWeight = FontWeight.Bold, + modifier = liveStreamTag, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveActivityTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveActivityTopBar.kt new file mode 100644 index 000000000..8a713e17e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveActivityTopBar.kt @@ -0,0 +1,51 @@ +/** + * 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.nip53LiveActivities.header + +import androidx.compose.runtime.Composable +import com.vitorpamplona.amethyst.model.LiveActivitiesChannel +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.TopBarExtensibleWithBackButton +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LongLiveActivityChannelHeader +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.ShortLiveActivityChannelHeader + +@Composable +fun LiveActivityTopBar( + baseChannel: LiveActivitiesChannel, + accountViewModel: AccountViewModel, + nav: INav, +) { + TopBarExtensibleWithBackButton( + title = { + ShortLiveActivityChannelHeader( + baseChannel = baseChannel, + accountViewModel = accountViewModel, + nav = nav, + showFlag = true, + ) + }, + extendableRow = { + LongLiveActivityChannelHeader(baseChannel = baseChannel, accountViewModel = accountViewModel, nav = nav) + }, + popBack = nav::popBack, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt new file mode 100644 index 000000000..19be89cec --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -0,0 +1,653 @@ +/** + * 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.send + +import android.content.Context +import android.util.Log +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.text.input.TextFieldValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.compose.currentWord +import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor +import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord +import com.vitorpamplona.amethyst.commons.richtext.RichTextParser +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.model.LiveActivitiesChannel +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.LocationState +import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource +import com.vitorpamplona.amethyst.service.uploads.MediaCompressor +import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator +import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger +import com.vitorpamplona.amethyst.ui.actions.UserSuggestionAnchor +import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS +import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.ui.components.Split +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.UserSuggestions +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.ChatFileUploadState +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent +import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent +import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash +import com.vitorpamplona.quartz.nip01Core.tags.geohash.getGeoHash +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags +import com.vitorpamplona.quartz.nip01Core.tags.references.references +import com.vitorpamplona.quartz.nip10Notes.content.findHashtags +import com.vitorpamplona.quartz.nip10Notes.content.findNostrUris +import com.vitorpamplona.quartz.nip10Notes.content.findURLs +import com.vitorpamplona.quartz.nip18Reposts.quotes.quotes +import com.vitorpamplona.quartz.nip28PublicChat.base.notify +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent +import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji +import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag +import com.vitorpamplona.quartz.nip30CustomEmoji.emojis +import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive +import com.vitorpamplona.quartz.nip37Drafts.DraftEvent +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.chat.notify +import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup +import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplitSetup +import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount +import com.vitorpamplona.quartz.nip92IMeta.imetas +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import java.util.UUID + +@Stable +open class ChannelNewMessageViewModel : ViewModel() { + var draftTag: String by mutableStateOf(UUID.randomUUID().toString()) + + var accountViewModel: AccountViewModel? = null + var account: Account? = null + var channel: Channel? = null + + val replyTo = mutableStateOf(null) + + var uploadState by mutableStateOf(null) + val iMetaAttachments = IMetaAttachments() + var nip95attachments by mutableStateOf>>(emptyList()) + + var message by mutableStateOf(TextFieldValue("")) + var urlPreview by mutableStateOf(null) + var isUploadingImage by mutableStateOf(false) + + val userSuggestions = UserSuggestions() + var userSuggestionsMainMessage: UserSuggestionAnchor? = null + + val emojiSearch: MutableStateFlow = MutableStateFlow("") + val emojiSuggestions: StateFlow> by lazy { + account!! + .myEmojis + .combine(emojiSearch) { list, search -> + if (search.length == 1) { + list + } else if (search.isNotEmpty()) { + val code = search.removePrefix(":") + list.filter { it.code.startsWith(code) } + } else { + emptyList() + } + }.flowOn(Dispatchers.Default) + .stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5000), + emptyList(), + ) + } + + // Invoices + var canAddInvoice by mutableStateOf(false) + var wantsInvoice by mutableStateOf(false) + + // Forward Zap to + var wantsForwardZapTo by mutableStateOf(false) + var forwardZapTo by mutableStateOf>(Split()) + var forwardZapToEditting by mutableStateOf(TextFieldValue("")) + + // NSFW, Sensitive + var wantsToMarkAsSensitive by mutableStateOf(false) + + // GeoHash + var wantsToAddGeoHash by mutableStateOf(false) + var location: StateFlow? = null + var wantsExclusiveGeoPost by mutableStateOf(false) + + // ZapRaiser + var canAddZapRaiser by mutableStateOf(false) + var wantsZapraiser by mutableStateOf(false) + var zapRaiserAmount by mutableStateOf(null) + + val draftTextChanges = kotlinx.coroutines.channels.Channel(kotlinx.coroutines.channels.Channel.CONFLATED) + + fun lnAddress(): String? = account?.userProfile()?.info?.lnAddress() + + fun hasLnAddress(): Boolean = account?.userProfile()?.info?.lnAddress() != null + + fun user(): User? = account?.userProfile() + + open fun init(accountVM: AccountViewModel) { + this.accountViewModel = accountVM + this.account = accountVM.account + this.canAddInvoice = hasLnAddress() + this.canAddZapRaiser = hasLnAddress() + + this.uploadState = + ChatFileUploadState( + account?.settings?.defaultFileServer ?: DEFAULT_MEDIA_SERVERS[0], + ) + } + + open fun load(channel: Channel) { + this.channel = channel + } + + open fun reply(replyNote: Note) { + replyTo.value = replyNote + saveDraft() + } + + fun clearReply() { + replyTo.value = null + saveDraft() + } + + open fun editFromDraft(draft: Note) { + val noteEvent = draft.event + val noteAuthor = draft.author + + if (noteEvent is DraftEvent && noteAuthor != null) { + viewModelScope.launch(Dispatchers.IO) { + accountViewModel?.createTempDraftNote(noteEvent) { innerNote -> + if (innerNote != null) { + val oldTag = (draft.event as? AddressableEvent)?.dTag() + if (oldTag != null) { + draftTag = oldTag + } + loadFromDraft(innerNote) + } + } + } + } + } + + private fun loadFromDraft(draft: Note) { + val draftEvent = draft.event ?: return + + val localfowardZapTo = draftEvent.tags.zapSplitSetup() + val totalWeight = localfowardZapTo.sumOf { it.weight } + forwardZapTo = Split() + localfowardZapTo.forEach { + if (it is ZapSplitSetup) { + val user = LocalCache.getOrCreateUser(it.pubKeyHex) + forwardZapTo.addItem(user, (it.weight / totalWeight).toFloat()) + } + // don't support edditing old-style splits. + } + forwardZapToEditting = TextFieldValue("") + wantsForwardZapTo = localfowardZapTo.isNotEmpty() + + wantsToMarkAsSensitive = draftEvent.isSensitive() + + val geohash = draftEvent.getGeoHash() + wantsToAddGeoHash = geohash != null + + val zapraiser = draftEvent.zapraiserAmount() + wantsZapraiser = zapraiser != null + zapRaiserAmount = null + if (zapraiser != null) { + zapRaiserAmount = zapraiser + } + + if (forwardZapTo.items.isNotEmpty()) { + wantsForwardZapTo = true + } + + if (draftEvent as? ChannelMessageEvent != null) { + val replyId = draftEvent.reply()?.eventId + if (replyId != null) { + accountViewModel?.checkGetOrCreateNote(replyId) { + replyTo.value = it + } + } + } else if (draftEvent as? LiveActivitiesChatMessageEvent != null) { + val replyId = draftEvent.reply()?.eventId + if (replyId != null) { + accountViewModel?.checkGetOrCreateNote(replyId) { + replyTo.value = it + } + } + } + + message = TextFieldValue(draftEvent.content) + + urlPreview = findUrlInMessage() + } + + fun sendPost(onDone: () -> Unit) { + viewModelScope.launch(Dispatchers.IO) { + sendPostSync() + onDone() + } + } + + suspend fun sendPostSync() { + val template = createTemplate() ?: return + val channelRelays = channel?.relays() ?: emptyList() + + accountViewModel?.account?.signAndSendPrivately(template, channelRelays) + + accountViewModel?.deleteDraft(draftTag) + + cancel() + } + + fun sendDraft() { + viewModelScope.launch(Dispatchers.IO) { + sendDraftSync() + } + } + + suspend fun sendDraftSync() { + val accountViewModel = accountViewModel ?: return + + if (message.text.isBlank()) { + account?.deleteDraft(draftTag) + } else { + val template = createTemplate() ?: return + accountViewModel.account.createAndSendDraft(draftTag, template) + } + } + + fun pickedMedia(list: ImmutableList) { + uploadState?.load(list) + } + + fun upload( + onError: (title: String, message: String) -> Unit, + context: Context, + ) { + viewModelScope.launch(Dispatchers.Default) { + val myAccount = account ?: return@launch + val uploadState = uploadState ?: return@launch + + val myMultiOrchestrator = uploadState.multiOrchestrator ?: return@launch + + isUploadingImage = true + + val results = + myMultiOrchestrator.upload( + viewModelScope, + uploadState.caption, + uploadState.contentWarningReason, + MediaCompressor.intToCompressorQuality(uploadState.mediaQualitySlider), + uploadState.selectedServer, + myAccount, + context, + ) + + if (results.allGood) { + results.successful.forEach { + if (it.result is UploadOrchestrator.OrchestratorResult.NIP95Result) { + account?.createNip95(it.result.bytes, headerInfo = it.result.fileHeader, uploadState.caption, uploadState.contentWarningReason) { nip95 -> + nip95attachments = nip95attachments + nip95 + val note = nip95.let { it1 -> account?.consumeNip95(it1.first, it1.second) } + + note?.let { + message = message.insertUrlAtCursor(it.toNostrUri()) + } + + urlPreview = findUrlInMessage() + } + } else if (it.result is UploadOrchestrator.OrchestratorResult.ServerResult) { + iMetaAttachments.add(it.result, uploadState.caption, uploadState.contentWarningReason) + + message = message.insertUrlAtCursor(it.result.url) + urlPreview = findUrlInMessage() + } + } + + uploadState.reset() + } else { + val errorMessages = results.errors.map { stringRes(context, it.errorResource, *it.params) }.distinct() + + onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n")) + } + + isUploadingImage = false + } + } + + private suspend fun createTemplate(): EventTemplate? { + val channel = channel ?: return null + val accountViewModel = accountViewModel ?: return null + + val tagger = + NewMessageTagger( + message = message.text, + pTags = listOfNotNull(replyTo.value?.author), + eTags = listOfNotNull(replyTo.value), + channelHex = channel.idHex, + dao = accountViewModel, + ) + tagger.run() + + val urls = findURLs(message.text) + val usedAttachments = iMetaAttachments.filterIsIn(urls.toSet()) + val emojis = findEmoji(message.text, accountViewModel.account.myEmojis.value) + + val channelRelays = channel.relays() + val geoHash = (location?.value as? LocationState.LocationResult.Success)?.geoHash?.toString() + + return if (channel is PublicChatChannel) { + val replyingToEvent = replyTo.value?.toEventHint() + val channelEvent = channel.event + + if (replyingToEvent != null) { + ChannelMessageEvent.reply(tagger.message, replyingToEvent) { + notify(replyingToEvent.toPTag()) + + hashtags(findHashtags(tagger.message)) + references(findURLs(tagger.message)) + quotes(findNostrUris(tagger.message)) + + geoHash?.let { geohash(it) } + + emojis(emojis) + imetas(usedAttachments) + } + } else if (channelEvent != null) { + val hint = EventHintBundle(channelEvent, channelRelays.firstOrNull()) + ChannelMessageEvent.message(tagger.message, hint) { + hashtags(findHashtags(tagger.message)) + references(findURLs(tagger.message)) + quotes(findNostrUris(tagger.message)) + + geoHash?.let { geohash(it) } + + emojis(emojis) + imetas(usedAttachments) + } + } else { + ChannelMessageEvent.message(tagger.message, ETag(channel.idHex, channelRelays.firstOrNull())) { + hashtags(findHashtags(tagger.message)) + references(findURLs(tagger.message)) + quotes(findNostrUris(tagger.message)) + + geoHash?.let { geohash(it) } + + emojis(emojis) + imetas(usedAttachments) + } + } + } else if (channel is LiveActivitiesChannel) { + val replyingToEvent = replyTo.value?.toEventHint() + val activity = channel.info + + if (replyingToEvent != null) { + LiveActivitiesChatMessageEvent.reply(tagger.message, replyingToEvent) { + notify(replyingToEvent.toPTag()) + + hashtags(findHashtags(tagger.message)) + references(findURLs(tagger.message)) + quotes(findNostrUris(tagger.message)) + + emojis(emojis) + imetas(usedAttachments) + } + } else if (activity != null) { + val hint = EventHintBundle(activity, channelRelays.firstOrNull() ?: replyingToEvent?.relay) + + LiveActivitiesChatMessageEvent.message(tagger.message, hint) { + hashtags(findHashtags(tagger.message)) + references(findURLs(tagger.message)) + quotes(findNostrUris(tagger.message)) + + emojis(emojis) + imetas(usedAttachments) + } + } else { + LiveActivitiesChatMessageEvent.message(tagger.message, channel.toATag()) { + hashtags(findHashtags(tagger.message)) + references(findURLs(tagger.message)) + quotes(findNostrUris(tagger.message)) + + emojis(emojis) + imetas(usedAttachments) + } + } + } else { + null + } + } + + fun findEmoji( + message: String, + myEmojiSet: List?, + ): List { + if (myEmojiSet == null) return emptyList() + return CustomEmoji.findAllEmojiCodes(message).mapNotNull { possibleEmoji -> + myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.url.url) } + } + } + + open fun cancel() { + message = TextFieldValue("") + + replyTo.value = null + + urlPreview = null + + wantsInvoice = false + wantsZapraiser = false + zapRaiserAmount = null + + wantsForwardZapTo = false + wantsToMarkAsSensitive = false + + wantsToAddGeoHash = false + + forwardZapTo = Split() + forwardZapToEditting = TextFieldValue("") + + userSuggestions.reset() + userSuggestionsMainMessage = null + + if (emojiSearch.value.isNotEmpty()) { + emojiSearch.tryEmit("") + } + + draftTag = UUID.randomUUID().toString() + + NostrSearchEventOrUserDataSource.clear() + } + + fun deleteDraft() { + viewModelScope.launch(Dispatchers.IO) { + accountViewModel?.deleteDraft(draftTag) + } + } + + open fun findUrlInMessage(): String? = RichTextParser().parseValidUrls(message.text).firstOrNull() + + private fun saveDraft() { + draftTextChanges.trySend("") + } + + open fun addToMessage(it: String) { + updateMessage(TextFieldValue(message.text + " " + it)) + } + + open fun updateMessage(newMessage: TextFieldValue) { + message = newMessage + urlPreview = findUrlInMessage() + + if (newMessage.selection.collapsed) { + val lastWord = newMessage.currentWord() + + userSuggestionsMainMessage = UserSuggestionAnchor.MAIN_MESSAGE + + accountViewModel?.let { + userSuggestions.processCurrentWord(lastWord, it) + } + + if (lastWord.startsWith(":")) { + emojiSearch.tryEmit(lastWord) + } else { + if (emojiSearch.value.isNotBlank()) { + emojiSearch.tryEmit("") + } + } + } + + saveDraft() + } + + open fun updateZapForwardTo(newZapForwardTo: TextFieldValue) { + forwardZapToEditting = newZapForwardTo + if (newZapForwardTo.selection.collapsed) { + val lastWord = newZapForwardTo.text + userSuggestionsMainMessage = UserSuggestionAnchor.FORWARD_ZAPS + accountViewModel?.let { + userSuggestions.processCurrentWord(lastWord, it) + } + } + } + + open fun autocompleteWithUser(item: User) { + if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) { + val lastWord = message.currentWord() + message = userSuggestions.replaceCurrentWord(message, lastWord, item) + } else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) { + forwardZapTo.addItem(item) + forwardZapToEditting = TextFieldValue("") + } + + userSuggestionsMainMessage = null + userSuggestions.reset() + + saveDraft() + } + + open fun autocompleteWithEmoji(item: Account.EmojiMedia) { + val wordToInsert = ":${item.code}:" + message = message.replaceCurrentWord(wordToInsert) + + emojiSearch.tryEmit("") + + saveDraft() + } + + open fun autocompleteWithEmojiUrl(item: Account.EmojiMedia) { + val wordToInsert = item.url.url + " " + + viewModelScope.launch(Dispatchers.IO) { + iMetaAttachments.downloadAndPrepare( + item.url.url, + accountViewModel?.account?.shouldUseTorForImageDownload() ?: false, + ) + } + + message = message.replaceCurrentWord(wordToInsert) + + emojiSearch.tryEmit("") + + urlPreview = findUrlInMessage() + + saveDraft() + } + + fun canPost(): Boolean = + message.text.isNotBlank() && + uploadState?.isUploadingImage != true && + !wantsInvoice && + (!wantsZapraiser || zapRaiserAmount != null) && + uploadState?.multiOrchestrator == null + + fun insertAtCursor(newElement: String) { + message = message.insertUrlAtCursor(newElement) + } + + fun locationFlow(): StateFlow { + if (location == null) { + location = Amethyst.instance.locationManager.geohashStateFlow + } + + return location!! + } + + override fun onCleared() { + super.onCleared() + Log.d("Init", "OnCleared: ${this.javaClass.simpleName}") + } + + fun updateZapPercentage( + index: Int, + sliderValue: Float, + ) { + forwardZapTo.updatePercentage(index, sliderValue) + } + + fun updateZapFromText() { + viewModelScope.launch(Dispatchers.Default) { + val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), null, accountViewModel!!) + tagger.run() + tagger.pTags?.forEach { taggedUser -> + if (!forwardZapTo.items.any { it.key == taggedUser }) { + forwardZapTo.addItem(taggedUser) + } + } + } + } + + fun updateZapRaiserAmount(newAmount: Long?) { + zapRaiserAmount = newAmount + saveDraft() + } + + fun toggleMarkAsSensitive() { + wantsToMarkAsSensitive = !wantsToMarkAsSensitive + saveDraft() + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt new file mode 100644 index 000000000..2148f4ce0 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt @@ -0,0 +1,141 @@ +/** + * 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.send + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.style.TextDirection +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation +import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery +import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.note.ShowEmojiSuggestionList +import com.vitorpamplona.amethyst.ui.note.ShowUserSuggestionList +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ThinSendButton +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.EditFieldBorder +import com.vitorpamplona.amethyst.ui.theme.EditFieldLeadingIconModifier +import com.vitorpamplona.amethyst.ui.theme.EditFieldModifier +import com.vitorpamplona.amethyst.ui.theme.EditFieldTrailingIconModifier +import com.vitorpamplona.amethyst.ui.theme.placeholderText +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch + +@OptIn(FlowPreview::class) +@Composable +fun EditFieldRow( + channelScreenModel: ChannelNewMessageViewModel, + accountViewModel: AccountViewModel, + onSendNewMessage: () -> Unit, + nav: INav, +) { + channelScreenModel.replyTo.value?.let { + DisplayReplyingToNote(it, accountViewModel, nav) { + channelScreenModel.clearReply() + } + } + + LaunchedEffect(Unit) { + launch(Dispatchers.IO) { + channelScreenModel.draftTextChanges + .receiveAsFlow() + .debounce(1000) + .collectLatest { + channelScreenModel.sendDraft() + } + } + } + + Column( + modifier = EditFieldModifier, + ) { + ShowUserSuggestionList( + channelScreenModel.userSuggestions.userSuggestions, + channelScreenModel::autocompleteWithUser, + accountViewModel, + ) + + ShowEmojiSuggestionList( + channelScreenModel.emojiSuggestions, + channelScreenModel::autocompleteWithEmoji, + channelScreenModel::autocompleteWithEmojiUrl, + accountViewModel, + ) + + ThinPaddingTextField( + value = channelScreenModel.message, + onValueChange = { channelScreenModel.updateMessage(it) }, + keyboardOptions = + KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.Sentences, + ), + shape = EditFieldBorder, + modifier = Modifier.fillMaxWidth(), + placeholder = { + Text( + text = stringRes(R.string.reply_here), + color = MaterialTheme.colorScheme.placeholderText, + ) + }, + textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content), + trailingIcon = { + ThinSendButton( + isActive = + channelScreenModel.message.text.isNotBlank() && !channelScreenModel.isUploadingImage, + modifier = EditFieldTrailingIconModifier, + ) { + channelScreenModel.sendPost(onSendNewMessage) + } + }, + leadingIcon = { + SelectFromGallery( + isUploading = channelScreenModel.isUploadingImage, + tint = MaterialTheme.colorScheme.placeholderText, + modifier = EditFieldLeadingIconModifier, + onImageChosen = channelScreenModel::pickedMedia, + ) + }, + colors = + TextFieldDefaults.colors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + visualTransformation = UrlUserTagTransformation(MaterialTheme.colorScheme.primary), + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/list/ChannelFabColumn.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChannelFabColumn.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/list/ChannelFabColumn.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChannelFabColumn.kt index 09d17a288..e2984d219 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/list/ChannelFabColumn.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChannelFabColumn.kt @@ -18,7 +18,7 @@ * 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.list +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState @@ -48,10 +48,10 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.actions.NewChannelView import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.buildNewPostRoute import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.metadata.ChannelMetadataDialog import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Font12SP import com.vitorpamplona.amethyst.ui.theme.Size55Modifier @@ -66,7 +66,7 @@ fun ChannelFabColumn( var wantsToCreateChannel by remember { mutableStateOf(false) } if (wantsToCreateChannel) { - NewChannelView({ wantsToCreateChannel = false }, accountViewModel = accountViewModel) + ChannelMetadataDialog({ wantsToCreateChannel = false }, accountViewModel = accountViewModel) } Column { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/list/ChatroomHeaderCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/list/ChatroomHeaderCompose.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt index 4e1028273..1bfaaf44d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/list/ChatroomHeaderCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt @@ -18,7 +18,7 @@ * 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.list +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/list/MessagesScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/MessagesScreen.kt similarity index 91% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/list/MessagesScreen.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/MessagesScreen.kt index ff75c2755..1bd85d7ae 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/list/MessagesScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/MessagesScreen.kt @@ -18,7 +18,7 @@ * 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.list +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable @@ -27,8 +27,8 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.list.singlepane.MessagesSinglePane -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.list.twopane.MessagesTwoPane +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.singlepane.MessagesSinglePane +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.twopane.MessagesTwoPane @Composable fun MessagesScreen( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/list/WatchAccountForListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/WatchAccountForListScreen.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/list/WatchAccountForListScreen.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/WatchAccountForListScreen.kt index 21ef62ac1..c3f79437d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/list/WatchAccountForListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/WatchAccountForListScreen.kt @@ -18,7 +18,7 @@ * 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.list +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/list/WatchLifecycleAndRefreshDataSource.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/WatchLifecycleAndRefreshDataSource.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/list/WatchLifecycleAndRefreshDataSource.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/WatchLifecycleAndRefreshDataSource.kt index b360a8f9a..79c751e1d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/list/WatchLifecycleAndRefreshDataSource.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/WatchLifecycleAndRefreshDataSource.kt @@ -18,7 +18,7 @@ * 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.list +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/list/feed/ChatroomListFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/list/feed/ChatroomListFeedView.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt index 00a0a7d66..69d521b28 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/list/feed/ChatroomListFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt @@ -18,7 +18,7 @@ * 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.list.feed +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.feed import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Row @@ -42,7 +42,7 @@ import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.list.ChatroomHeaderCompose +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.ChatroomHeaderCompose import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.FeedPadding diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/list/feed/ChatroomListTabs.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/list/feed/ChatroomListTabs.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt index 86b36d8b3..0ce4facea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/list/feed/ChatroomListTabs.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt @@ -18,7 +18,7 @@ * 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.list.feed +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.feed import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/list/singlepane/MessagesSinglePane.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/singlepane/MessagesSinglePane.kt similarity index 86% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/list/singlepane/MessagesSinglePane.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/singlepane/MessagesSinglePane.kt index eaf27c6ff..df423756d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/list/singlepane/MessagesSinglePane.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/singlepane/MessagesSinglePane.kt @@ -18,7 +18,7 @@ * 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.list.singlepane +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.singlepane import androidx.compose.foundation.layout.Column import androidx.compose.foundation.pager.rememberPagerState @@ -35,12 +35,12 @@ import com.vitorpamplona.amethyst.ui.navigation.MainTopBar import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.DisappearingScaffold -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.list.ChannelFabColumn -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.list.WatchAccountForListScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.list.WatchLifecycleAndRefreshDataSource -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.list.feed.MessagesPager -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.list.feed.MessagesTabHeader -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.list.feed.MessagesTabItem +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.ChannelFabColumn +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.WatchAccountForListScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.WatchLifecycleAndRefreshDataSource +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.feed.MessagesPager +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.feed.MessagesTabHeader +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.feed.MessagesTabItem @Composable fun MessagesSinglePane( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/list/twopane/ChatroomListPane.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/ChatroomListPane.kt similarity index 84% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/list/twopane/ChatroomListPane.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/ChatroomListPane.kt index 212f019f7..5bd153c53 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/list/twopane/ChatroomListPane.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/ChatroomListPane.kt @@ -18,7 +18,7 @@ * 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.list.twopane +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.twopane import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -33,11 +33,11 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.list.WatchAccountForListScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.list.WatchLifecycleAndRefreshDataSource -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.list.feed.MessagesPager -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.list.feed.MessagesTabHeader -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.list.feed.MessagesTabItem +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.WatchAccountForListScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.WatchLifecycleAndRefreshDataSource +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.feed.MessagesPager +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.feed.MessagesTabHeader +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.feed.MessagesTabItem @Composable fun ChatroomList( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/list/twopane/MessagesTwoPane.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/list/twopane/MessagesTwoPane.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt index 053738bfd..2d88c2115 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/list/twopane/MessagesTwoPane.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt @@ -18,7 +18,7 @@ * 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.list.twopane +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.twopane import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -42,9 +42,9 @@ import com.vitorpamplona.amethyst.ui.navigation.MainTopBar import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.DisappearingScaffold -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.list.ChannelFabColumn import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.Chatroom -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.public.Channel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ChannelView +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.ChannelFabColumn import com.vitorpamplona.amethyst.ui.theme.Size20dp @Composable @@ -113,7 +113,7 @@ fun MessagesTwoPane( } if (it.route == "Channel") { - Channel( + ChannelView( channelId = it.id, accountViewModel = accountViewModel, nav = nav, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/list/twopane/TwoPaneNav.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/TwoPaneNav.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/list/twopane/TwoPaneNav.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/TwoPaneNav.kt index d4643b076..1d6b703e6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/list/twopane/TwoPaneNav.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/TwoPaneNav.kt @@ -18,7 +18,7 @@ * 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.list.twopane +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.twopane import androidx.compose.material3.DrawerState import androidx.compose.runtime.mutableStateOf diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/DisplayReplyingToNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/DisplayReplyingToNote.kt index ad4749b33..b8dbaa4bd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/DisplayReplyingToNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/DisplayReplyingToNote.kt @@ -39,7 +39,7 @@ import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.messages.ChatroomMessageCompose +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.ChatroomMessageCompose import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.placeholderText diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ThinSendButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ThinSendButton.kt index bc8a45e6d..f3a0d42ce 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ThinSendButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ThinSendButton.kt @@ -38,7 +38,7 @@ fun ThinSendButton( ) { IconButton( enabled = isActive, - modifier = modifier, + // modifier = modifier, onClick = onClick, ) { Icon( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt index 306ac1f37..5f44f0f28 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt @@ -75,7 +75,7 @@ import com.vitorpamplona.amethyst.ui.note.UserCompose import com.vitorpamplona.amethyst.ui.note.elements.ObserveRelayListForSearchAndDisplayIfNotFound import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.DisappearingScaffold -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.list.ChannelName +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.ChannelName import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.FeedPadding diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index c82b5255b..66bbc706d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -147,7 +147,7 @@ import com.vitorpamplona.amethyst.ui.note.types.VideoDisplay import com.vitorpamplona.amethyst.ui.screen.LevelFeedViewModel import com.vitorpamplona.amethyst.ui.screen.RenderFeedState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.public.ChannelHeader +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ChannelHeader import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ThinSendButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt index e59d34fa9..ed6ab88c6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt @@ -116,7 +116,9 @@ val HalfStartPadding = Modifier.padding(start = 5.dp) val StdStartPadding = Modifier.padding(start = 10.dp) val StdTopPadding = Modifier.padding(top = 10.dp) val HalfTopPadding = Modifier.padding(top = 5.dp) + val HalfHalfVertPadding = Modifier.padding(vertical = 3.dp) +val HalfHalfHorzModifier = Modifier.padding(horizontal = 3.dp) val HalfPadding = Modifier.padding(5.dp) val StdPadding = Modifier.padding(10.dp) @@ -209,7 +211,7 @@ val CashuCardBorders = Modifier.fillMaxWidth().padding(10.dp).clip(shape = Quote val EditFieldModifier = Modifier.padding(start = 10.dp, end = 10.dp, bottom = 10.dp, top = 5.dp).fillMaxWidth() -val EditFieldTrailingIconModifier = Modifier.height(26.dp).padding(start = 5.dp, end = 0.dp) +val EditFieldTrailingIconModifier = Modifier.padding(start = 5.dp, end = 0.dp) val EditFieldLeadingIconModifier = Modifier.height(32.dp).padding(start = 2.dp) val ZeroPadding = PaddingValues(0.dp) @@ -301,3 +303,8 @@ val ripple24dp = ripple(bounded = false, radius = Size24dp) val defaultTweenDuration = 100 val defaultTweenFloatSpec = tween(durationMillis = defaultTweenDuration) val defaultTweenIntOffsetSpec = tween(durationMillis = defaultTweenDuration) + +val StreamingHeaderModifier = + Modifier + .fillMaxWidth() + .heightIn(min = 50.dp, max = 300.dp) diff --git a/amethyst/src/main/res/values-ar/strings.xml b/amethyst/src/main/res/values-ar/strings.xml index 2dbcb8bce..5515a4f59 100644 --- a/amethyst/src/main/res/values-ar/strings.xml +++ b/amethyst/src/main/res/values-ar/strings.xml @@ -17,21 +17,21 @@ محتوى فاضح محتوى عشوائي عدد الأحداث المزعجة من هذا المرحل - التمثيل + أنتحال تصرف غير قانوني أخرى غير معروف - أيقونة الخادم + أيقونة المرحل كاتب غير معروف نسخ النص نسخ المفتاح العام - نسخ معرف + نسخ معرف الملاحظة بث - حذف الطلب + طلب حذفها حظر / تبليغ - الابلاغ عن البريد العشوائي / الاحتيال - الإبلاغ عن التمثيل + الإبلاغ عن إزعاج / احتيال + الإبلاغ عن انتحال الشخصية الإبلاغ عن المحتوى الصريح / الفاضح الإبلاغ عن سلوك غير قانوني الإبلاغ عن برمجيات خبيثة @@ -270,15 +270,15 @@ شارك رابط المتصفح مشاركة معرف المؤلف (ID) - رمز/معرف الملاحظة (Note ID) + معرف الملاحظة نسخ النص حذف الغاء المتابعة متابعة إزالته من المعرض إزالة هذه الوسيطة من المعرض. - طلب الحذف - سيطلب Amethyst حذف ملاحظتك من الخوادم (relays) الذي أنت متصل به حاليا. لا يوجد ضمان لحذف ملاحظتك بشكل دائم من تلك الخوادم أو من الخوادم الأخرى التي ربما حفظت فيها. + طلب حذفها + سيطلب Amethyst حذف ملاحظتك من المرحلات المتصل بها حاليا، لا يوجد ضمان لحذفها من هذه المرحلات أو من المرحلات الأخرى التي ربما حفظت فيها. حظر حذف حظر @@ -287,7 +287,7 @@ لا تظهر مرة أخرى رسائل مزعجة أو رسائل خداع سلوك بغيض - انتحال الشخصية + انتحال الشخصية محتوى دموي أو خادش للحياء تصرف غير قانوني برمجية خبيثة @@ -368,6 +368,7 @@ user@ او Lnaddres خوادم الوسائط المرحلات الخاصة بك (NIP-95) + الملفات مُستضافة في مرحلاتك. الـNIP جديد، تأكد من دعمهم له إعداد Tor/Orbot الاتصال من خلال إعدادات Orbot الخاص بك قطع الاتصال بTor/Orbot؟ @@ -476,8 +477,7 @@ غير متصل انتهى مجدولة - تسجيل الخروج سوف يؤدي الى جميع المعلومات المحلية الخاصة بك. تأكد من وجود نسخة احتياطية لمفاتحك السرية لتجنب فقدان حسابك. - هل تريد الاستمرار؟ + تسجيل الخروج سوف يحذف معلوماتك من على الجهاز. تأكد من نسخ مفتاحك الخاص لتجنب فقدان الحساب. هل تريد ألاستمرار؟ العلامات المُتابعة مرحلات مجتمع @@ -545,6 +545,7 @@ تسجيل الدخول باستخدام Amber تحديث الحالة خطأ أثناء تحليل رسالة المشكل + تُحسب الاصوات بكمية الزاب، يمكنك وضع حد أعلى لتجنب انحياز التصويت وحد ادنى لتجنب المزعجين. يمكنك استخدام نفس الكمية للحدين للتأكد من ان جميع الاصوات متساوية. لا تضع أي حدود لإستقبال أي كمية. رسالة إلى المستخدم موافق فشل الوصول إلى @@ -554,6 +555,7 @@ فشل تحليل الرد من %1$s: %2$s المرحل رفض الطلب %1$s: %2$s فشل الوصول إلى %1$s: %2$s + فشل تحليل مخرجات %1$s: %2$s فشل %1$s مع الرمز %2$s نشط لـ: الصفحة الرئيسية @@ -643,7 +645,27 @@ الرد إعجاب تغيير ردود الفعل السريعة + المرحل %1$s + توسعة قائمة المرحلات + منتقي قائمة المرحلات + مرحلات الرسائل الخاصة + مرحلات: %1$s + أستخدام المرحلات العادية + أعد مرحلات رسائلك الخاصة + هذا الإعداد يوضح للعملاء أين يرسلون الرسائل الخاصة لك، بدونه قد تفقد بعضهم. + الخيارات الجيدة هي:\n - inbox.nostr.wine (مدفوع)\n - auth.nostr1.com (مجاني)\n - you.nostr1.com (مرحلات خاصة - مدفوع) + الخيارات الجيدة هي:\n - auth.nostr1.com (مجاني)\n - inbox.nostr.wine (مدفوع)\n - relay.0xchat.com (مجاني) + اختر من واحد إلى ثلاث مرحلات لاستقبال رسائلك الخاصة بهم. مرحلات الرسائل الخاصة يجب أن تستقبل الرسائل من أي شخص وتسمح لك فقط بقراءتهم. + الإعداد الآن + مرحلات البحث + أعد مرحلات البحث + إنشاء قائمة مرحلات مخصصة للبحث سوف يحسن من نتائج البحث. + أدخل من واحد إلى ثلاث مرحلات لاستخدامهم عند البحث عن محتوى أو اشخاص، تأكد من أن هذا المرحلات تدعم NIP-50 + بعض المرحلات الجيدة:\n - nostr.wine\n - relay.nostr.band\n - relay.noswhere.com + إعدادات المرحل تعديل المنشور مسح رمز ال QR البحث عن تطبيق + حذف الكل + متأكد من حذف جميع المسودات؟ diff --git a/amethyst/src/main/res/values-fr/strings.xml b/amethyst/src/main/res/values-fr/strings.xml index f5533727b..7a45dad67 100644 --- a/amethyst/src/main/res/values-fr/strings.xml +++ b/amethyst/src/main/res/values-fr/strings.xml @@ -81,6 +81,13 @@ Merci beaucoup ! Montant en Sats Envoyer des Sats + Créateur d\'émojis secrets + Ajouter un émoji avec un message caché à la publication + Note secrète pour le destinataire + Mon message caché + Préfixe visible + 😎 + Ajouter au message "Erreur d'analyse de l'aperçu pour %1$s : %2$s" "Aperçu de l'image pour %1$s" Nouveau canal @@ -106,6 +113,7 @@ Flux mondial Flux de recherche Ajouter un relais + Nom Nom visible du public Mon nom visible du public Ostrich McGénial diff --git a/amethyst/src/main/res/values-hi-rIN/strings.xml b/amethyst/src/main/res/values-hi-rIN/strings.xml index a1077eb6e..24b790b76 100644 --- a/amethyst/src/main/res/values-hi-rIN/strings.xml +++ b/amethyst/src/main/res/values-hi-rIN/strings.xml @@ -81,6 +81,13 @@ बहुत धन्यवाद! मात्रा साट्स में साट्स भेजें + रहस्य चित्राक्षर कर्ता + गुप्त सन्देश युक्त चित्राक्षर जोडें पत्र के साथ + गुप्त टीका ग्राहक के लिए + मेरा गुप्त सन्देश + दृश्यमान उपसर्ग + 😎 + पत्र के साथ जोडें "%1$s का पूर्वीक्षण परखने में अपक्रम : %2$s" "%1$s के लिए पूर्वीक्षण पत्ता चित्र" नयी प्रणाली diff --git a/amethyst/src/main/res/values-hu/strings.xml b/amethyst/src/main/res/values-hu/strings.xml index 8d0d447a3..103dc3ff2 100644 --- a/amethyst/src/main/res/values-hu/strings.xml +++ b/amethyst/src/main/res/values-hu/strings.xml @@ -81,6 +81,13 @@ Nagyon szépen köszönöm! Összeg satoshiban Satoshi küldése + Titkos emodzsikészítő + Emodzsi hozzáadása rejtett üzenettel a bejegyzéshez + Titkos jegyzet a kedvezményezettnek + Saját rejtett üzenet + Látható előtag + 😎 + Hozzáadás a bejegyzéshez "Hiba a következő előnézet elemzésekor %1$s : %2$s" "Kártyakép előnézete a következőhöz: %1$s" Új csatorna diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index c812a5d4a..f2a494754 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -81,6 +81,13 @@ Dziękuję bardzo! Kwota w Satsach Wyślij + Kreator tajnych emoji + Dodaj emoji z ukrytą wiadomością do wpisu + Tajna notatka dla odbiorcy + Treść tajnej notatki + Widoczny prefiks + 😎 + Dodaj do wpisu "Błąd analizowania podglądu dla %1$s : %2$s" "Podgląd obrazu karty dla %1$s" Nowy kanał diff --git a/amethyst/src/main/res/values-zh-rCN/strings.xml b/amethyst/src/main/res/values-zh-rCN/strings.xml index 4622785ca..b70f711a2 100644 --- a/amethyst/src/main/res/values-zh-rCN/strings.xml +++ b/amethyst/src/main/res/values-zh-rCN/strings.xml @@ -81,6 +81,13 @@ 非常感谢! 聪金额 发送聪 + 私密 Emoji 生成器 + 在帖子中添加 Emoji 到隐藏消息的帖子中 + 发送私密笔记给收件人 + 我的隐藏消息 + 可见前缀 + 😎 + 添加到帖子 "解析 %1$s 预览出错:%2$s" "预览 %1$s 卡片图片" 新频道 @@ -106,6 +113,7 @@ 全球 搜索 添加中继器 + 名称 显示名称 我的显示名称 Nostr 太酷了 @@ -141,6 +149,7 @@ 会话 动态 回复 + 你的 相册 "关注" "举报" diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSigner.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSigner.kt index af3f876f3..cb9e70b98 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSigner.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSigner.kt @@ -109,4 +109,21 @@ abstract class NostrSigner( ) as T, ) } + + fun assembleRumor(ev: EventTemplate) = assembleRumor(ev.createdAt, ev.kind, ev.tags, ev.content) + + fun assembleRumor( + createdAt: Long, + kind: Int, + tags: Array>, + content: String, + ) = EventFactory.create( + id = EventHasher.hashId(pubKey, createdAt, kind, tags, content), + pubKey = pubKey, + createdAt = createdAt, + kind = kind, + tags = tags, + content = content, + sig = "", + ) as T } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/message/ChannelMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/message/ChannelMessageEvent.kt index 4c5c418e4..0ae65be00 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/message/ChannelMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/message/ChannelMessageEvent.kt @@ -23,14 +23,17 @@ package com.vitorpamplona.quartz.nip28PublicChat.message import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.core.tagArray import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent +import com.vitorpamplona.quartz.nip10Notes.tags.markedETag import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel import com.vitorpamplona.quartz.nip28PublicChat.base.channel import com.vitorpamplona.quartz.nip28PublicChat.base.reply +import com.vitorpamplona.quartz.nip37Drafts.ExposeInDraft import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -42,7 +45,8 @@ class ChannelMessageEvent( content: String, sig: HexKey, ) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig), - IsInPublicChatChannel { + IsInPublicChatChannel, + ExposeInDraft { override fun channel() = markedRoot() ?: unmarkedRoot() override fun channelId() = channel()?.eventId @@ -51,6 +55,12 @@ class ChannelMessageEvent( override fun unmarkedReplyTos() = super.unmarkedReplyTos().filter { it != channelId() } + override fun exposeInDraft() = + tagArray { + channel()?.let { markedETag(it) } + reply()?.let { markedETag(it) } + } + companion object { const val KIND = 42 const val ALT = "Public chat message" diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftBuilder.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftBuilder.kt new file mode 100644 index 000000000..64753dd76 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftBuilder.kt @@ -0,0 +1,61 @@ +/** + * 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.quartz.nip37Drafts + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag +import com.vitorpamplona.quartz.nip01Core.tags.kinds.kind +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip37Drafts.DraftEvent.Companion.ALT_DESCRIPTION +import com.vitorpamplona.quartz.nip37Drafts.DraftEvent.Companion.KIND +import com.vitorpamplona.quartz.utils.TimeUtils + +class DraftBuilder { + companion object { + fun encryptAndSign( + dTag: String, + draft: T, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (DraftEvent) -> Unit, + ) { + signer.nip44Encrypt(draft.toJson(), signer.pubKey) { encryptedContent -> + val template = + eventTemplate(KIND, encryptedContent, createdAt) { + alt(ALT_DESCRIPTION) + dTag(dTag) + kind(draft.kind) + + if (draft is ExposeInDraft) { + addAll(draft.exposeInDraft()) + } + } + + signer.sign(template) { + it.addToCache(signer.pubKey, draft) + onReady(it) + } + } + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEvent.kt index a0de590fc..8181069da 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEvent.kt @@ -28,6 +28,8 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag +import com.vitorpamplona.quartz.nip01Core.tags.kinds.kind import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent @@ -121,6 +123,7 @@ class DraftEvent( companion object { const val KIND = 31234 + const val ALT_DESCRIPTION = "Draft Event" fun createAddressTag( pubKey: HexKey, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/ExposeInDraft.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/ExposeInDraft.kt new file mode 100644 index 000000000..12710bb4b --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/ExposeInDraft.kt @@ -0,0 +1,27 @@ +/** + * 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.quartz.nip37Drafts + +import com.vitorpamplona.quartz.nip01Core.core.TagArray + +interface ExposeInDraft { + fun exposeInDraft(): TagArray +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/chat/LiveActivitiesChatMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/chat/LiveActivitiesChatMessageEvent.kt index 65f82c8c3..5444f2f83 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/chat/LiveActivitiesChatMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/chat/LiveActivitiesChatMessageEvent.kt @@ -23,16 +23,20 @@ package com.vitorpamplona.quartz.nip53LiveActivities.chat import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.core.tagArray import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.aTag import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent +import com.vitorpamplona.quartz.nip10Notes.tags.markedETag import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag +import com.vitorpamplona.quartz.nip37Drafts.ExposeInDraft import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.utils.TimeUtils @@ -47,7 +51,8 @@ class LiveActivitiesChatMessageEvent( ) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig), EventHintProvider, PubKeyHintProvider, - AddressHintProvider { + AddressHintProvider, + ExposeInDraft { override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + tags.mapNotNull(QTag::parseEventAsHint) @@ -64,6 +69,12 @@ class LiveActivitiesChatMessageEvent( override fun unmarkedReplyTos() = super.markedReplyTos().minus(activityHex() ?: "") + override fun exposeInDraft() = + tagArray { + activity()?.let { aTag(it) } + reply()?.let { markedETag(it) } + } + companion object { const val KIND = 1311 const val ALT = "Live activity chat message" diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/UrlId.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/UrlId.kt index 996256fa8..404288d1d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/UrlId.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/UrlId.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.quartz.nip73ExternalIds import com.vitorpamplona.quartz.utils.toStringNoFragment -import com.vitorpamplona.quartz.utils.toStringSchemeHost import org.czeal.rfc3986.URIReference class UrlId( @@ -37,6 +36,6 @@ class UrlId( companion object { fun toScope(url: String) = URIReference.parse(url).normalize().toStringNoFragment() - fun toKind(url: String) = URIReference.parse(url).normalize().toStringSchemeHost() + fun toKind(url: String) = "web" } }