mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-03-17 13:21:50 +01:00
re organizing the private dm composables
This commit is contained in:
parent
639e2bb645
commit
429f9f81c4
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
||||
|
@ -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<ChatroomKey?>(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?>(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<SelectedMedia>, 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<String>(accountViewModel.userProfile().privateChatrooms[room]?.subject ?: "")
|
||||
}
|
||||
val message = remember { mutableStateOf<String>("") }
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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?>(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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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<ChatroomKey?>(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)
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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<String>(accountViewModel.userProfile().privateChatrooms[room]?.subject ?: "")
|
||||
}
|
||||
val message = remember { mutableStateOf<String>("") }
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
@ -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<Color>? = null,
|
||||
onClick: () -> Boolean,
|
||||
onAuthorClick: () -> Unit,
|
||||
actionMenu: @Composable (onDismiss: () -> Unit) -> Unit,
|
||||
detailRow: @Composable () -> Unit,
|
||||
drawAuthorLine: @Composable () -> Unit,
|
||||
inner: @Composable (MutableState<Color>) -> 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<Color>? = null,
|
||||
onClick: () -> Boolean,
|
||||
onAuthorClick: () -> Unit,
|
||||
actionMenu: @Composable (onDismiss: () -> Unit) -> Unit,
|
||||
detailRow: @Composable () -> Unit,
|
||||
drawAuthorLine: @Composable () -> Unit,
|
||||
inner: @Composable (MutableState<Color>) -> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
@ -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<Note?>(null)
|
||||
|
||||
var uploadState by mutableStateOf<ChatFileUploadState?>(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<MultiOrchestrator?>(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<SelectedMedia>) {
|
||||
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<Int, String> = 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<SelectedMedia>) {
|
||||
multiOrchestrator = MultiOrchestrator(uris)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
Log.d("Init", "OnCleared: ${this.javaClass.simpleName}")
|
@ -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
|
@ -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,
|
||||
)
|
||||
}
|
@ -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
|
@ -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,
|
||||
)
|
@ -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<MultiOrchestrator?>(null)
|
||||
|
||||
// 0 = Low, 1 = Medium, 2 = High, 3=UNCOMPRESSED
|
||||
var mediaQualitySlider by mutableIntStateOf(1)
|
||||
|
||||
fun load(uris: ImmutableList<SelectedMedia>) {
|
||||
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
|
||||
}
|
@ -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<ServerName?>(null)
|
||||
var caption by mutableStateOf("")
|
||||
var sensitiveContent by mutableStateOf(false)
|
||||
|
||||
// Images and Videos
|
||||
var multiOrchestrator by mutableStateOf<MultiOrchestrator?>(null)
|
||||
|
||||
// 0 = Low, 1 = Medium, 2 = High, 3=UNCOMPRESSED
|
||||
var mediaQualitySlider by mutableIntStateOf(1)
|
||||
|
||||
open fun load(
|
||||
uris: ImmutableList<SelectedMedia>,
|
||||
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]
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user