From e898d582398eb788a2ae59be562464a229ad71a9 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 1 Apr 2024 17:07:27 -0400 Subject: [PATCH] Finishing the rendering of card notes for DMs and live chats --- .../amethyst/ui/navigation/RouteMaker.kt | 63 +++++++++++--- .../amethyst/ui/note/NoteCompose.kt | 26 ++++++ .../amethyst/ui/note/types/ChatMessage.kt | 77 +++++++++++++++++ .../ui/note/types/LiveActivityChatMessage.kt | 84 +++++++++++++++++++ .../amethyst/ui/note/types/PrivateMessage.kt | 23 +++++ .../ui/screen/ChatroomListFeedView.kt | 2 - .../ui/screen/loggedIn/ChatroomScreen.kt | 8 -- .../vitorpamplona/quartz/events/DraftEvent.kt | 4 + 8 files changed, 264 insertions(+), 23 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessage.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivityChatMessage.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/RouteMaker.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/RouteMaker.kt index e27c3d5be..418662dcf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/RouteMaker.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/RouteMaker.kt @@ -25,12 +25,14 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.events.AddressableEvent import com.vitorpamplona.quartz.events.ChannelCreateEvent -import com.vitorpamplona.quartz.events.ChannelMessageEvent -import com.vitorpamplona.quartz.events.ChannelMetadataEvent import com.vitorpamplona.quartz.events.ChatroomKey import com.vitorpamplona.quartz.events.ChatroomKeyable import com.vitorpamplona.quartz.events.CommunityDefinitionEvent +import com.vitorpamplona.quartz.events.DraftEvent +import com.vitorpamplona.quartz.events.EventInterface +import com.vitorpamplona.quartz.events.IsInPublicChatChannel import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.events.LiveActivitiesEvent import kotlinx.collections.immutable.persistentSetOf @@ -40,18 +42,51 @@ fun routeFor( note: Note, loggedIn: User, ): String? { - val noteEvent = note.event + val noteEvent = note.event ?: return "Note/${URLEncoder.encode(note.idHex, "utf-8")}" - if ( - noteEvent is ChannelMessageEvent || - noteEvent is ChannelCreateEvent || - noteEvent is ChannelMetadataEvent - ) { - note.channelHex()?.let { + return routeFor(noteEvent, loggedIn) +} + +fun routeFor( + noteEvent: EventInterface, + loggedIn: User, +): String? { + if (noteEvent is DraftEvent) { + val innerEvent = noteEvent.preCachedDraft(loggedIn.pubkeyHex) + + if (innerEvent is IsInPublicChatChannel) { + innerEvent.channel()?.let { + return "Channel/$it" + } + } else if (innerEvent is LiveActivitiesEvent) { + innerEvent.address().toTag().let { + return "Channel/${URLEncoder.encode(it, "utf-8")}" + } + } else if (innerEvent is LiveActivitiesChatMessageEvent) { + innerEvent.activity()?.toTag()?.let { + return "Channel/${URLEncoder.encode(it, "utf-8")}" + } + } else if (innerEvent is ChatroomKeyable) { + val room = innerEvent.chatroomKey(loggedIn.pubkeyHex) + loggedIn.createChatroom(room) + return "Room/${room.hashCode()}" + } else if (innerEvent is AddressableEvent) { + return "Note/${URLEncoder.encode(noteEvent.address().toTag(), "utf-8")}" + } else { + return "Note/${URLEncoder.encode(noteEvent.id(), "utf-8")}" + } + } else if (noteEvent is IsInPublicChatChannel) { + noteEvent.channel()?.let { return "Channel/$it" } - } else if (noteEvent is LiveActivitiesEvent || noteEvent is LiveActivitiesChatMessageEvent) { - note.channelHex()?.let { + } else if (noteEvent is ChannelCreateEvent) { + return "Channel/${noteEvent.id}" + } else if (noteEvent is LiveActivitiesEvent) { + noteEvent.address().toTag().let { + return "Channel/${URLEncoder.encode(it, "utf-8")}" + } + } else if (noteEvent is LiveActivitiesChatMessageEvent) { + noteEvent.activity()?.toTag()?.let { return "Channel/${URLEncoder.encode(it, "utf-8")}" } } else if (noteEvent is ChatroomKeyable) { @@ -59,9 +94,11 @@ fun routeFor( loggedIn.createChatroom(room) return "Room/${room.hashCode()}" } else if (noteEvent is CommunityDefinitionEvent) { - return "Community/${URLEncoder.encode(note.idHex, "utf-8")}" + return "Community/${URLEncoder.encode(noteEvent.address().toTag(), "utf-8")}" + } else if (noteEvent is AddressableEvent) { + return "Note/${URLEncoder.encode(noteEvent.address().toTag(), "utf-8")}" } else { - return "Note/${URLEncoder.encode(note.idHex, "utf-8")}" + return "Note/${URLEncoder.encode(noteEvent.id(), "utf-8")}" } return null diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 609bbc80b..1c9e320bf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -85,6 +85,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderAppDefinition import com.vitorpamplona.amethyst.ui.note.types.RenderAudioHeader import com.vitorpamplona.amethyst.ui.note.types.RenderAudioTrack import com.vitorpamplona.amethyst.ui.note.types.RenderBadgeAward +import com.vitorpamplona.amethyst.ui.note.types.RenderChatMessage import com.vitorpamplona.amethyst.ui.note.types.RenderClassifieds import com.vitorpamplona.amethyst.ui.note.types.RenderEmojiPack import com.vitorpamplona.amethyst.ui.note.types.RenderFhirResource @@ -92,6 +93,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderGitIssueEvent import com.vitorpamplona.amethyst.ui.note.types.RenderGitPatchEvent import com.vitorpamplona.amethyst.ui.note.types.RenderGitRepositoryEvent import com.vitorpamplona.amethyst.ui.note.types.RenderHighlight +import com.vitorpamplona.amethyst.ui.note.types.RenderLiveActivityChatMessage import com.vitorpamplona.amethyst.ui.note.types.RenderLiveActivityEvent import com.vitorpamplona.amethyst.ui.note.types.RenderLongFormContent import com.vitorpamplona.amethyst.ui.note.types.RenderPinListEvent @@ -139,6 +141,7 @@ import com.vitorpamplona.quartz.events.BaseTextNoteEvent import com.vitorpamplona.quartz.events.ChannelCreateEvent import com.vitorpamplona.quartz.events.ChannelMessageEvent import com.vitorpamplona.quartz.events.ChannelMetadataEvent +import com.vitorpamplona.quartz.events.ChatMessageEvent import com.vitorpamplona.quartz.events.ClassifiedsEvent import com.vitorpamplona.quartz.events.CommunityDefinitionEvent import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent @@ -618,6 +621,18 @@ private fun RenderNoteRow( nav, ) } + is ChatMessageEvent -> { + RenderChatMessage( + baseNote, + makeItShort, + canPreview, + quotesLeft, + backgroundColor, + editState, + accountViewModel, + nav, + ) + } is ClassifiedsEvent -> { RenderClassifieds( noteEvent, @@ -672,6 +687,17 @@ private fun RenderNoteRow( nav, ) } + is LiveActivitiesChatMessageEvent -> + RenderLiveActivityChatMessage( + baseNote, + makeItShort, + canPreview, + quotesLeft, + backgroundColor, + editState, + accountViewModel, + nav, + ) else -> { RenderTextEvent( baseNote, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessage.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessage.kt new file mode 100644 index 000000000..13e3a779d --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessage.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.note.types + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.components.GenericLoadable +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomHeader +import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer +import com.vitorpamplona.amethyst.ui.theme.replyModifier +import com.vitorpamplona.quartz.events.ChatroomKeyable + +@Composable +fun RenderChatMessage( + note: Note, + makeItShort: Boolean, + canPreview: Boolean, + quotesLeft: Int, + backgroundColor: MutableState, + editState: State>, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val userRoom by + remember(note) { + derivedStateOf { + (note.event as? ChatroomKeyable)?.chatroomKey(accountViewModel.userProfile().pubkeyHex) + } + } + + userRoom?.let { + if (it.users.size > 1 || (it.users.size == 1 && note.author == accountViewModel.account.userProfile())) { + ChatroomHeader(it, MaterialTheme.colorScheme.replyModifier.padding(10.dp), accountViewModel, nav) + Spacer(modifier = StdVertSpacer) + } + } + + RenderTextEvent( + note, + makeItShort, + canPreview, + quotesLeft, + backgroundColor, + editState, + accountViewModel, + nav, + ) +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivityChatMessage.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivityChatMessage.kt new file mode 100644 index 000000000..369fe586a --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivityChatMessage.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.note.types + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.components.GenericLoadable +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader +import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer +import com.vitorpamplona.amethyst.ui.theme.replyModifier +import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent + +@Composable +fun RenderLiveActivityChatMessage( + note: Note, + makeItShort: Boolean, + canPreview: Boolean, + quotesLeft: Int, + backgroundColor: MutableState, + editState: State>, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val noteEvent = note.event + val showChannelInfo = + remember(noteEvent) { + if (noteEvent is LiveActivitiesChatMessageEvent) { + noteEvent.activity()?.toTag() + } else { + null + } + } + + showChannelInfo?.let { + ChannelHeader( + channelHex = it, + showVideo = false, + sendToChannel = true, + modifier = MaterialTheme.colorScheme.replyModifier.padding(10.dp), + accountViewModel = accountViewModel, + nav = nav, + ) + Spacer(modifier = StdVertSpacer) + } + + RenderTextEvent( + note, + makeItShort, + canPreview, + quotesLeft, + backgroundColor, + editState, + accountViewModel, + nav, + ) +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt index 67d88ca68..9a6088065 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt @@ -20,16 +20,21 @@ */ package com.vitorpamplona.amethyst.ui.note.types +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.SensitivityWarning @@ -37,8 +42,12 @@ import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContent import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomHeader +import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.amethyst.ui.theme.replyModifier import com.vitorpamplona.quartz.encoders.toNpub +import com.vitorpamplona.quartz.events.ChatroomKeyable import com.vitorpamplona.quartz.events.EmptyTagList import com.vitorpamplona.quartz.events.PrivateDmEvent import com.vitorpamplona.quartz.events.toImmutableListOfLists @@ -57,6 +66,20 @@ fun RenderPrivateMessage( ) { val noteEvent = note.event as? PrivateDmEvent ?: return + val userRoom by + remember(note) { + derivedStateOf { + (note.event as? ChatroomKeyable)?.chatroomKey(accountViewModel.userProfile().pubkeyHex) + } + } + + userRoom?.let { + if (it.users.size > 1 || (it.users.size == 1 && note.author == accountViewModel.account.userProfile())) { + ChatroomHeader(it, MaterialTheme.colorScheme.replyModifier.padding(10.dp), accountViewModel, nav) + Spacer(modifier = StdVertSpacer) + } + } + val withMe = remember { noteEvent.with(accountViewModel.userProfile().pubkeyHex) } if (withMe) { LoadDecryptedContent(note, accountViewModel) { eventContent -> diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt index 6e775bb63..943f81a7e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt @@ -39,7 +39,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.StdTopPadding -import kotlin.time.ExperimentalTime @Composable fun ChatroomListFeedView( @@ -81,7 +80,6 @@ private fun CrossFadeState( } } -@OptIn(ExperimentalTime::class) @Composable private fun FeedLoaded( state: FeedState.Loaded, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt index d7699b22b..dfe9b374b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt @@ -645,10 +645,6 @@ fun ChatroomHeader( } } } - - HorizontalDivider( - thickness = DividerThickness, - ) } } @@ -685,10 +681,6 @@ fun GroupChatroomHeader( LongRoomHeader(room = room, accountViewModel = accountViewModel, nav = nav) } } - - HorizontalDivider( - thickness = DividerThickness, - ) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/DraftEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/DraftEvent.kt index 4543042bc..d2803317c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/DraftEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/DraftEvent.kt @@ -45,6 +45,10 @@ class DraftEvent( return cachedInnerEvent[signer.pubKey] } + fun preCachedDraft(pubKey: HexKey): Event? { + return cachedInnerEvent[pubKey] + } + fun allCache() = cachedInnerEvent.values fun addToCache(