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 d2d0e6682..deaceaad6 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 @@ -61,8 +61,8 @@ 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.private.ChatroomScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.private.ChatroomScreenByAuthor +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.communities.CommunityScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.DiscoverScreen @@ -244,7 +244,7 @@ fun AppNavigation( popEnterTransition = { scaleIn }, popExitTransition = { slideOutHorizontallyToEnd }, ) { - ChatroomScreenByAuthor(it.id(), null, accountViewModel, nav) + ChatroomByAuthorScreen(it.id(), null, accountViewModel, nav) } composable( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessage.kt index eb4c82414..4b66a1e19 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessage.kt @@ -36,7 +36,7 @@ import com.vitorpamplona.amethyst.ui.components.GenericLoadable 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.private.ChatroomHeader +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.ChatroomHeader import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.replyModifier import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable 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 84a6e21d2..70f58baa6 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 @@ -49,7 +49,7 @@ 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.private.ChatroomHeader +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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt index fe8bd24c6..3f8157de3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt @@ -43,7 +43,7 @@ import com.vitorpamplona.amethyst.ui.navigation.routeFor 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.chats.private.ChatroomHeader +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.ChatroomHeader import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.placeholderText 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/list/ChatroomHeaderCompose.kt index a49e2c677..4e1028273 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/list/ChatroomHeaderCompose.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.list import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width @@ -54,15 +53,12 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.distinctUntilChanged -import androidx.lifecycle.map import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled -import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.layouts.ChatHeaderLayout import com.vitorpamplona.amethyst.ui.navigation.INav @@ -71,9 +67,9 @@ import com.vitorpamplona.amethyst.ui.note.LoadChannel import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContentOrNull import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures import com.vitorpamplona.amethyst.ui.note.ObserveDraftEvent -import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.note.timeAgo import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.RoomNameDisplay import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.AccountPictureModifier import com.vitorpamplona.amethyst.ui.theme.Size55dp @@ -85,7 +81,6 @@ import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip37Drafts.DraftEvent -import kotlin.math.min @Composable fun ChatroomHeaderCompose( @@ -303,131 +298,6 @@ private fun UserRoomCompose( ) } -@Composable -fun RoomNameDisplay( - room: ChatroomKey, - modifier: Modifier, - accountViewModel: AccountViewModel, -) { - val roomSubject by - accountViewModel - .userProfile() - .live() - .messages - .map { it.user.privateChatrooms[room]?.subject } - .distinctUntilChanged() - .observeAsState(accountViewModel.userProfile().privateChatrooms[room]?.subject) - - CrossfadeIfEnabled(targetState = roomSubject, modifier, label = "RoomNameDisplay", accountViewModel = accountViewModel) { - if (!it.isNullOrBlank()) { - if (room.users.size > 1) { - DisplayRoomSubject(it) - } else { - DisplayUserAndSubject(room.users.first(), it, accountViewModel) - } - } else { - DisplayUserSetAsSubject(room, accountViewModel) - } - } -} - -@Composable -private fun DisplayUserAndSubject( - user: HexKey, - subject: String, - accountViewModel: AccountViewModel, -) { - Row { - Text( - text = subject, - fontWeight = FontWeight.Bold, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - Text( - text = " - ", - fontWeight = FontWeight.Bold, - maxLines = 1, - ) - LoadUser(baseUserHex = user, accountViewModel = accountViewModel) { - it?.let { UsernameDisplay(it, Modifier.weight(1f), accountViewModel = accountViewModel) } - } - } -} - -@Composable -fun DisplayUserSetAsSubject( - room: ChatroomKey, - accountViewModel: AccountViewModel, - fontWeight: FontWeight = FontWeight.Bold, -) { - val userList = remember(room) { room.users.toList() } - - if (userList.size == 1) { - // Regular Design - Row { - LoadUser(baseUserHex = userList[0], accountViewModel) { - it?.let { UsernameDisplay(it, Modifier.weight(1f), fontWeight = fontWeight, accountViewModel = accountViewModel) } - } - } - } else { - Row { - userList.take(4).forEachIndexed { index, value -> - LoadUser(baseUserHex = value, accountViewModel) { - it?.let { ShortUsernameDisplay(baseUser = it, fontWeight = fontWeight, accountViewModel = accountViewModel) } - } - - if (min(userList.size, 4) - 1 != index) { - Text( - text = ", ", - fontWeight = fontWeight, - maxLines = 1, - ) - } - } - } - } -} - -@Composable -fun DisplayRoomSubject( - roomSubject: String, - fontWeight: FontWeight = FontWeight.Bold, -) { - Row { - Text( - text = roomSubject, - fontWeight = fontWeight, - maxLines = 1, - ) - } -} - -@Composable -fun ShortUsernameDisplay( - baseUser: User, - weight: Modifier = Modifier, - fontWeight: FontWeight = FontWeight.Bold, - accountViewModel: AccountViewModel, -) { - val userName by - baseUser - .live() - .metadata - .map { it.user.toBestShortFirstName() } - .distinctUntilChanged() - .observeAsState(baseUser.toBestShortFirstName()) - - CrossfadeIfEnabled(targetState = userName, modifier = weight, accountViewModel = accountViewModel) { - CreateTextWithEmoji( - text = it, - tags = baseUser.info?.tags, - fontWeight = fontWeight, - maxLines = 1, - ) - } -} - @Composable fun LoadUser( baseUserHex: String, 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/list/twopane/MessagesTwoPane.kt index 06f201b59..053738bfd 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/list/twopane/MessagesTwoPane.kt @@ -43,7 +43,7 @@ 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.private.Chatroom +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.theme.Size20dp diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/private/ChatroomScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/private/ChatroomScreen.kt deleted file mode 100644 index a056f300a..000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/private/ChatroomScreen.kt +++ /dev/null @@ -1,985 +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.private - -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.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.EditNote -import androidx.compose.material.icons.filled.Send -import androidx.compose.material3.Button -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -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.material3.TextFieldDefaults -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -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.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.platform.LocalLifecycleOwner -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.TextAlign -import androidx.compose.ui.text.style.TextDirection -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties -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.model.LocalCache -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.NostrChatroomDataSource -import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled -import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia -import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.TopBarExtensibleWithBackButton -import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture -import com.vitorpamplona.amethyst.ui.note.IncognitoIconOff -import com.vitorpamplona.amethyst.ui.note.IncognitoIconOn -import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures -import com.vitorpamplona.amethyst.ui.note.QuickActionAlertDialog -import com.vitorpamplona.amethyst.ui.note.ShowEmojiSuggestionList -import com.vitorpamplona.amethyst.ui.note.ShowUserSuggestionList -import com.vitorpamplona.amethyst.ui.note.UserCompose -import com.vitorpamplona.amethyst.ui.note.UsernameDisplay -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.CloseButton -import com.vitorpamplona.amethyst.ui.screen.loggedIn.DisappearingScaffold -import com.vitorpamplona.amethyst.ui.screen.loggedIn.PostButton -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.list.DisplayRoomSubject -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.list.DisplayUserSetAsSubject -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.list.LoadUser -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote -import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.BottomTopHeight -import com.vitorpamplona.amethyst.ui.theme.DividerThickness -import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer -import com.vitorpamplona.amethyst.ui.theme.EditFieldBorder -import com.vitorpamplona.amethyst.ui.theme.EditFieldModifier -import com.vitorpamplona.amethyst.ui.theme.EditFieldTrailingIconModifier -import com.vitorpamplona.amethyst.ui.theme.Size20Modifier -import com.vitorpamplona.amethyst.ui.theme.Size30Modifier -import com.vitorpamplona.amethyst.ui.theme.Size34dp -import com.vitorpamplona.amethyst.ui.theme.StdPadding -import com.vitorpamplona.amethyst.ui.theme.ZeroPadding -import com.vitorpamplona.amethyst.ui.theme.placeholderText -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey -import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent -import com.vitorpamplona.quartz.nip17Dm.messages.changeSubject -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentSetOf -import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.launch - -@Composable -fun ChatroomScreen( - roomId: String?, - draftMessage: String? = null, - replyToNote: HexKey? = null, - editFromDraft: HexKey? = null, - accountViewModel: AccountViewModel, - nav: INav, -) { - if (roomId == null) return - - DisappearingScaffold( - isInvertedLayout = true, - topBar = { - RoomTopBar(roomId, accountViewModel, nav) - }, - accountViewModel = accountViewModel, - ) { - Column(Modifier.padding(it)) { - Chatroom(roomId, draftMessage, replyToNote, editFromDraft, accountViewModel, nav) - } - } -} - -@Composable -private fun RoomTopBar( - id: String, - accountViewModel: AccountViewModel, - nav: INav, -) { - LoadRoom(roomId = id, accountViewModel) { room -> - if (room != null) { - RenderRoomTopBar(room, accountViewModel, nav) - } else { - Spacer(BottomTopHeight) - } - } -} - -@Composable -private fun RenderRoomTopBar( - room: ChatroomKey, - accountViewModel: AccountViewModel, - nav: INav, -) { - if (room.users.size == 1) { - TopBarExtensibleWithBackButton( - title = { - LoadUser(baseUserHex = room.users.first(), accountViewModel) { baseUser -> - if (baseUser != null) { - ClickableUserPicture( - baseUser = baseUser, - accountViewModel = accountViewModel, - size = Size34dp, - ) - - Spacer(modifier = DoubleHorzSpacer) - - UsernameDisplay(baseUser, Modifier.weight(1f), fontWeight = FontWeight.Normal, accountViewModel = accountViewModel) - } - } - }, - extendableRow = { - LoadUser(baseUserHex = room.users.first(), accountViewModel) { - if (it != null) { - UserCompose( - baseUser = it, - accountViewModel = accountViewModel, - nav = nav, - ) - } - } - }, - popBack = nav::popBack, - ) - } else { - TopBarExtensibleWithBackButton( - title = { - Row(verticalAlignment = Alignment.CenterVertically) { - NonClickableUserPictures( - room = room, - accountViewModel = accountViewModel, - size = Size34dp, - ) - - RoomNameOnlyDisplay(room, Modifier.padding(start = 10.dp).weight(1f), FontWeight.Normal, accountViewModel) - } - }, - extendableRow = { - LongRoomHeader(room = room, accountViewModel = accountViewModel, nav = nav) - }, - popBack = nav::popBack, - ) - } -} - -@Composable -fun Chatroom( - roomId: String?, - draftMessage: String? = null, - replyToNote: HexKey? = null, - editFromDraft: HexKey? = null, - accountViewModel: AccountViewModel, - nav: INav, -) { - if (roomId == null) return - - LoadRoom(roomId, accountViewModel) { - it?.let { - PrepareChatroomViewModels( - room = it, - draftMessage = draftMessage, - replyToNote = replyToNote, - editFromDraft = editFromDraft, - accountViewModel = accountViewModel, - nav = nav, - ) - } - } -} - -@Composable -fun ChatroomScreenByAuthor( - authorPubKeyHex: String?, - draftMessage: String? = null, - accountViewModel: AccountViewModel, - nav: INav, -) { - if (authorPubKeyHex == null) return - - DisappearingScaffold( - isInvertedLayout = true, - topBar = { - RoomByAuthorTopBar(authorPubKeyHex, accountViewModel, nav) - }, - accountViewModel = accountViewModel, - ) { - Column(Modifier.padding(it)) { - ChatroomByAuthor(authorPubKeyHex, draftMessage, accountViewModel, nav) - } - } -} - -@Composable -private fun RoomByAuthorTopBar( - authorPubKeyHex: String, - accountViewModel: AccountViewModel, - nav: INav, -) { - LoadRoomByAuthor(authorPubKeyHex = authorPubKeyHex, accountViewModel) { room -> - if (room != null) { - RenderRoomTopBar(room, accountViewModel, nav) - } else { - Spacer(BottomTopHeight) - } - } -} - -@Composable -fun ChatroomByAuthor( - authorPubKeyHex: String?, - draftMessage: String? = null, - accountViewModel: AccountViewModel, - nav: INav, -) { - if (authorPubKeyHex == null) return - - LoadRoomByAuthor(authorPubKeyHex, accountViewModel) { - it?.let { - PrepareChatroomViewModels( - room = it, - draftMessage = draftMessage, - replyToNote = null, - editFromDraft = null, - accountViewModel = accountViewModel, - nav = nav, - ) - } - } -} - -@Composable -fun LoadRoom( - roomId: String, - accountViewModel: AccountViewModel, - content: @Composable (ChatroomKey?) -> Unit, -) { - var room by remember(roomId) { mutableStateOf(null) } - - if (room == null) { - LaunchedEffect(key1 = roomId) { - launch(Dispatchers.IO) { - val newRoom = - accountViewModel.userProfile().privateChatrooms.keys.firstOrNull { - it.hashCode().toString() == roomId - } - if (room != newRoom) { - room = newRoom - } - } - } - } - - content(room) -} - -@Composable -fun LoadRoomByAuthor( - authorPubKeyHex: String, - accountViewModel: AccountViewModel, - content: @Composable (ChatroomKey?) -> Unit, -) { - val room by - remember(authorPubKeyHex) { - mutableStateOf(ChatroomKey(persistentSetOf(authorPubKeyHex))) - } - - content(room) -} - -@Composable -fun PrepareChatroomViewModels( - room: ChatroomKey, - draftMessage: String?, - replyToNote: HexKey? = null, - editFromDraft: HexKey? = null, - accountViewModel: AccountViewModel, - nav: INav, -) { - val feedViewModel: NostrChatroomFeedViewModel = - viewModel( - key = room.hashCode().toString() + "ChatroomViewModels", - factory = - NostrChatroomFeedViewModel.Factory( - room, - accountViewModel.account, - ), - ) - - val newPostModel: ChatNewMessageViewModel = viewModel() - newPostModel.init(accountViewModel) - newPostModel.load(room) - - if (replyToNote != null) { - LaunchedEffect(key1 = replyToNote) { - accountViewModel.checkGetOrCreateNote(replyToNote) { - if (it != null) { - newPostModel.reply(it) - } - } - } - } - if (editFromDraft != null) { - LaunchedEffect(key1 = replyToNote) { - accountViewModel.checkGetOrCreateNote(editFromDraft) { - if (it != null) { - newPostModel.editFromDraft(it) - } - } - } - } - - if (room.users.size == 1) { - // Activates NIP-17 if the user has DM relays - ObserveRelayListForDMs(pubkey = room.users.first(), accountViewModel = accountViewModel) { - if (it?.relays().isNullOrEmpty()) { - newPostModel.nip17 = false - } else { - newPostModel.nip17 = true - } - } - } - - val imageUpload: ChatFileUploadModel = viewModel() - - if (draftMessage != null) { - LaunchedEffect(key1 = draftMessage) { - newPostModel.updateMessage(TextFieldValue(draftMessage)) - } - } - - ChatroomScreen( - room = room, - feedViewModel = feedViewModel, - newPostModel = newPostModel, - fileUpload = imageUpload, - accountViewModel = accountViewModel, - nav = nav, - ) -} - -@Composable -fun ChatroomScreen( - room: ChatroomKey, - feedViewModel: NostrChatroomFeedViewModel, - newPostModel: ChatNewMessageViewModel, - fileUpload: ChatFileUploadModel, - accountViewModel: AccountViewModel, - nav: INav, -) { - NostrChatroomDataSource.loadMessagesBetween(accountViewModel.account, room) - - val lifeCycleOwner = LocalLifecycleOwner.current - - DisposableEffect(room, accountViewModel) { - NostrChatroomDataSource.loadMessagesBetween(accountViewModel.account, room) - NostrChatroomDataSource.start() - feedViewModel.invalidateData() - - onDispose { NostrChatroomDataSource.stop() } - } - - DisposableEffect(lifeCycleOwner) { - val observer = - LifecycleEventObserver { _, event -> - if (event == Lifecycle.Event.ON_RESUME) { - println("Private Message Start") - NostrChatroomDataSource.start() - feedViewModel.invalidateData() - } - if (event == Lifecycle.Event.ON_PAUSE) { - println("Private Message Stop") - NostrChatroomDataSource.stop() - } - } - - lifeCycleOwner.lifecycle.addObserver(observer) - onDispose { lifeCycleOwner.lifecycle.removeObserver(observer) } - } - - Column(Modifier.fillMaxHeight()) { - ObserveRelayListForDMsAndDisplayIfNotFound(accountViewModel, nav) - - Column( - modifier = - Modifier - .fillMaxHeight() - .padding(vertical = 0.dp) - .weight(1f, true), - ) { - RefreshingChatroomFeedView( - viewModel = feedViewModel, - accountViewModel = accountViewModel, - nav = nav, - routeForLastRead = "Room/${room.hashCode()}", - avoidDraft = newPostModel.draftTag, - onWantsToReply = newPostModel::reply, - onWantsToEditDraft = newPostModel::editFromDraft, - ) - } - - Spacer(modifier = Modifier.height(10.dp)) - - newPostModel.replyTo.value?.let { DisplayReplyingToNote(it, accountViewModel, nav) { newPostModel.replyTo.value = null } } - - val scope = rememberCoroutineScope() - - LaunchedEffect(key1 = newPostModel.draftTag) { - launch(Dispatchers.IO) { - newPostModel.draftTextChanges - .receiveAsFlow() - .debounce(1000) - .collectLatest { - newPostModel.sendDraft() - } - } - } - - fileUpload.multiOrchestrator?.let { - ChatFileUploadView( - fileUpload, - onClose = fileUpload::cancelModel, - accountViewModel, - nav, - ) - } - - // LAST ROW - PrivateMessageEditFieldRow( - newPostModel, - accountViewModel, - onSendNewMessage = { - scope.launch(Dispatchers.IO) { - newPostModel.sendPostSync() - feedViewModel.sendToTop() - } - }, - onSendNewMedia = { list, isNip17 -> - fileUpload.load(list, room, isNip17, accountViewModel.account) - }, - ) - } -} - -@Composable -fun PrivateMessageEditFieldRow( - channelScreenModel: ChatNewMessageViewModel, - accountViewModel: AccountViewModel, - onSendNewMessage: () -> Unit, - onSendNewMedia: (ImmutableList, isNip17: Boolean) -> Unit, -) { - 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, - ) - }, - trailingIcon = { - ThinSendButton( - isActive = - channelScreenModel.message.text.isNotBlank() && !channelScreenModel.isUploadingImage, - modifier = EditFieldTrailingIconModifier, - ) { - onSendNewMessage() - } - }, - leadingIcon = { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(horizontal = 6.dp), - ) { - SelectFromGallery( - isUploading = channelScreenModel.isUploadingImage, - tint = MaterialTheme.colorScheme.placeholderText, - modifier = Modifier.size(30.dp).padding(start = 2.dp), - onImageChosen = { - onSendNewMedia(it, channelScreenModel.nip17) - }, - ) - - var wantsToActivateNIP17 by remember { mutableStateOf(false) } - - if (wantsToActivateNIP17) { - NewFeatureNIP17AlertDialog( - accountViewModel = accountViewModel, - onConfirm = { channelScreenModel.toggleNIP04And24() }, - onDismiss = { wantsToActivateNIP17 = false }, - ) - } - - IconButton( - modifier = Size30Modifier, - onClick = { - if ( - !accountViewModel.account.settings.hideNIP17WarningDialog && - !channelScreenModel.nip17 && - !channelScreenModel.requiresNIP17 - ) { - wantsToActivateNIP17 = true - } else { - channelScreenModel.toggleNIP04And24() - } - }, - ) { - if (channelScreenModel.nip17) { - IncognitoIconOn( - modifier = - Modifier - .padding(top = 2.dp) - .size(18.dp), - tint = MaterialTheme.colorScheme.primary, - ) - } else { - IncognitoIconOff( - modifier = - Modifier - .padding(top = 2.dp) - .size(18.dp), - tint = MaterialTheme.colorScheme.placeholderText, - ) - } - } - } - }, - colors = - TextFieldDefaults.colors( - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - ), - visualTransformation = UrlUserTagTransformation(MaterialTheme.colorScheme.primary), - ) - } -} - -@Composable -fun NewFeatureNIP17AlertDialog( - accountViewModel: AccountViewModel, - onConfirm: () -> Unit, - onDismiss: () -> Unit, -) { - val scope = rememberCoroutineScope() - - QuickActionAlertDialog( - title = stringRes(R.string.new_feature_nip17_might_not_be_available_title), - textContent = stringRes(R.string.new_feature_nip17_might_not_be_available_description), - buttonIconResource = R.drawable.incognito, - buttonText = stringRes(R.string.new_feature_nip17_activate), - onClickDoOnce = { - scope.launch { onConfirm() } - onDismiss() - }, - onClickDontShowAgain = { - scope.launch { - onConfirm() - accountViewModel.account.settings.setHideNIP17WarningDialog() - } - onDismiss() - }, - onDismiss = onDismiss, - ) -} - -@Composable -fun ThinSendButton( - isActive: Boolean, - modifier: Modifier, - onClick: () -> Unit, -) { - IconButton( - enabled = isActive, - modifier = modifier, - onClick = onClick, - ) { - Icon( - imageVector = Icons.Default.Send, - contentDescription = stringRes(id = R.string.accessibility_send), - modifier = Size20Modifier, - ) - } -} - -@Composable -fun ChatroomHeader( - room: ChatroomKey, - modifier: Modifier = StdPadding, - accountViewModel: AccountViewModel, - onClick: () -> Unit, -) { - if (room.users.size == 1) { - LoadUser(baseUserHex = room.users.first(), accountViewModel) { baseUser -> - if (baseUser != null) { - ChatroomHeader( - baseUser = baseUser, - modifier = modifier, - accountViewModel = accountViewModel, - onClick = onClick, - ) - } - } - } else { - GroupChatroomHeader( - room = room, - modifier = modifier, - accountViewModel = accountViewModel, - onClick = onClick, - ) - } -} - -@Composable -fun ChatroomHeader( - baseUser: User, - modifier: Modifier = StdPadding, - accountViewModel: AccountViewModel, - onClick: () -> Unit, -) { - Column( - modifier = - Modifier - .fillMaxWidth() - .clickable( - onClick = onClick, - ), - ) { - Column( - verticalArrangement = Arrangement.Center, - modifier = modifier, - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - ClickableUserPicture( - baseUser = baseUser, - accountViewModel = accountViewModel, - size = Size34dp, - ) - - Column(modifier = Modifier.padding(start = 10.dp)) { - UsernameDisplay(baseUser, accountViewModel = accountViewModel) - } - } - } - } -} - -@Composable -fun GroupChatroomHeader( - room: ChatroomKey, - modifier: Modifier = StdPadding, - accountViewModel: AccountViewModel, - onClick: () -> Unit, -) { - Column( - modifier = - Modifier - .fillMaxWidth() - .clickable(onClick = onClick), - ) { - Column( - verticalArrangement = Arrangement.Center, - modifier = modifier, - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - NonClickableUserPictures( - room = room, - accountViewModel = accountViewModel, - size = Size34dp, - ) - - RoomNameOnlyDisplay(room, Modifier.padding(start = 10.dp), FontWeight.Bold, accountViewModel) - } - } - } -} - -@Composable -private fun EditRoomSubjectButton( - room: ChatroomKey, - accountViewModel: AccountViewModel, -) { - var wantsToPost by remember { mutableStateOf(false) } - - if (wantsToPost) { - NewSubjectView({ wantsToPost = false }, accountViewModel, room) - } - - 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 NewSubjectView( - onClose: () -> Unit, - accountViewModel: AccountViewModel, - room: ChatroomKey, -) { - Dialog( - onDismissRequest = { onClose() }, - properties = - DialogProperties( - dismissOnClickOutside = false, - ), - ) { - Surface { - val groupName = - remember { - mutableStateOf(accountViewModel.userProfile().privateChatrooms[room]?.subject ?: "") - } - val message = remember { mutableStateOf("") } - val scope = rememberCoroutineScope() - - Column( - modifier = - Modifier - .padding(10.dp) - .verticalScroll(rememberScrollState()), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - CloseButton(onPress = { onClose() }) - - PostButton( - onPost = { - scope.launch(Dispatchers.IO) { - val template = - ChatMessageEvent.build( - message.value, - room.users.map { LocalCache.getOrCreateUser(it).toPTag() }, - ) { - groupName.value.ifBlank { null }?.let { changeSubject(it) } - } - - accountViewModel.account.sendNIP17PrivateMessage(template) - } - - onClose() - }, - true, - ) - } - - Spacer(modifier = Modifier.height(15.dp)) - - OutlinedTextField( - label = { Text(text = stringRes(R.string.messages_new_message_subject)) }, - modifier = Modifier.fillMaxWidth(), - value = groupName.value, - onValueChange = { groupName.value = it }, - placeholder = { - Text( - text = stringRes(R.string.messages_new_message_subject_caption), - 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.messages_new_subject_message)) }, - modifier = - Modifier - .fillMaxWidth() - .height(100.dp), - value = message.value, - onValueChange = { message.value = it }, - placeholder = { - Text( - text = stringRes(R.string.messages_new_subject_message_placeholder), - color = MaterialTheme.colorScheme.placeholderText, - ) - }, - keyboardOptions = - KeyboardOptions.Default.copy( - capitalization = KeyboardCapitalization.Sentences, - ), - textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content), - maxLines = 10, - ) - } - } - } -} - -@Composable -fun LongRoomHeader( - room: ChatroomKey, - lineModifier: Modifier = StdPadding, - accountViewModel: AccountViewModel, - nav: INav, -) { - val list = remember(room) { room.users.toPersistentList() } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringRes(id = R.string.messages_group_descriptor), - fontWeight = FontWeight.Bold, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f), - textAlign = TextAlign.Center, - ) - - EditRoomSubjectButton(room, accountViewModel) - } - - LazyColumn( - modifier = Modifier, - state = rememberLazyListState(), - ) { - itemsIndexed(list, key = { _, item -> item }) { _, item -> - LoadUser(baseUserHex = item, accountViewModel) { - if (it != null) { - UserCompose( - baseUser = it, - overallModifier = lineModifier, - accountViewModel = accountViewModel, - nav = nav, - ) - HorizontalDivider( - thickness = DividerThickness, - ) - } - } - } - } -} - -@Composable -fun RoomNameOnlyDisplay( - room: ChatroomKey, - modifier: Modifier, - fontWeight: FontWeight = FontWeight.Bold, - accountViewModel: AccountViewModel, -) { - val roomSubject by - accountViewModel - .userProfile() - .live() - .messages - .map { it.user.privateChatrooms[room]?.subject } - .distinctUntilChanged() - .observeAsState(accountViewModel.userProfile().privateChatrooms[room]?.subject) - - CrossfadeIfEnabled(targetState = roomSubject, modifier, accountViewModel = accountViewModel) { - if (!it.isNullOrBlank()) { - DisplayRoomSubject(it, fontWeight) - } else { - DisplayUserSetAsSubject(room, accountViewModel, FontWeight.Normal) - } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomByAuthorScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomByAuthorScreen.kt new file mode 100644 index 000000000..c189b9d51 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomByAuthorScreen.kt @@ -0,0 +1,110 @@ +/** + * 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.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import com.vitorpamplona.amethyst.ui.navigation.INav +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.header.RenderRoomTopBar +import com.vitorpamplona.amethyst.ui.theme.BottomTopHeight +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey +import kotlinx.collections.immutable.persistentSetOf + +@Composable +fun ChatroomByAuthorScreen( + authorPubKeyHex: String?, + draftMessage: String? = null, + accountViewModel: AccountViewModel, + nav: INav, +) { + if (authorPubKeyHex == null) return + + DisappearingScaffold( + isInvertedLayout = true, + topBar = { + RoomByAuthorTopBar(authorPubKeyHex, accountViewModel, nav) + }, + accountViewModel = accountViewModel, + ) { + Column(Modifier.padding(it)) { + ChatroomByAuthor(authorPubKeyHex, draftMessage, accountViewModel, nav) + } + } +} + +@Composable +fun LoadRoomByAuthor( + authorPubKeyHex: String, + accountViewModel: AccountViewModel, + content: @Composable (ChatroomKey?) -> Unit, +) { + val room by remember(authorPubKeyHex) { + mutableStateOf(ChatroomKey(persistentSetOf(authorPubKeyHex))) + } + + content(room) +} + +@Composable +private fun RoomByAuthorTopBar( + authorPubKeyHex: String, + accountViewModel: AccountViewModel, + nav: INav, +) { + LoadRoomByAuthor(authorPubKeyHex = authorPubKeyHex, accountViewModel) { room -> + if (room != null) { + RenderRoomTopBar(room, accountViewModel, nav) + } else { + Spacer(BottomTopHeight) + } + } +} + +@Composable +fun ChatroomByAuthor( + authorPubKeyHex: String?, + draftMessage: String? = null, + accountViewModel: AccountViewModel, + nav: INav, +) { + if (authorPubKeyHex == null) return + + LoadRoomByAuthor(authorPubKeyHex, accountViewModel) { + it?.let { + ChatroomView( + room = it, + draftMessage = draftMessage, + replyToNote = null, + editFromDraft = null, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomScreen.kt new file mode 100644 index 000000000..7b6e3173f --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomScreen.kt @@ -0,0 +1,130 @@ +/** + * 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.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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 com.vitorpamplona.amethyst.ui.navigation.INav +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.header.RenderRoomTopBar +import com.vitorpamplona.amethyst.ui.theme.BottomTopHeight +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +@Composable +fun ChatroomScreen( + roomId: String?, + draftMessage: String? = null, + replyToNote: HexKey? = null, + editFromDraft: HexKey? = null, + accountViewModel: AccountViewModel, + nav: INav, +) { + if (roomId == null) return + + DisappearingScaffold( + isInvertedLayout = true, + topBar = { + RoomTopBar(roomId, accountViewModel, nav) + }, + accountViewModel = accountViewModel, + ) { + Column(Modifier.padding(it)) { + Chatroom(roomId, draftMessage, replyToNote, editFromDraft, accountViewModel, nav) + } + } +} + +@Composable +private fun RoomTopBar( + id: String, + accountViewModel: AccountViewModel, + nav: INav, +) { + LoadRoom(roomId = id, accountViewModel) { room -> + if (room != null) { + RenderRoomTopBar(room, accountViewModel, nav) + } else { + Spacer(BottomTopHeight) + } + } +} + +@Composable +fun Chatroom( + roomId: String?, + draftMessage: String? = null, + replyToNote: HexKey? = null, + editFromDraft: HexKey? = null, + accountViewModel: AccountViewModel, + nav: INav, +) { + if (roomId == null) return + + LoadRoom(roomId, accountViewModel) { + it?.let { + ChatroomView( + room = it, + draftMessage = draftMessage, + replyToNote = replyToNote, + editFromDraft = editFromDraft, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } +} + +@Composable +fun LoadRoom( + roomId: String, + accountViewModel: AccountViewModel, + content: @Composable (ChatroomKey?) -> Unit, +) { + var room by remember(roomId) { mutableStateOf(null) } + + if (room == null) { + LaunchedEffect(key1 = roomId) { + launch(Dispatchers.IO) { + val newRoom = + accountViewModel.userProfile().privateChatrooms.keys.firstOrNull { + it.hashCode().toString() == roomId + } + if (room != newRoom) { + room = newRoom + } + } + } + } + + content(room) +} 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 new file mode 100644 index 000000000..3a38db622 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.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.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 +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.service.NostrChatroomDataSource +import com.vitorpamplona.amethyst.ui.navigation.INav +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.privateDM.send.ChatNewMessageViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.PrivateMessageEditFieldRow +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey +import kotlinx.coroutines.launch + +@Composable +fun ChatroomView( + room: ChatroomKey, + draftMessage: String?, + replyToNote: HexKey? = null, + editFromDraft: HexKey? = null, + accountViewModel: AccountViewModel, + nav: INav, +) { + val feedViewModel: NostrChatroomFeedViewModel = + viewModel( + key = room.hashCode().toString() + "ChatroomViewModels", + factory = + NostrChatroomFeedViewModel.Factory( + room, + accountViewModel.account, + ), + ) + + val newPostModel: ChatNewMessageViewModel = viewModel() + newPostModel.init(accountViewModel) + newPostModel.load(room) + + if (replyToNote != null) { + LaunchedEffect(key1 = replyToNote) { + accountViewModel.checkGetOrCreateNote(replyToNote) { + if (it != null) { + newPostModel.reply(it) + } + } + } + } + if (editFromDraft != null) { + LaunchedEffect(key1 = replyToNote) { + accountViewModel.checkGetOrCreateNote(editFromDraft) { + if (it != null) { + newPostModel.editFromDraft(it) + } + } + } + } + + if (room.users.size == 1) { + // Activates NIP-17 if the user has DM relays + ObserveRelayListForDMs(pubkey = room.users.first(), accountViewModel = accountViewModel) { + if (it?.relays().isNullOrEmpty()) { + newPostModel.nip17 = false + } else { + newPostModel.nip17 = true + } + } + } + + if (draftMessage != null) { + LaunchedEffect(key1 = draftMessage) { + newPostModel.updateMessage(TextFieldValue(draftMessage)) + } + } + + ChatroomViewUI( + room = room, + feedViewModel = feedViewModel, + newPostModel = newPostModel, + accountViewModel = accountViewModel, + nav = nav, + ) +} + +@Composable +fun ChatroomViewUI( + room: ChatroomKey, + feedViewModel: NostrChatroomFeedViewModel, + newPostModel: ChatNewMessageViewModel, + accountViewModel: AccountViewModel, + nav: INav, +) { + NostrChatroomDataSource.loadMessagesBetween(accountViewModel.account, room) + + val lifeCycleOwner = LocalLifecycleOwner.current + + DisposableEffect(room, accountViewModel) { + NostrChatroomDataSource.loadMessagesBetween(accountViewModel.account, room) + NostrChatroomDataSource.start() + feedViewModel.invalidateData() + + onDispose { NostrChatroomDataSource.stop() } + } + + DisposableEffect(lifeCycleOwner) { + val observer = + LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_RESUME) { + println("Private Message Start") + NostrChatroomDataSource.start() + feedViewModel.invalidateData() + } + if (event == Lifecycle.Event.ON_PAUSE) { + println("Private Message Stop") + NostrChatroomDataSource.stop() + } + } + + lifeCycleOwner.lifecycle.addObserver(observer) + onDispose { lifeCycleOwner.lifecycle.removeObserver(observer) } + } + + Column(Modifier.fillMaxHeight()) { + ObserveRelayListForDMsAndDisplayIfNotFound(accountViewModel, nav) + + Column( + modifier = + Modifier + .fillMaxHeight() + .padding(vertical = 0.dp) + .weight(1f, true), + ) { + RefreshingChatroomFeedView( + viewModel = feedViewModel, + accountViewModel = accountViewModel, + nav = nav, + routeForLastRead = "Room/${room.hashCode()}", + avoidDraft = newPostModel.draftTag, + onWantsToReply = newPostModel::reply, + onWantsToEditDraft = newPostModel::editFromDraft, + ) + } + + Spacer(modifier = Modifier.height(10.dp)) + + val scope = rememberCoroutineScope() + + // LAST ROW + PrivateMessageEditFieldRow( + newPostModel, + accountViewModel, + onSendNewMessage = { + scope.launch { + feedViewModel.sendToTop() + } + }, + nav, + ) + } +} 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/privateDM/feed/ChatDivisor.kt new file mode 100644 index 000000000..fd4d6578b --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/feed/ChatDivisor.kt @@ -0,0 +1,53 @@ +/** + * 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.feed + +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.HorizontalDivider +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.font.FontWeight +import com.vitorpamplona.amethyst.ui.theme.DividerThickness +import com.vitorpamplona.amethyst.ui.theme.Font14SP +import com.vitorpamplona.amethyst.ui.theme.HalfPadding +import com.vitorpamplona.amethyst.ui.theme.StdPadding + +@Composable +fun ChatDivisor(info: String) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = StdPadding) { + HorizontalDivider( + modifier = Modifier.weight(1f), + thickness = DividerThickness, + ) + Text( + text = info, + fontWeight = FontWeight.Bold, + fontSize = Font14SP, + modifier = HalfPadding, + ) + HorizontalDivider( + modifier = Modifier.weight(1f), + thickness = DividerThickness, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/private/ChatroomFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/feed/ChatroomFeedView.kt similarity index 81% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/private/ChatroomFeedView.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/feed/ChatroomFeedView.kt index eff8b3890..806fc8395 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/private/ChatroomFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/feed/ChatroomFeedView.kt @@ -18,23 +18,18 @@ * 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.private +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.feed import androidx.compose.animation.core.tween -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note @@ -49,12 +44,9 @@ 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.DividerThickness import com.vitorpamplona.amethyst.ui.theme.FeedPadding -import com.vitorpamplona.amethyst.ui.theme.Font14SP -import com.vitorpamplona.amethyst.ui.theme.HalfPadding -import com.vitorpamplona.amethyst.ui.theme.StdPadding import com.vitorpamplona.quartz.nip14Subject.subject import com.vitorpamplona.quartz.nip37Drafts.DraftEvent @@ -101,13 +93,10 @@ fun RenderChatroomFeedView( CrossfadeIfEnabled(targetState = feedState, animationSpec = tween(durationMillis = 100), accountViewModel = accountViewModel) { state -> when (state) { - is FeedState.Empty -> { - FeedEmpty { viewModel.invalidateData() } - } - is FeedState.FeedError -> { - FeedError(state.errorMessage) { viewModel.invalidateData() } - } - is FeedState.Loaded -> { + is FeedState.Loading -> LoadingFeed() + is FeedState.Empty -> FeedEmpty { viewModel.invalidateData() } + is FeedState.FeedError -> FeedError(state.errorMessage) { viewModel.invalidateData() } + is FeedState.Loaded -> ChatroomFeedLoaded( state, accountViewModel, @@ -118,10 +107,6 @@ fun RenderChatroomFeedView( onWantsToEditDraft, avoidDraft, ) - } - is FeedState.Loading -> { - LoadingFeed() - } } } } @@ -163,14 +148,14 @@ fun ChatroomFeedLoaded( onWantsToEditDraft = onWantsToEditDraft, ) - NewDateSubject(items.list.getOrNull(index + 1), item) + NewDateOrSubjectDivisor(items.list.getOrNull(index + 1), item) } } } } @Composable -fun NewDateSubject( +fun NewDateOrSubjectDivisor( previous: Note?, note: Note, ) { @@ -196,23 +181,3 @@ fun NewDateSubject( } } } - -@Composable -fun ChatDivisor(info: String) { - Row(verticalAlignment = Alignment.CenterVertically, modifier = StdPadding) { - HorizontalDivider( - modifier = Modifier.weight(1f), - thickness = DividerThickness, - ) - Text( - text = info, - fontWeight = FontWeight.Bold, - fontSize = Font14SP, - modifier = HalfPadding, - ) - HorizontalDivider( - modifier = Modifier.weight(1f), - thickness = DividerThickness, - ) - } -} 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 new file mode 100644 index 000000000..10ea0292f --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/ChatroomHeader.kt @@ -0,0 +1,130 @@ +/** + * 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.header + +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.model.User +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.theme.Size34dp +import com.vitorpamplona.amethyst.ui.theme.StdPadding +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey + +@Composable +fun ChatroomHeader( + room: ChatroomKey, + modifier: Modifier = StdPadding, + accountViewModel: AccountViewModel, + onClick: () -> Unit, +) { + if (room.users.size == 1) { + LoadUser(baseUserHex = room.users.first(), accountViewModel) { baseUser -> + if (baseUser != null) { + UserChatroomHeader( + baseUser = baseUser, + modifier = modifier, + accountViewModel = accountViewModel, + onClick = onClick, + ) + } + } + } else { + GroupChatroomHeader( + room = room, + modifier = modifier, + accountViewModel = accountViewModel, + onClick = onClick, + ) + } +} + +@Composable +fun UserChatroomHeader( + baseUser: User, + modifier: Modifier = StdPadding, + accountViewModel: AccountViewModel, + onClick: () -> Unit, +) { + Column( + Modifier + .fillMaxWidth() + .clickable( + onClick = onClick, + ), + ) { + Column(modifier, Arrangement.Center) { + Row(verticalAlignment = Alignment.CenterVertically) { + ClickableUserPicture( + baseUser = baseUser, + accountViewModel = accountViewModel, + size = Size34dp, + ) + + Column(modifier = Modifier.padding(start = 10.dp)) { + UsernameDisplay(baseUser, accountViewModel = accountViewModel) + } + } + } + } +} + +@Composable +fun GroupChatroomHeader( + room: ChatroomKey, + modifier: Modifier = StdPadding, + accountViewModel: AccountViewModel, + onClick: () -> Unit, +) { + Column( + modifier = + Modifier + .fillMaxWidth() + .clickable(onClick = onClick), + ) { + Column( + verticalArrangement = Arrangement.Center, + modifier = modifier, + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + NonClickableUserPictures( + room = room, + accountViewModel = accountViewModel, + size = Size34dp, + ) + + RoomNameOnlyDisplay(room, Modifier.padding(start = 10.dp), FontWeight.Bold, accountViewModel) + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/NewChatroomSubjectDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/NewChatroomSubjectDialog.kt new file mode 100644 index 000000000..70b2d3e2d --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/NewChatroomSubjectDialog.kt @@ -0,0 +1,162 @@ +/** + * 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.header + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.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.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +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 com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.LocalCache +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 +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey +import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent +import com.vitorpamplona.quartz.nip17Dm.messages.changeSubject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +@Composable +fun NewChatroomSubjectDialog( + onClose: () -> Unit, + accountViewModel: AccountViewModel, + room: ChatroomKey, +) { + Dialog( + onDismissRequest = { onClose() }, + properties = + DialogProperties( + dismissOnClickOutside = false, + ), + ) { + Surface { + val groupName = + remember { + mutableStateOf(accountViewModel.userProfile().privateChatrooms[room]?.subject ?: "") + } + val message = remember { mutableStateOf("") } + val scope = rememberCoroutineScope() + + Column( + modifier = + Modifier + .padding(10.dp) + .verticalScroll(rememberScrollState()), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + CloseButton(onPress = { onClose() }) + + PostButton( + onPost = { + scope.launch(Dispatchers.IO) { + val template = + ChatMessageEvent.build( + message.value, + room.users.map { LocalCache.getOrCreateUser(it).toPTag() }, + ) { + groupName.value.ifBlank { null }?.let { changeSubject(it) } + } + + accountViewModel.account.sendNIP17PrivateMessage(template) + } + + onClose() + }, + true, + ) + } + + Spacer(modifier = Modifier.height(15.dp)) + + OutlinedTextField( + label = { Text(text = stringRes(R.string.messages_new_message_subject)) }, + modifier = Modifier.fillMaxWidth(), + value = groupName.value, + onValueChange = { groupName.value = it }, + placeholder = { + Text( + text = stringRes(R.string.messages_new_message_subject_caption), + 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.messages_new_subject_message)) }, + modifier = + Modifier + .fillMaxWidth() + .height(100.dp), + value = message.value, + onValueChange = { message.value = it }, + placeholder = { + Text( + text = stringRes(R.string.messages_new_subject_message_placeholder), + 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/screen/loggedIn/chats/privateDM/header/RenderRoomTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RenderRoomTopBar.kt new file mode 100644 index 000000000..27b2a9b59 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RenderRoomTopBar.kt @@ -0,0 +1,198 @@ +/** + * 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.header + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.EditNote +import androidx.compose.material3.Button +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +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.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.TopBarExtensibleWithBackButton +import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture +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.stringRes +import com.vitorpamplona.amethyst.ui.theme.DividerThickness +import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer +import com.vitorpamplona.amethyst.ui.theme.Size34dp +import com.vitorpamplona.amethyst.ui.theme.StdPadding +import com.vitorpamplona.amethyst.ui.theme.ZeroPadding +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey +import kotlinx.collections.immutable.toPersistentList + +@Composable +fun RenderRoomTopBar( + room: ChatroomKey, + accountViewModel: AccountViewModel, + nav: INav, +) { + if (room.users.size == 1) { + TopBarExtensibleWithBackButton( + title = { + LoadUser(baseUserHex = room.users.first(), accountViewModel) { baseUser -> + if (baseUser != null) { + ClickableUserPicture( + baseUser = baseUser, + accountViewModel = accountViewModel, + size = Size34dp, + ) + + Spacer(modifier = DoubleHorzSpacer) + + UsernameDisplay(baseUser, Modifier.weight(1f), fontWeight = FontWeight.Normal, accountViewModel = accountViewModel) + } + } + }, + extendableRow = { + LoadUser(baseUserHex = room.users.first(), accountViewModel) { + if (it != null) { + UserCompose( + baseUser = it, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } + }, + popBack = nav::popBack, + ) + } else { + TopBarExtensibleWithBackButton( + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + NonClickableUserPictures( + room = room, + accountViewModel = accountViewModel, + size = Size34dp, + ) + + RoomNameOnlyDisplay(room, Modifier.padding(start = 10.dp).weight(1f), FontWeight.Normal, accountViewModel) + } + }, + extendableRow = { + GroupMembersHeader(room = room, accountViewModel = accountViewModel, nav = nav) + }, + popBack = nav::popBack, + ) + } +} + +@Composable +fun GroupMembersHeader( + room: ChatroomKey, + lineModifier: Modifier = StdPadding, + accountViewModel: AccountViewModel, + nav: INav, +) { + val list = remember(room) { room.users.toPersistentList() } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringRes(id = R.string.messages_group_descriptor), + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + textAlign = TextAlign.Center, + ) + + EditRoomSubjectButton(room, accountViewModel) + } + + LazyColumn( + modifier = Modifier, + state = rememberLazyListState(), + ) { + itemsIndexed(list, key = { _, item -> item }) { _, item -> + LoadUser(baseUserHex = item, accountViewModel) { + if (it != null) { + UserCompose( + baseUser = it, + overallModifier = lineModifier, + accountViewModel = accountViewModel, + nav = nav, + ) + HorizontalDivider( + thickness = DividerThickness, + ) + } + } + } + } +} + +@Composable +private fun EditRoomSubjectButton( + room: ChatroomKey, + accountViewModel: AccountViewModel, +) { + var wantsToPost by remember { mutableStateOf(false) } + + if (wantsToPost) { + NewChatroomSubjectDialog({ wantsToPost = false }, accountViewModel, room) + } + + 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/privateDM/header/RoomNameOnlyDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RoomNameOnlyDisplay.kt new file mode 100644 index 000000000..668f6c48a --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RoomNameOnlyDisplay.kt @@ -0,0 +1,192 @@ +/** + * 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.header + +import androidx.compose.foundation.layout.Row +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.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.lifecycle.distinctUntilChanged +import androidx.lifecycle.map +import com.vitorpamplona.amethyst.model.User +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.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey +import kotlin.math.min + +@Composable +fun RoomNameOnlyDisplay( + room: ChatroomKey, + modifier: Modifier, + fontWeight: FontWeight = FontWeight.Bold, + accountViewModel: AccountViewModel, +) { + val roomSubject by + accountViewModel + .userProfile() + .live() + .messages + .map { it.user.privateChatrooms[room]?.subject } + .distinctUntilChanged() + .observeAsState(accountViewModel.userProfile().privateChatrooms[room]?.subject) + + CrossfadeIfEnabled(targetState = roomSubject, modifier, accountViewModel = accountViewModel) { + if (!it.isNullOrBlank()) { + DisplayRoomSubject(it, fontWeight) + } else { + DisplayUserSetAsSubject(room, accountViewModel, FontWeight.Normal) + } + } +} + +@Composable +fun DisplayUserSetAsSubject( + room: ChatroomKey, + accountViewModel: AccountViewModel, + fontWeight: FontWeight = FontWeight.Bold, +) { + val userList = remember(room) { room.users.toList() } + + if (userList.size == 1) { + // Regular Design + Row { + LoadUser(baseUserHex = userList[0], accountViewModel) { + it?.let { UsernameDisplay(it, Modifier.weight(1f), fontWeight = fontWeight, accountViewModel = accountViewModel) } + } + } + } else { + Row { + userList.take(4).forEachIndexed { index, value -> + LoadUser(baseUserHex = value, accountViewModel) { + it?.let { ShortUsernameDisplay(baseUser = it, fontWeight = fontWeight, accountViewModel = accountViewModel) } + } + + if (min(userList.size, 4) - 1 != index) { + Text( + text = ", ", + fontWeight = fontWeight, + maxLines = 1, + ) + } + } + } + } +} + +@Composable +fun RoomNameDisplay( + room: ChatroomKey, + modifier: Modifier, + accountViewModel: AccountViewModel, +) { + val roomSubject by + accountViewModel + .userProfile() + .live() + .messages + .map { it.user.privateChatrooms[room]?.subject } + .distinctUntilChanged() + .observeAsState(accountViewModel.userProfile().privateChatrooms[room]?.subject) + + CrossfadeIfEnabled(targetState = roomSubject, modifier, label = "RoomNameDisplay", accountViewModel = accountViewModel) { + if (!it.isNullOrBlank()) { + if (room.users.size > 1) { + DisplayRoomSubject(it) + } else { + DisplayUserAndSubject(room.users.first(), it, accountViewModel) + } + } else { + DisplayUserSetAsSubject(room, accountViewModel) + } + } +} + +@Composable +fun DisplayRoomSubject( + roomSubject: String, + fontWeight: FontWeight = FontWeight.Bold, +) { + Row { + Text( + text = roomSubject, + fontWeight = fontWeight, + maxLines = 1, + ) + } +} + +@Composable +private fun DisplayUserAndSubject( + user: HexKey, + subject: String, + accountViewModel: AccountViewModel, +) { + Row { + Text( + text = subject, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = " - ", + fontWeight = FontWeight.Bold, + maxLines = 1, + ) + LoadUser(baseUserHex = user, accountViewModel = accountViewModel) { + it?.let { UsernameDisplay(it, Modifier.weight(1f), accountViewModel = accountViewModel) } + } + } +} + +@Composable +fun ShortUsernameDisplay( + baseUser: User, + weight: Modifier = Modifier, + fontWeight: FontWeight = FontWeight.Bold, + accountViewModel: AccountViewModel, +) { + val userName by + baseUser + .live() + .metadata + .map { it.user.toBestShortFirstName() } + .distinctUntilChanged() + .observeAsState(baseUser.toBestShortFirstName()) + + CrossfadeIfEnabled(targetState = userName, modifier = weight, accountViewModel = accountViewModel) { + CreateTextWithEmoji( + text = it, + tags = baseUser.info?.tags, + fontWeight = fontWeight, + maxLines = 1, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/private/ChatBubbleLayout.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/messages/ChatBubbleLayout.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/private/ChatBubbleLayout.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/messages/ChatBubbleLayout.kt index 1a2a53211..56984c4dd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/private/ChatBubbleLayout.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/messages/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.private +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.messages import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background @@ -60,6 +60,118 @@ import com.vitorpamplona.amethyst.ui.theme.chatDraftBackground import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink import com.vitorpamplona.amethyst.ui.theme.messageBubbleLimits +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun ChatBubbleLayout( + isLoggedInUser: Boolean, + isDraft: Boolean, + innerQuote: Boolean, + isComplete: Boolean, + hasDetailsToShow: Boolean, + drawAuthorInfo: Boolean, + parentBackgroundColor: MutableState? = null, + onClick: () -> Boolean, + onAuthorClick: () -> Unit, + actionMenu: @Composable (onDismiss: () -> Unit) -> Unit, + detailRow: @Composable () -> Unit, + drawAuthorLine: @Composable () -> Unit, + inner: @Composable (MutableState) -> Unit, +) { + val loggedInColors = MaterialTheme.colorScheme.mediumImportanceLink + val otherColors = MaterialTheme.colorScheme.chatBackground + val defaultBackground = MaterialTheme.colorScheme.background + val draftColor = MaterialTheme.colorScheme.chatDraftBackground + + val backgroundBubbleColor = + remember { + if (isLoggedInUser) { + if (isDraft) { + mutableStateOf( + draftColor.compositeOver(parentBackgroundColor?.value ?: defaultBackground), + ) + } else { + mutableStateOf( + loggedInColors.compositeOver(parentBackgroundColor?.value ?: defaultBackground), + ) + } + } else { + mutableStateOf(otherColors.compositeOver(parentBackgroundColor?.value ?: defaultBackground)) + } + } + + Row( + modifier = if (innerQuote) ChatPaddingInnerQuoteModifier else ChatPaddingModifier, + horizontalArrangement = if (isLoggedInUser) Arrangement.End else Arrangement.Start, + ) { + val popupExpanded = remember { mutableStateOf(false) } + + val showDetails = + remember { + mutableStateOf( + if (isComplete) { + true + } else { + hasDetailsToShow + }, + ) + } + + val clickableModifier = + remember { + Modifier.combinedClickable( + onClick = { + if (!onClick()) { + if (!isComplete) { + showDetails.value = !showDetails.value + } + } + }, + onLongClick = { popupExpanded.value = true }, + ) + } + + Row( + horizontalArrangement = if (isLoggedInUser) Arrangement.End else Arrangement.Start, + modifier = if (innerQuote) Modifier else ChatBubbleMaxSizeModifier, + ) { + Surface( + color = backgroundBubbleColor.value, + shape = if (isLoggedInUser) ChatBubbleShapeMe else ChatBubbleShapeThem, + modifier = clickableModifier, + ) { + Column(modifier = messageBubbleLimits, verticalArrangement = RowColSpacing5dp) { + if (drawAuthorInfo) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = if (isLoggedInUser) Arrangement.End else Arrangement.Start, + modifier = HalfHalfVertPadding.clickable(onClick = onAuthorClick), + ) { + drawAuthorLine() + } + } + + inner(backgroundBubbleColor) + + if (showDetails.value) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = ReactionRowHeightChat, + ) { + detailRow() + } + } + } + } + } + + if (popupExpanded.value) { + actionMenu { + popupExpanded.value = false + } + } + } +} + @Preview @Composable private fun BubblePreview() { @@ -207,115 +319,3 @@ private fun BubblePreview() { } } } - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun ChatBubbleLayout( - isLoggedInUser: Boolean, - isDraft: Boolean, - innerQuote: Boolean, - isComplete: Boolean, - hasDetailsToShow: Boolean, - drawAuthorInfo: Boolean, - parentBackgroundColor: MutableState? = null, - onClick: () -> Boolean, - onAuthorClick: () -> Unit, - actionMenu: @Composable (onDismiss: () -> Unit) -> Unit, - detailRow: @Composable () -> Unit, - drawAuthorLine: @Composable () -> Unit, - inner: @Composable (MutableState) -> Unit, -) { - val loggedInColors = MaterialTheme.colorScheme.mediumImportanceLink - val otherColors = MaterialTheme.colorScheme.chatBackground - val defaultBackground = MaterialTheme.colorScheme.background - val draftColor = MaterialTheme.colorScheme.chatDraftBackground - - val backgroundBubbleColor = - remember { - if (isLoggedInUser) { - if (isDraft) { - mutableStateOf( - draftColor.compositeOver(parentBackgroundColor?.value ?: defaultBackground), - ) - } else { - mutableStateOf( - loggedInColors.compositeOver(parentBackgroundColor?.value ?: defaultBackground), - ) - } - } else { - mutableStateOf(otherColors.compositeOver(parentBackgroundColor?.value ?: defaultBackground)) - } - } - - Row( - modifier = if (innerQuote) ChatPaddingInnerQuoteModifier else ChatPaddingModifier, - horizontalArrangement = if (isLoggedInUser) Arrangement.End else Arrangement.Start, - ) { - val popupExpanded = remember { mutableStateOf(false) } - - val showDetails = - remember { - mutableStateOf( - if (isComplete) { - true - } else { - hasDetailsToShow - }, - ) - } - - val clickableModifier = - remember { - Modifier.combinedClickable( - onClick = { - if (!onClick()) { - if (!isComplete) { - showDetails.value = !showDetails.value - } - } - }, - onLongClick = { popupExpanded.value = true }, - ) - } - - Row( - horizontalArrangement = if (isLoggedInUser) Arrangement.End else Arrangement.Start, - modifier = if (innerQuote) Modifier else ChatBubbleMaxSizeModifier, - ) { - Surface( - color = backgroundBubbleColor.value, - shape = if (isLoggedInUser) ChatBubbleShapeMe else ChatBubbleShapeThem, - modifier = clickableModifier, - ) { - Column(modifier = messageBubbleLimits, verticalArrangement = RowColSpacing5dp) { - if (drawAuthorInfo) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = if (isLoggedInUser) Arrangement.End else Arrangement.Start, - modifier = HalfHalfVertPadding.clickable(onClick = onAuthorClick), - ) { - drawAuthorLine() - } - } - - inner(backgroundBubbleColor) - - if (showDetails.value) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = ReactionRowHeightChat, - ) { - detailRow() - } - } - } - } - } - - if (popupExpanded.value) { - actionMenu { - popupExpanded.value = false - } - } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/private/ChatroomMessageCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/messages/ChatroomMessageCompose.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/private/ChatroomMessageCompose.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/messages/ChatroomMessageCompose.kt index a829645a4..9ab47b27e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/private/ChatroomMessageCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/messages/ChatroomMessageCompose.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.private +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.messages import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/private/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt similarity index 87% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/private/ChatNewMessageViewModel.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index abaa13e1c..3d00cec2a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/private/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -18,20 +18,17 @@ * 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.private +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send import android.content.Context import android.util.Log import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -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 @@ -41,17 +38,14 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -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.ServerName +import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.components.Split import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.ChatFileUploadState +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.ChatFileUploader import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags import com.vitorpamplona.quartz.nip01Core.tags.references.references @@ -101,6 +95,7 @@ open class ChatNewMessageViewModel : ViewModel() { val replyTo = mutableStateOf(null) + var uploadState by mutableStateOf(null) val iMetaAttachments = IMetaAttachments() var message by mutableStateOf(TextFieldValue("")) @@ -134,9 +129,6 @@ open class ChatNewMessageViewModel : ViewModel() { var toUsers by mutableStateOf(TextFieldValue("")) var subject by mutableStateOf(TextFieldValue("")) - // Images and Videos - var multiOrchestrator by mutableStateOf(null) - // Invoices var canAddInvoice by mutableStateOf(false) var wantsInvoice by mutableStateOf(false) @@ -168,8 +160,13 @@ open class ChatNewMessageViewModel : ViewModel() { open fun init(accountVM: AccountViewModel) { this.accountViewModel = accountVM this.account = accountVM.account - this.canAddInvoice = accountVM.userProfile().info?.lnAddress() != null - this.canAddZapRaiser = accountVM.userProfile().info?.lnAddress() != null + this.canAddInvoice = hasLnAddress() + this.canAddZapRaiser = hasLnAddress() + + this.uploadState = + ChatFileUploadState( + account?.settings?.defaultFileServer ?: DEFAULT_MEDIA_SERVERS[0], + ) } open fun load(room: ChatroomKey) { @@ -285,11 +282,10 @@ open class ChatNewMessageViewModel : ViewModel() { urlPreview = findUrlInMessage() } - fun sendPost() { + fun sendPost(onDone: () -> Unit) { viewModelScope.launch(Dispatchers.IO) { - innerSendPost(null) - accountViewModel?.deleteDraft(draftTag) - cancel() + sendPostSync() + onDone() } } @@ -313,6 +309,26 @@ open class ChatNewMessageViewModel : ViewModel() { } } + fun pickedMedia(list: ImmutableList) { + uploadState?.load(list) + } + + fun upload( + onError: (title: String, message: String) -> Unit, + context: Context, + onceUploaded: () -> Unit, + ) { + val room = room ?: return + val account = account ?: return + val uploadState = uploadState ?: return + + if (nip17) { + ChatFileUploader(room, account).uploadNIP17(uploadState, viewModelScope, onError, context, onceUploaded) + } else { + ChatFileUploader(room, account).uploadNIP04(uploadState, viewModelScope, onError, context, onceUploaded) + } + } + private fun innerSendPost(dTag: String?) { val room = room ?: return val accountViewModel = accountViewModel ?: return @@ -370,54 +386,6 @@ open class ChatNewMessageViewModel : ViewModel() { } } - fun upload( - alt: String?, - contentWarningReason: String?, - mediaQuality: Int, - isPrivate: Boolean = false, - server: ServerName, - onError: (title: String, message: String) -> Unit, - context: Context, - ) { - viewModelScope.launch(Dispatchers.Default) { - val myAccount = account ?: return@launch - - val myMultiOrchestrator = multiOrchestrator ?: return@launch - - isUploadingImage = true - - val results = - myMultiOrchestrator.upload( - viewModelScope, - alt, - contentWarningReason, - MediaCompressor.intToCompressorQuality(mediaQuality), - server, - myAccount, - context, - ) - - if (results.allGood) { - results.successful.forEach { - if (it.result is UploadOrchestrator.OrchestratorResult.ServerResult) { - iMetaAttachments.add(it.result, alt, contentWarningReason) - - message = message.insertUrlAtCursor(it.result.url) - urlPreview = findUrlInMessage() - } - } - - multiOrchestrator = null - } 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 - } - } - open fun cancel() { message = TextFieldValue("") toUsers = TextFieldValue("") @@ -425,9 +393,7 @@ open class ChatNewMessageViewModel : ViewModel() { replyTo.value = null - multiOrchestrator = null urlPreview = null - isUploadingImage = false wantsInvoice = false wantsZapraiser = false @@ -457,10 +423,6 @@ open class ChatNewMessageViewModel : ViewModel() { } } - fun deleteMediaToUpload(selected: SelectedMediaProcessing) { - this.multiOrchestrator?.remove(selected) - } - open fun findUrlInMessage(): String? = RichTextParser().parseValidUrls(message.text).firstOrNull() private fun saveDraft() { @@ -575,24 +537,18 @@ open class ChatNewMessageViewModel : ViewModel() { saveDraft() } - private fun newStateMapPollOptions(): SnapshotStateMap = mutableStateMapOf(Pair(0, ""), Pair(1, "")) - fun canPost(): Boolean = message.text.isNotBlank() && - !isUploadingImage && + uploadState?.isUploadingImage != true && !wantsInvoice && (!wantsZapraiser || zapRaiserAmount != null) && (toUsers.text.isNotBlank()) && - multiOrchestrator == null + uploadState?.multiOrchestrator == null fun insertAtCursor(newElement: String) { message = message.insertUrlAtCursor(newElement) } - fun selectImage(uris: ImmutableList) { - multiOrchestrator = MultiOrchestrator(uris) - } - override fun onCleared() { super.onCleared() Log.d("Init", "OnCleared: ${this.javaClass.simpleName}") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/private/IMetaAttachments.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/IMetaAttachments.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/private/IMetaAttachments.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/IMetaAttachments.kt index 77d306568..bb4663297 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/private/IMetaAttachments.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/IMetaAttachments.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.private +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send import android.webkit.MimeTypeMap import androidx.compose.runtime.getValue 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 new file mode 100644 index 000000000..6c4e3dba4 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt @@ -0,0 +1,253 @@ +/** + * 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.send + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.IconButton +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.runtime.getValue +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.platform.LocalContext +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.unit.dp +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.INav +import com.vitorpamplona.amethyst.ui.note.IncognitoIconOff +import com.vitorpamplona.amethyst.ui.note.IncognitoIconOn +import com.vitorpamplona.amethyst.ui.note.QuickActionAlertDialog +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.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.stringRes +import com.vitorpamplona.amethyst.ui.theme.EditFieldBorder +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.placeholderText +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch + +@Composable +fun PrivateMessageEditFieldRow( + channelScreenModel: ChatNewMessageViewModel, + accountViewModel: AccountViewModel, + onSendNewMessage: () -> Unit, + nav: INav, +) { + channelScreenModel.replyTo.value?.let { DisplayReplyingToNote(it, accountViewModel, nav) { channelScreenModel.replyTo.value = null } } + + LaunchedEffect(key1 = channelScreenModel.draftTag) { + launch(Dispatchers.IO) { + channelScreenModel.draftTextChanges + .receiveAsFlow() + .debounce(1000) + .collectLatest { + channelScreenModel.sendDraft() + } + } + } + + channelScreenModel.uploadState?.let { uploading -> + uploading.multiOrchestrator?.let { selectedFiles -> + val context = LocalContext.current + + ChatFileUploadDialog( + room = channelScreenModel.room!!, + state = uploading, + upload = { + channelScreenModel.upload( + onError = accountViewModel::toast, + context = context, + onceUploaded = onSendNewMessage, + ) + + if (uploading.selectedServer.type != ServerType.NIP95) { + accountViewModel.account.settings.changeDefaultFileServer(uploading.selectedServer) + } + }, + onCancel = uploading::reset, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } + + 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, + ) + }, + trailingIcon = { + ThinSendButton( + isActive = + channelScreenModel.message.text.isNotBlank() && !channelScreenModel.isUploadingImage, + modifier = EditFieldTrailingIconModifier, + ) { + channelScreenModel.sendPost(onSendNewMessage) + } + }, + leadingIcon = { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(horizontal = 6.dp), + ) { + SelectFromGallery( + isUploading = channelScreenModel.isUploadingImage, + tint = MaterialTheme.colorScheme.placeholderText, + modifier = + Modifier + .size(30.dp) + .padding(start = 2.dp), + onImageChosen = channelScreenModel::pickedMedia, + ) + + var wantsToActivateNIP17 by remember { mutableStateOf(false) } + + if (wantsToActivateNIP17) { + NewFeatureNIP17AlertDialog( + accountViewModel = accountViewModel, + onConfirm = { channelScreenModel.toggleNIP04And24() }, + onDismiss = { wantsToActivateNIP17 = false }, + ) + } + + IconButton( + modifier = Size30Modifier, + onClick = { + if ( + !accountViewModel.account.settings.hideNIP17WarningDialog && + !channelScreenModel.nip17 && + !channelScreenModel.requiresNIP17 + ) { + wantsToActivateNIP17 = true + } else { + channelScreenModel.toggleNIP04And24() + } + }, + ) { + if (channelScreenModel.nip17) { + IncognitoIconOn( + modifier = + Modifier + .padding(top = 2.dp) + .size(18.dp), + tint = MaterialTheme.colorScheme.primary, + ) + } else { + IncognitoIconOff( + modifier = + Modifier + .padding(top = 2.dp) + .size(18.dp), + tint = MaterialTheme.colorScheme.placeholderText, + ) + } + } + } + }, + colors = + TextFieldDefaults.colors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + visualTransformation = UrlUserTagTransformation(MaterialTheme.colorScheme.primary), + ) + } +} + +@Composable +fun NewFeatureNIP17AlertDialog( + accountViewModel: AccountViewModel, + onConfirm: () -> Unit, + onDismiss: () -> Unit, +) { + val scope = rememberCoroutineScope() + + QuickActionAlertDialog( + title = stringRes(R.string.new_feature_nip17_might_not_be_available_title), + textContent = stringRes(R.string.new_feature_nip17_might_not_be_available_description), + buttonIconResource = R.drawable.incognito, + buttonText = stringRes(R.string.new_feature_nip17_activate), + onClickDoOnce = { + scope.launch { onConfirm() } + onDismiss() + }, + onClickDontShowAgain = { + scope.launch { + onConfirm() + accountViewModel.account.settings.setHideNIP17WarningDialog() + } + onDismiss() + }, + onDismiss = onDismiss, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/private/UserSuggestions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/UserSuggestions.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/private/UserSuggestions.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/UserSuggestions.kt index afdfc0942..29d9ab52f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/private/UserSuggestions.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/UserSuggestions.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.private +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/private/ChatFileUploadView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadDialog.kt similarity index 77% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/private/ChatFileUploadView.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadDialog.kt index 58ab37c21..127aed8c7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/private/ChatFileUploadView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadDialog.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.private +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -51,10 +51,8 @@ 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.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardCapitalization -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog @@ -72,28 +70,29 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.SettingSwitchItem import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.RoomNameOnlyDisplay import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size34dp import com.vitorpamplona.amethyst.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import kotlinx.collections.immutable.toImmutableList @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ChatFileUploadView( - postViewModel: ChatFileUploadModel, - onClose: () -> Unit, +fun ChatFileUploadDialog( + room: ChatroomKey, + state: ChatFileUploadState, + upload: () -> Unit, + onCancel: () -> Unit, accountViewModel: AccountViewModel, nav: INav, ) { - val account = accountViewModel.account - val context = LocalContext.current - val scrollState = rememberScrollState() Dialog( - onDismissRequest = { onClose() }, + onDismissRequest = { onCancel() }, properties = DialogProperties( usePlatformDefaultWidth = false, @@ -108,34 +107,22 @@ fun ChatFileUploadView( scrollBehavior = rememberHeightDecreaser(), modifier = Modifier, title = { - val room = postViewModel.chatroom - - if (room == null) { - Text( - text = stringRes(R.string.dm_upload), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.titleLarge, - overflow = TextOverflow.Ellipsis, - maxLines = 1, + Row(verticalAlignment = Alignment.CenterVertically) { + NonClickableUserPictures( + room = room, + accountViewModel = accountViewModel, + size = Size34dp, ) - } else { - Row(verticalAlignment = Alignment.CenterVertically) { - NonClickableUserPictures( - room = room, - accountViewModel = accountViewModel, - size = Size34dp, - ) - RoomNameOnlyDisplay(room, Modifier.padding(start = 10.dp), FontWeight.Normal, accountViewModel) - } + RoomNameOnlyDisplay(room, Modifier.padding(start = 10.dp), FontWeight.Normal, accountViewModel) } }, navigationIcon = { IconButton( modifier = TitleIconModifier, onClick = { - postViewModel.cancelModel() - onClose() + state.reset() + onCancel() }, ) { ArrowBackIcon() @@ -144,20 +131,8 @@ fun ChatFileUploadView( actions = { SendButton( modifier = Modifier.padding(end = 5.dp), - onPost = { - postViewModel.upload( - onError = accountViewModel::toast, - context = context, - onceUploaded = onClose, - ) - - postViewModel.selectedServer?.let { - if (it.type != ServerType.NIP95) { - account.settings.changeDefaultFileServer(it) - } - } - }, - isActive = postViewModel.canPost(), + onPost = upload, + isActive = state.canPost(), ) }, colors = @@ -176,7 +151,7 @@ fun ChatFileUploadView( ) { Column(Modifier.fillMaxSize().padding(start = 10.dp, end = 10.dp, bottom = 10.dp)) { Column(Modifier.fillMaxWidth().verticalScroll(scrollState)) { - ImageVideoPostChat(postViewModel, accountViewModel) + ImageVideoPostChat(state, accountViewModel) } } } @@ -186,7 +161,7 @@ fun ChatFileUploadView( @Composable private fun ImageVideoPostChat( - postViewModel: ChatFileUploadModel, + fileUploadState: ChatFileUploadState, accountViewModel: AccountViewModel, ) { val fileServers by accountViewModel.account.liveServerList.collectAsState() @@ -203,10 +178,10 @@ private fun ImageVideoPostChat( }.toImmutableList() } - postViewModel.multiOrchestrator?.let { + fileUploadState.multiOrchestrator?.let { ShowImageUploadGallery( it, - postViewModel::deleteMediaToUpload, + fileUploadState::deleteMediaToUpload, accountViewModel, ) } @@ -215,8 +190,8 @@ private fun ImageVideoPostChat( label = { Text(text = stringRes(R.string.content_description)) }, modifier = Modifier.fillMaxWidth().padding(top = 3.dp).height(150.dp), maxLines = 10, - value = postViewModel.caption, - onValueChange = { postViewModel.caption = it }, + value = fileUploadState.caption, + onValueChange = { fileUploadState.caption = it }, placeholder = { Text( text = stringRes(R.string.content_description_example), @@ -233,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 = postViewModel.sensitiveContent, - onCheckedChange = { postViewModel.sensitiveContent = it }, + checked = fileUploadState.sensitiveContent, + onCheckedChange = { fileUploadState.sensitiveContent = it }, ) SettingsRow(R.string.file_server, R.string.file_server_description) { @@ -246,7 +221,7 @@ private fun ImageVideoPostChat( ?.name ?: fileServers[0].name, options = fileServerOptions, - onSelect = { postViewModel.selectedServer = fileServers[it] }, + onSelect = { fileUploadState.selectedServer = fileServers[it] }, ) } @@ -272,7 +247,7 @@ private fun ImageVideoPostChat( Box(modifier = Modifier.fillMaxWidth()) { Text( text = - when (postViewModel.mediaQualitySlider) { + when (fileUploadState.mediaQualitySlider) { 0 -> stringRes(R.string.media_compression_quality_low) 1 -> stringRes(R.string.media_compression_quality_medium) 2 -> stringRes(R.string.media_compression_quality_high) @@ -284,8 +259,8 @@ private fun ImageVideoPostChat( } Slider( - value = postViewModel.mediaQualitySlider.toFloat(), - onValueChange = { postViewModel.mediaQualitySlider = it.toInt() }, + value = fileUploadState.mediaQualitySlider.toFloat(), + onValueChange = { fileUploadState.mediaQualitySlider = it.toInt() }, valueRange = 0f..3f, steps = 2, ) 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 new file mode 100644 index 000000000..9bc20bb7d --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadState.kt @@ -0,0 +1,75 @@ +/** + * 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.send.upload + +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import com.vitorpamplona.amethyst.commons.richtext.RichTextParser +import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator +import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName +import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing +import kotlinx.collections.immutable.ImmutableList + +@Stable +class ChatFileUploadState( + val defaultServer: ServerName, +) { + var isUploadingImage by mutableStateOf(false) + + var selectedServer by mutableStateOf(defaultServer) + var caption by mutableStateOf("") + var sensitiveContent by mutableStateOf(false) + + // Images and Videos + var multiOrchestrator by mutableStateOf(null) + + // 0 = Low, 1 = Medium, 2 = High, 3=UNCOMPRESSED + var mediaQualitySlider by mutableIntStateOf(1) + + fun load(uris: ImmutableList) { + reset() + this.multiOrchestrator = MultiOrchestrator(uris) + } + + fun isImage( + url: String, + mimeType: String?, + ): Boolean = mimeType?.startsWith("image/") == true || RichTextParser.isImageUrl(url) + + fun reset() { + multiOrchestrator = null + isUploadingImage = false + caption = "" + selectedServer = defaultServer + } + + fun deleteMediaToUpload(selected: SelectedMediaProcessing) { + multiOrchestrator?.remove(selected) + } + + fun canPost(): Boolean = !isUploadingImage && multiOrchestrator != null + + fun hasPickedMedia() = multiOrchestrator != null +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/private/ChatFileUploadModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt similarity index 51% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/private/ChatFileUploadModel.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt index 38b32875f..f70c67e5a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/private/ChatFileUploadModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt @@ -18,110 +18,52 @@ * 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.private +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload import android.content.Context -import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS -import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent import com.vitorpamplona.quartz.nip17Dm.files.encryption.AESGCM import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning -import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -@Stable -open class ChatFileUploadModel : ViewModel() { - var account: Account? = null - var chatroom: ChatroomKey? = null - var isNip17: Boolean = false - - var isUploadingImage by mutableStateOf(false) - - var selectedServer by mutableStateOf(null) - var caption by mutableStateOf("") - var sensitiveContent by mutableStateOf(false) - - // Images and Videos - var multiOrchestrator by mutableStateOf(null) - - // 0 = Low, 1 = Medium, 2 = High, 3=UNCOMPRESSED - var mediaQualitySlider by mutableIntStateOf(1) - - open fun load( - uris: ImmutableList, - chatroom: ChatroomKey, - isNip17: Boolean, - account: Account, - ) { - this.chatroom = chatroom - this.caption = "" - this.account = account - this.multiOrchestrator = MultiOrchestrator(uris) - this.selectedServer = defaultServer() - this.isNip17 = isNip17 - } - - fun isImage( - url: String, - mimeType: String?, - ): Boolean = mimeType?.startsWith("image/") == true || RichTextParser.isImageUrl(url) - - fun upload( - onError: (title: String, message: String) -> Unit, - context: Context, - onceUploaded: () -> Unit, - ) { - if (isNip17) { - uploadNIP17(onError, context, onceUploaded) - } else { - uploadNIP04(onError, context, onceUploaded) - } - } - +class ChatFileUploader( + val chatroom: ChatroomKey, + val account: Account, +) { fun uploadNIP17( + viewState: ChatFileUploadState, + scope: CoroutineScope, onError: (title: String, message: String) -> Unit, context: Context, onceUploaded: () -> Unit, ) { - val myAccount = account ?: return - val mySelectedServer = selectedServer ?: return - val myChatroom = chatroom ?: return - val myMultiOrchestrator = multiOrchestrator ?: return + val orchestrator = viewState.multiOrchestrator ?: return - viewModelScope.launch(Dispatchers.Default) { - isUploadingImage = true + scope.launch(Dispatchers.Default) { + viewState.isUploadingImage = true val cipher = AESGCM() val results = - myMultiOrchestrator.uploadEncrypted( - viewModelScope, - caption, - if (sensitiveContent) "" else null, - MediaCompressor.intToCompressorQuality(mediaQualitySlider), + orchestrator.uploadEncrypted( + scope, + viewState.caption, + if (viewState.sensitiveContent) "" else null, + MediaCompressor.intToCompressorQuality(viewState.mediaQualitySlider), cipher, - mySelectedServer, - myAccount, + viewState.selectedServer, + account, context, ) @@ -131,7 +73,7 @@ open class ChatFileUploadModel : ViewModel() { val template = ChatMessageEncryptedFileHeaderEvent.build( url = state.result.url, - to = myChatroom.users.map { LocalCache.getOrCreateUser(it).toPTag() }, + to = chatroom.users.map { LocalCache.getOrCreateUser(it).toPTag() }, cipher = cipher, mimeType = state.result.mimeTypeBeforeEncryption, originalHash = state.result.hashBeforeEncryption, @@ -142,54 +84,51 @@ open class ChatFileUploadModel : ViewModel() { state.result.fileHeader.blurHash ?.blurhash, ) { - if (caption.isNotEmpty()) { - alt(caption) + if (viewState.caption.isNotEmpty()) { + alt(viewState.caption) } - if (sensitiveContent) { + if (viewState.sensitiveContent) { contentWarning("") } } - account?.sendNIP17EncryptedFile(template) + account.sendNIP17EncryptedFile(template) } } onceUploaded() - cancelModel() + viewState.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 + viewState.isUploadingImage = false } } fun uploadNIP04( + viewState: ChatFileUploadState, + scope: CoroutineScope, onError: (title: String, message: String) -> Unit, context: Context, onceUploaded: () -> Unit, ) { - viewModelScope.launch(Dispatchers.Default) { - val myAccount = account ?: return@launch + val orchestrator = viewState.multiOrchestrator ?: return - val myMultiOrchestrator = multiOrchestrator ?: return@launch - - val mySelectedServer = selectedServer ?: return@launch - val myChatroom = chatroom ?: return@launch - - isUploadingImage = true + scope.launch(Dispatchers.Default) { + viewState.isUploadingImage = true val results = - myMultiOrchestrator.upload( - viewModelScope, - caption, - if (sensitiveContent) "" else null, - MediaCompressor.intToCompressorQuality(mediaQualitySlider), - mySelectedServer, - myAccount, + orchestrator.upload( + scope, + viewState.caption, + if (viewState.sensitiveContent) "" else null, + MediaCompressor.intToCompressorQuality(viewState.mediaQualitySlider), + viewState.selectedServer, + account, context, ) @@ -197,11 +136,11 @@ open class ChatFileUploadModel : ViewModel() { results.successful.forEach { if (it.result is UploadOrchestrator.OrchestratorResult.ServerResult) { val iMetaAttachments = IMetaAttachments() - iMetaAttachments.add(it.result, caption, if (sensitiveContent) "" else null) + iMetaAttachments.add(it.result, viewState.caption, if (viewState.sensitiveContent) "" else null) - myAccount.sendPrivateMessage( + account.sendPrivateMessage( message = it.result.url, - toUser = myChatroom.users.first().let { LocalCache.getOrCreateUser(it).toPTag() }, + toUser = chatroom.users.first().let { LocalCache.getOrCreateUser(it).toPTag() }, replyingTo = null, zapReceiver = null, contentWarningReason = null, @@ -214,7 +153,7 @@ open class ChatFileUploadModel : ViewModel() { } onceUploaded() - cancelModel() + viewState.reset() } } else { val errorMessages = results.errors.map { stringRes(context, it.errorResource, *it.params) }.distinct() @@ -222,22 +161,7 @@ open class ChatFileUploadModel : ViewModel() { onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n")) } - isUploadingImage = false + viewState.isUploadingImage = false } } - - open fun cancelModel() { - multiOrchestrator = null - isUploadingImage = false - caption = "" - selectedServer = defaultServer() - } - - fun deleteMediaToUpload(selected: SelectedMediaProcessing) { - multiOrchestrator?.remove(selected) - } - - fun canPost(): Boolean = !isUploadingImage && multiOrchestrator != null && selectedServer != null - - fun defaultServer() = account?.settings?.defaultFileServer ?: DEFAULT_MEDIA_SERVERS[0] } 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 index d40c9126e..b0a4faf2e 100644 --- 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 @@ -120,9 +120,9 @@ 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.private.RefreshingChatroomFeedView -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.private.ThinSendButton +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 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 54861385e..ad4749b33 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.private.ChatroomMessageCompose +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.messages.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 new file mode 100644 index 000000000..bc8a45e6d --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ThinSendButton.kt @@ -0,0 +1,50 @@ +/** + * 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.utils + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Send +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.Size20Modifier + +@Composable +fun ThinSendButton( + isActive: Boolean, + modifier: Modifier, + onClick: () -> Unit, +) { + IconButton( + enabled = isActive, + modifier = modifier, + onClick = onClick, + ) { + Icon( + imageVector = Icons.Default.Send, + contentDescription = stringRes(id = R.string.accessibility_send), + modifier = Size20Modifier, + ) + } +} 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 6221bbf06..c82b5255b 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,8 +147,8 @@ 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.private.ThinSendButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.public.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 import com.vitorpamplona.amethyst.ui.theme.DividerThickness