- Migrates NIP-17 DM new message model to its own model and disable some of the generic behavior from new posts.
- Fixes a bug on edits showing a previous edit. - Refactors user suggestions and lastword procedures - Adds a DM inline reply from other parts of the app, like on notification. - Hides Retweet button from DMs on Notifications - Fixes colors for drafts in chats. - Adds a nav function that computes the destination only after the user clicks on it and on a coroutine.
This commit is contained in:
@@ -2577,7 +2577,6 @@ class Account(
|
||||
message: String,
|
||||
toUser: User,
|
||||
replyingTo: Note? = null,
|
||||
mentions: List<User>?,
|
||||
zapReceiver: List<ZapSplitSetup>? = null,
|
||||
contentWarningReason: String? = null,
|
||||
zapRaiserAmount: Long? = null,
|
||||
@@ -2590,7 +2589,6 @@ class Account(
|
||||
message,
|
||||
toUser.toPTag(),
|
||||
replyingTo,
|
||||
mentions,
|
||||
zapReceiver,
|
||||
contentWarningReason,
|
||||
zapRaiserAmount,
|
||||
@@ -2605,7 +2603,6 @@ class Account(
|
||||
message: String,
|
||||
toUser: PTag,
|
||||
replyingTo: Note? = null,
|
||||
mentions: List<User>?,
|
||||
zapReceiver: List<ZapSplitSetup>? = null,
|
||||
contentWarningReason: String? = null,
|
||||
zapRaiserAmount: Long? = null,
|
||||
@@ -3183,10 +3180,16 @@ class Account(
|
||||
fun cachedDecryptContent(event: Event?): String? {
|
||||
if (event == null) return null
|
||||
|
||||
return if (event is PrivateDmEvent && isWriteable()) {
|
||||
event.cachedContentFor(signer)
|
||||
} else if (event is LnZapRequestEvent && event.isPrivateZap() && isWriteable()) {
|
||||
event.cachedPrivateZap()?.content
|
||||
return if (isWriteable()) {
|
||||
if (event is PrivateDmEvent) {
|
||||
event.cachedContentFor(signer)
|
||||
} else if (event is LnZapRequestEvent && event.isPrivateZap()) {
|
||||
event.cachedPrivateZap()?.content
|
||||
} else if (event is DraftEvent) {
|
||||
event.preCachedDraft(signer)?.content
|
||||
} else {
|
||||
event.content
|
||||
}
|
||||
} else {
|
||||
event.content
|
||||
}
|
||||
|
||||
@@ -236,16 +236,13 @@ class User(
|
||||
}.flatten()
|
||||
|
||||
@Synchronized
|
||||
private fun getOrCreatePrivateChatroomSync(key: ChatroomKey): Chatroom {
|
||||
checkNotInMainThread()
|
||||
|
||||
return privateChatrooms[key]
|
||||
private fun getOrCreatePrivateChatroomSync(key: ChatroomKey): Chatroom =
|
||||
privateChatrooms[key]
|
||||
?: run {
|
||||
val privateChatroom = Chatroom()
|
||||
privateChatrooms = privateChatrooms + Pair(key, privateChatroom)
|
||||
privateChatroom
|
||||
}
|
||||
}
|
||||
|
||||
private fun getOrCreatePrivateChatroom(user: User): Chatroom {
|
||||
val key = ChatroomKey(persistentSetOf(user.pubkeyHex))
|
||||
|
||||
+1
-1
@@ -81,7 +81,7 @@ import com.vitorpamplona.amethyst.ui.note.SearchIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ChannelName
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.ChannelName
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.search.SearchBarViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
|
||||
@@ -86,6 +86,7 @@ import com.vitorpamplona.quartz.nip10Notes.content.findURLs
|
||||
import com.vitorpamplona.quartz.nip10Notes.tags.notify
|
||||
import com.vitorpamplona.quartz.nip10Notes.tags.positionalMarkedTags
|
||||
import com.vitorpamplona.quartz.nip14Subject.subject
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.BaseDMGroupEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group
|
||||
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.messages.changeSubject
|
||||
@@ -818,7 +819,6 @@ open class NewPostViewModel : ViewModel() {
|
||||
message = tagger.message,
|
||||
toUser = originalNote!!.author!!,
|
||||
replyingTo = originalNote!!,
|
||||
mentions = tagger.pTags,
|
||||
zapReceiver = zapReceiver,
|
||||
contentWarningReason = contentWarningReason,
|
||||
zapRaiserAmount = localZapRaiserAmount,
|
||||
@@ -827,7 +827,7 @@ open class NewPostViewModel : ViewModel() {
|
||||
draftTag = localDraft,
|
||||
)
|
||||
} else if (originalNote?.event is NIP17Group) {
|
||||
val replyHint = originalNote?.toEventHint<ChatMessageEvent>()
|
||||
val replyHint = originalNote?.toEventHint<BaseDMGroupEvent>()
|
||||
|
||||
val template =
|
||||
if (replyHint == null) {
|
||||
@@ -868,7 +868,7 @@ open class NewPostViewModel : ViewModel() {
|
||||
account?.sendNIP17PrivateMessage(template, localDraft)
|
||||
} else if (!dmUsers.isNullOrEmpty()) {
|
||||
if (nip17 || dmUsers.size > 1) {
|
||||
val replyHint = originalNote?.toEventHint<ChatMessageEvent>()
|
||||
val replyHint = originalNote?.toEventHint<BaseDMGroupEvent>()
|
||||
val template =
|
||||
if (replyHint == null) {
|
||||
ChatMessageEvent.build(tagger.message, dmUsers.map { it.toPTag() }) {
|
||||
@@ -910,7 +910,6 @@ open class NewPostViewModel : ViewModel() {
|
||||
message = tagger.message,
|
||||
toUser = dmUsers.first(),
|
||||
replyingTo = originalNote,
|
||||
mentions = tagger.pTags,
|
||||
contentWarningReason = contentWarningReason,
|
||||
zapReceiver = zapReceiver,
|
||||
zapRaiserAmount = localZapRaiserAmount,
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ import com.vitorpamplona.amethyst.ui.note.UserCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgo
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.LoadUser
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.LoadUser
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
|
||||
@@ -91,7 +91,7 @@ import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.LoadUser
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.LoadUser
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.inlinePlaceholder
|
||||
|
||||
@@ -41,7 +41,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.NewItemsBubble
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.NewItemsBubble
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
|
||||
@@ -60,10 +60,10 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoadRedirectScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.NewPostScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarks.BookmarkListScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ChannelScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ChatroomListScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ChatroomScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ChatroomScreenByAuthor
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.ChatroomListScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.private.ChatroomScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.private.ChatroomScreenByAuthor
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.public.ChannelScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.CommunityScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.DiscoverScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.drafts.DraftListScreen
|
||||
@@ -94,6 +94,16 @@ fun NavBackStackEntry.message(): String? =
|
||||
URLDecoder.decode(it, "utf-8")
|
||||
}
|
||||
|
||||
fun NavBackStackEntry.replyId(): String? =
|
||||
arguments?.getString("replyId")?.let {
|
||||
URLDecoder.decode(it, "utf-8")
|
||||
}
|
||||
|
||||
fun NavBackStackEntry.draftId(): String? =
|
||||
arguments?.getString("draftId")?.let {
|
||||
URLDecoder.decode(it, "utf-8")
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppNavigation(
|
||||
accountViewModel: AccountViewModel,
|
||||
@@ -219,6 +229,8 @@ fun AppNavigation(
|
||||
ChatroomScreen(
|
||||
roomId = it.id(),
|
||||
draftMessage = it.message(),
|
||||
replyToNote = it.replyId(),
|
||||
editFromDraft = it.draftId(),
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
|
||||
@@ -54,6 +54,8 @@ interface INav {
|
||||
|
||||
fun nav(route: String)
|
||||
|
||||
fun nav(computeRoute: suspend () -> String)
|
||||
|
||||
fun newStack(route: String)
|
||||
|
||||
fun popBack()
|
||||
@@ -91,6 +93,15 @@ class Nav(
|
||||
}
|
||||
}
|
||||
|
||||
override fun nav(computeRoute: suspend () -> String) {
|
||||
scope.launch {
|
||||
val route = computeRoute()
|
||||
if (getRouteWithArguments(controller) != route) {
|
||||
controller.navigate(route)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun newStack(route: String) {
|
||||
scope.launch {
|
||||
controller.navigate(route) {
|
||||
@@ -135,6 +146,9 @@ object EmptyNav : INav {
|
||||
override fun nav(route: String) {
|
||||
}
|
||||
|
||||
override fun nav(computeRoute: suspend () -> String) {
|
||||
}
|
||||
|
||||
override fun newStack(route: String) {
|
||||
}
|
||||
|
||||
@@ -161,6 +175,11 @@ class ObservableNavigate(
|
||||
nav.nav(route)
|
||||
}
|
||||
|
||||
override fun nav(computeRoute: suspend () -> String) {
|
||||
onNavigate()
|
||||
nav.nav(computeRoute)
|
||||
}
|
||||
|
||||
override fun newStack(route: String) {
|
||||
onNavigate()
|
||||
nav.newStack(route)
|
||||
|
||||
@@ -21,8 +21,10 @@
|
||||
package com.vitorpamplona.amethyst.ui.navigation
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.LocalCache.users
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource.user
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
@@ -36,7 +38,6 @@ import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessa
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import java.net.URLEncoder
|
||||
|
||||
fun routeFor(
|
||||
@@ -110,23 +111,71 @@ fun routeFor(
|
||||
fun routeToMessage(
|
||||
user: HexKey,
|
||||
draftMessage: String?,
|
||||
replyId: HexKey? = null,
|
||||
quoteId: HexKey? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
): String =
|
||||
routeToMessage(
|
||||
setOf(user),
|
||||
draftMessage,
|
||||
replyId,
|
||||
quoteId,
|
||||
accountViewModel,
|
||||
)
|
||||
|
||||
fun routeToMessage(
|
||||
users: Set<HexKey>,
|
||||
draftMessage: String?,
|
||||
replyId: HexKey? = null,
|
||||
quoteId: HexKey? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
) = routeToMessage(
|
||||
ChatroomKey(users),
|
||||
draftMessage,
|
||||
replyId,
|
||||
quoteId,
|
||||
accountViewModel,
|
||||
)
|
||||
|
||||
fun routeToMessage(
|
||||
room: ChatroomKey,
|
||||
draftMessage: String?,
|
||||
replyId: HexKey? = null,
|
||||
quoteId: HexKey? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
): String {
|
||||
val withKey = ChatroomKey(persistentSetOf(user))
|
||||
accountViewModel.account.userProfile().createChatroom(withKey)
|
||||
return if (draftMessage != null) {
|
||||
val encodedMessage = URLEncoder.encode(draftMessage, "utf-8")
|
||||
"Room/${withKey.hashCode()}?message=$encodedMessage"
|
||||
} else {
|
||||
"Room/${withKey.hashCode()}"
|
||||
accountViewModel.account.userProfile().createChatroom(room)
|
||||
|
||||
val params =
|
||||
listOfNotNull(
|
||||
draftMessage?.let {
|
||||
"message=${URLEncoder.encode(it, "utf-8")}"
|
||||
},
|
||||
replyId?.let {
|
||||
"replyId=${URLEncoder.encode(it, "utf-8")}"
|
||||
},
|
||||
quoteId?.let {
|
||||
"quoteId=${URLEncoder.encode(it, "utf-8")}"
|
||||
},
|
||||
)
|
||||
|
||||
return buildString {
|
||||
append("Room/")
|
||||
append(room.hashCode().toString())
|
||||
if (params.isNotEmpty()) {
|
||||
append("?")
|
||||
append(params.joinToString("&"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun routeToMessage(
|
||||
user: User,
|
||||
draftMessage: String?,
|
||||
replyId: HexKey? = null,
|
||||
quoteId: HexKey? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
): String = routeToMessage(user.pubkeyHex, draftMessage, accountViewModel)
|
||||
): String = routeToMessage(user.pubkeyHex, draftMessage, replyId, quoteId, accountViewModel)
|
||||
|
||||
fun routeFor(note: Channel): String = "Channel/${note.idHex}"
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ sealed class Route(
|
||||
|
||||
object Room :
|
||||
Route(
|
||||
route = "Room/{id}?message={message}",
|
||||
route = "Room/{id}?message={message}&replyId={replyId}&draftId={draftId}",
|
||||
icon = R.drawable.ic_moments,
|
||||
arguments =
|
||||
listOf(
|
||||
@@ -179,6 +179,16 @@ sealed class Route(
|
||||
nullable = true
|
||||
defaultValue = null
|
||||
},
|
||||
navArgument("replyId") {
|
||||
type = NavType.StringType
|
||||
nullable = true
|
||||
defaultValue = null
|
||||
},
|
||||
navArgument("draftId") {
|
||||
type = NavType.StringType
|
||||
nullable = true
|
||||
defaultValue = null
|
||||
},
|
||||
).toImmutableList(),
|
||||
)
|
||||
|
||||
|
||||
@@ -78,11 +78,11 @@ import com.vitorpamplona.amethyst.ui.layouts.LeftPictureLayout
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.BannerImage
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.EndedFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.LiveFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.OfflineFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ScheduledFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.public.ChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.public.EndedFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.public.LiveFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.public.OfflineFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.public.ScheduledFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.observeAppDefinition
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CheckIfVideoIsOnline
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
|
||||
|
||||
@@ -25,7 +25,6 @@ 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.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -73,15 +72,15 @@ fun LoadDecryptedContentOrNull(
|
||||
accountViewModel: AccountViewModel,
|
||||
inner: @Composable (String?) -> Unit,
|
||||
) {
|
||||
@Suppress("ProduceStateDoesNotAssignValue")
|
||||
val decryptedContent by
|
||||
produceState(initialValue = accountViewModel.cachedDecrypt(note), key1 = note.event?.id) {
|
||||
accountViewModel.decrypt(note) {
|
||||
if (value != it) {
|
||||
value = it
|
||||
}
|
||||
var decryptedContent by remember(note.event?.id) { mutableStateOf(accountViewModel.cachedDecrypt(note)) }
|
||||
|
||||
LaunchedEffect(note.event?.id) {
|
||||
accountViewModel.decrypt(note) {
|
||||
if (decryptedContent != it) {
|
||||
decryptedContent = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner(decryptedContent)
|
||||
}
|
||||
|
||||
@@ -43,7 +43,6 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
@@ -74,7 +73,6 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@@ -202,15 +200,14 @@ fun ErrorRow(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
errorState.user?.let {
|
||||
val scope = rememberCoroutineScope()
|
||||
Column(Modifier.width(Size40dp), horizontalAlignment = Alignment.Start) {
|
||||
UserPicture(errorState.user, Size30dp, Modifier, accountViewModel, nav)
|
||||
Spacer(StdVertSpacer)
|
||||
IconButton(
|
||||
modifier = Size30Modifier,
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
nav.nav(routeToMessage(it, errorState.error, accountViewModel))
|
||||
nav.nav {
|
||||
routeToMessage(it, errorState.error, accountViewModel = accountViewModel)
|
||||
}
|
||||
},
|
||||
) {
|
||||
|
||||
@@ -123,12 +123,11 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderTorrentComment
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderWikiContent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.VideoDisplay
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.RenderChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.public.RenderChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font12SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfDoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfEndPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing10dp
|
||||
@@ -1022,7 +1021,7 @@ fun DisplayDraftChat() {
|
||||
Text(
|
||||
"Draft",
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
modifier = HalfEndPadding,
|
||||
modifier = Modifier,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = Font12SP,
|
||||
maxLines = 1,
|
||||
|
||||
@@ -627,7 +627,11 @@ fun ZapVote(
|
||||
title = toast.title,
|
||||
textContent = toast.msg,
|
||||
onClickStartMessage = {
|
||||
baseNote.author?.let { nav.nav(routeToMessage(it, toast.msg, accountViewModel)) }
|
||||
baseNote.author?.let {
|
||||
nav.nav {
|
||||
routeToMessage(it, toast.msg, accountViewModel = accountViewModel)
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismiss = { showErrorMessageDialog = null },
|
||||
)
|
||||
|
||||
@@ -111,6 +111,7 @@ import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.buildNewPostRoute
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
|
||||
import com.vitorpamplona.amethyst.ui.note.types.EditState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
@@ -147,7 +148,9 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.reactionBox
|
||||
import com.vitorpamplona.amethyst.ui.theme.ripple24dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.selectedReactionBoxModifier
|
||||
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji
|
||||
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
@@ -209,13 +212,16 @@ private fun InnerReactionRow(
|
||||
)
|
||||
},
|
||||
three = {
|
||||
BoostWithDialog(
|
||||
baseNote,
|
||||
editState,
|
||||
MaterialTheme.colorScheme.placeholderText,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
val isDM = baseNote.event is ChatroomKeyable
|
||||
if (!isDM) {
|
||||
BoostWithDialog(
|
||||
baseNote,
|
||||
editState,
|
||||
MaterialTheme.colorScheme.placeholderText,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
},
|
||||
four = {
|
||||
LikeReaction(baseNote, MaterialTheme.colorScheme.placeholderText, accountViewModel, nav)
|
||||
@@ -558,56 +564,46 @@ private fun BoostWithDialog(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
var wantsToQuote by remember { mutableStateOf<Note?>(null) }
|
||||
var wantsToFork by remember { mutableStateOf<Note?>(null) }
|
||||
|
||||
if (wantsToQuote != null) {
|
||||
val route =
|
||||
buildNewPostRoute(
|
||||
quote = wantsToQuote?.idHex,
|
||||
version =
|
||||
(editState.value as? GenericLoadable.Loaded)
|
||||
?.loaded
|
||||
?.modificationToShow
|
||||
?.value
|
||||
?.idHex,
|
||||
)
|
||||
nav.nav(route)
|
||||
}
|
||||
|
||||
if (wantsToFork != null) {
|
||||
val replyTo =
|
||||
remember(wantsToFork) {
|
||||
val forkEvent = wantsToFork?.event
|
||||
if (forkEvent is BaseThreadedEvent) {
|
||||
val hex = forkEvent.replyingTo()
|
||||
wantsToFork?.replyTo?.filter { it.event?.id == hex }?.firstOrNull()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val route =
|
||||
buildNewPostRoute(
|
||||
quote = wantsToQuote?.idHex,
|
||||
baseReplyTo = replyTo?.idHex,
|
||||
fork = wantsToFork?.idHex,
|
||||
version =
|
||||
(editState.value as? GenericLoadable.Loaded)
|
||||
?.loaded
|
||||
?.modificationToShow
|
||||
?.value
|
||||
?.idHex,
|
||||
)
|
||||
nav.nav(route)
|
||||
}
|
||||
|
||||
BoostReaction(
|
||||
baseNote,
|
||||
grayTint,
|
||||
accountViewModel,
|
||||
onQuotePress = { wantsToQuote = baseNote },
|
||||
onForkPress = { wantsToFork = baseNote },
|
||||
onQuotePress = {
|
||||
nav.nav {
|
||||
buildNewPostRoute(
|
||||
quote = baseNote.idHex,
|
||||
version =
|
||||
(editState.value as? GenericLoadable.Loaded)
|
||||
?.loaded
|
||||
?.modificationToShow
|
||||
?.value
|
||||
?.idHex,
|
||||
)
|
||||
}
|
||||
},
|
||||
onForkPress = {
|
||||
nav.nav {
|
||||
val forkEvent = baseNote.event
|
||||
val replyTo =
|
||||
if (forkEvent is BaseThreadedEvent) {
|
||||
val hex = forkEvent.replyingTo()
|
||||
baseNote.replyTo?.filter { it.event?.id == hex }?.firstOrNull()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
buildNewPostRoute(
|
||||
baseReplyTo = replyTo?.idHex,
|
||||
fork = baseNote.idHex,
|
||||
version =
|
||||
(editState.value as? GenericLoadable.Loaded)
|
||||
?.loaded
|
||||
?.modificationToShow
|
||||
?.value
|
||||
?.idHex,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -618,18 +614,37 @@ private fun ReplyReactionWithDialog(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
var wantsToReplyTo by remember { mutableStateOf<Note?>(null) }
|
||||
|
||||
if (wantsToReplyTo != null) {
|
||||
val route =
|
||||
buildNewPostRoute(
|
||||
baseReplyTo = wantsToReplyTo?.idHex,
|
||||
quote = null,
|
||||
)
|
||||
nav.nav(route)
|
||||
ReplyReaction(baseNote, grayTint, accountViewModel) {
|
||||
val noteEvent = baseNote.event
|
||||
if (noteEvent is PrivateDmEvent) {
|
||||
nav.nav {
|
||||
routeToMessage(
|
||||
room = noteEvent.chatroomKey(accountViewModel.userProfile().pubkeyHex),
|
||||
draftMessage = null,
|
||||
replyId = noteEvent.id,
|
||||
quoteId = null,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
} else if (noteEvent is ChatroomKeyable) {
|
||||
nav.nav {
|
||||
routeToMessage(
|
||||
room = noteEvent.chatroomKey(accountViewModel.userProfile().pubkeyHex),
|
||||
draftMessage = null,
|
||||
replyId = noteEvent.id,
|
||||
quoteId = null,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
nav.nav {
|
||||
buildNewPostRoute(
|
||||
baseReplyTo = baseNote.idHex,
|
||||
quote = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ReplyReaction(baseNote, grayTint, accountViewModel) { wantsToReplyTo = baseNote }
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -44,7 +44,7 @@ import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.LoadUser
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.LoadUser
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.public.ChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
||||
|
||||
@@ -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.chatrooms.ChatroomHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.private.ChatroomHeader
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
|
||||
|
||||
+1
-2
@@ -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.chatrooms.ChatroomHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.private.ChatroomHeader
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
@@ -58,7 +58,6 @@ import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
|
||||
import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.files.encryption.AESGCM
|
||||
import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
|
||||
@@ -61,9 +61,9 @@ import com.vitorpamplona.amethyst.ui.note.ZapReaction
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.JoinCommunityButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.LeaveCommunityButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.NormalTimeAgo
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.public.JoinCommunityButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.public.LeaveCommunityButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.public.NormalTimeAgo
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
|
||||
@@ -58,8 +58,8 @@ import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.DisplayAuthorBanner
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.LiveFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ScheduledFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.public.LiveFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.public.ScheduledFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CheckIfVideoIsOnline
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CrossfadeCheckIfVideoIsOnline
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.public.ChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
||||
|
||||
@@ -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.chatrooms.ChatroomHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.private.ChatroomHeader
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
|
||||
+2
@@ -1666,6 +1666,8 @@ class AccountViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun findUsersStartingWithSync(prefix: String) = LocalCache.findUsersStartingWith(prefix, account)
|
||||
|
||||
fun relayStatusFlow() = Amethyst.instance.client.relayStatusFlow()
|
||||
|
||||
val draftNoteCache = CachedDraftNotes(this)
|
||||
|
||||
+1
-1
@@ -162,7 +162,7 @@ import com.vitorpamplona.amethyst.ui.note.ShowUserSuggestionList
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.WatchAndLoadMyEmojiList
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapSplitIcon
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.MyTextField
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.utils.MyTextField
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
|
||||
+1
-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.chatrooms
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
+1
-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.chatrooms
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
+1
-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.chatrooms
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist
|
||||
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.Row
|
||||
+18
-2
@@ -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.chatrooms
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -75,10 +75,13 @@ import com.vitorpamplona.amethyst.ui.navigation.MainTopBar
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.DisappearingScaffold
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.private.Chatroom
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.public.Channel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -121,6 +124,7 @@ fun ChatroomListScreen(
|
||||
|
||||
class TwoPaneNav(
|
||||
val nav: INav,
|
||||
val scope: CoroutineScope,
|
||||
) : INav {
|
||||
override val drawerState: DrawerState = nav.drawerState
|
||||
|
||||
@@ -134,6 +138,17 @@ class TwoPaneNav(
|
||||
}
|
||||
}
|
||||
|
||||
override fun nav(routeMaker: suspend () -> String) {
|
||||
scope.launch(Dispatchers.Default) {
|
||||
val route = routeMaker()
|
||||
if (route.startsWith("Room/") || route.startsWith("Channel/")) {
|
||||
innerNav.value = RouteId(route.substringBefore("/"), route.substringAfter("/"))
|
||||
} else {
|
||||
nav.nav(route)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun newStack(route: String) {
|
||||
nav.newStack(route)
|
||||
}
|
||||
@@ -172,7 +187,8 @@ fun ChatroomListTwoPane(
|
||||
nav: INav,
|
||||
) {
|
||||
/** The index of the currently selected word, or `null` if none is selected */
|
||||
val twoPaneNav = remember { TwoPaneNav(nav) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val twoPaneNav = remember { TwoPaneNav(nav, scope) }
|
||||
|
||||
val strategy =
|
||||
remember {
|
||||
+287
@@ -0,0 +1,287 @@
|
||||
/**
|
||||
* 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.chatlist.private
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.ui.theme.ChatBubbleMaxSizeModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.ChatBubbleShapeMe
|
||||
import com.vitorpamplona.amethyst.ui.theme.ChatBubbleShapeThem
|
||||
import com.vitorpamplona.amethyst.ui.theme.ChatPaddingInnerQuoteModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.ChatPaddingModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfHalfVertPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.ReactionRowHeightChat
|
||||
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.chatBackground
|
||||
import com.vitorpamplona.amethyst.ui.theme.chatDraftBackground
|
||||
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.messageBubbleLimits
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun BubblePreview() {
|
||||
val backgroundBubbleColor =
|
||||
remember {
|
||||
mutableStateOf<Color>(Color.Transparent)
|
||||
}
|
||||
|
||||
Column {
|
||||
ChatBubbleLayout(
|
||||
isLoggedInUser = false,
|
||||
isDraft = false,
|
||||
innerQuote = false,
|
||||
isComplete = true,
|
||||
hasDetailsToShow = true,
|
||||
drawAuthorInfo = true,
|
||||
parentBackgroundColor = backgroundBubbleColor,
|
||||
onClick = { false },
|
||||
onAuthorClick = {},
|
||||
actionMenu = { onDismiss ->
|
||||
},
|
||||
drawAuthorLine = {
|
||||
UserDisplayNameLayout(
|
||||
picture = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Person,
|
||||
contentDescription = null,
|
||||
modifier =
|
||||
Modifier
|
||||
.size(Size20dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color.LightGray),
|
||||
)
|
||||
},
|
||||
name = {
|
||||
Text("Someone else", fontWeight = FontWeight.Bold)
|
||||
},
|
||||
)
|
||||
},
|
||||
detailRow = { Text("Relays and Actions") },
|
||||
) { backgroundBubbleColor ->
|
||||
Text("This is my note")
|
||||
}
|
||||
|
||||
ChatBubbleLayout(
|
||||
isLoggedInUser = true,
|
||||
isDraft = true,
|
||||
innerQuote = false,
|
||||
isComplete = true,
|
||||
hasDetailsToShow = true,
|
||||
drawAuthorInfo = true,
|
||||
parentBackgroundColor = backgroundBubbleColor,
|
||||
onClick = { false },
|
||||
onAuthorClick = {},
|
||||
actionMenu = { onDismiss ->
|
||||
},
|
||||
drawAuthorLine = {
|
||||
UserDisplayNameLayout(
|
||||
picture = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Person,
|
||||
contentDescription = null,
|
||||
modifier =
|
||||
Modifier
|
||||
.size(Size20dp)
|
||||
.clip(CircleShape),
|
||||
)
|
||||
},
|
||||
name = {
|
||||
Text("Me", fontWeight = FontWeight.Bold)
|
||||
},
|
||||
)
|
||||
},
|
||||
detailRow = { Text("Relays and Actions") },
|
||||
) { backgroundBubbleColor ->
|
||||
Text("This is a very long long loong note")
|
||||
}
|
||||
|
||||
ChatBubbleLayout(
|
||||
isLoggedInUser = true,
|
||||
isDraft = false,
|
||||
innerQuote = false,
|
||||
isComplete = false,
|
||||
hasDetailsToShow = false,
|
||||
drawAuthorInfo = false,
|
||||
parentBackgroundColor = backgroundBubbleColor,
|
||||
onClick = { false },
|
||||
onAuthorClick = {},
|
||||
actionMenu = { onDismiss ->
|
||||
},
|
||||
drawAuthorLine = {
|
||||
UserDisplayNameLayout(
|
||||
picture = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Person,
|
||||
contentDescription = null,
|
||||
modifier =
|
||||
Modifier
|
||||
.size(Size20dp)
|
||||
.clip(CircleShape),
|
||||
)
|
||||
},
|
||||
name = {
|
||||
Text("Me", fontWeight = FontWeight.Bold)
|
||||
},
|
||||
)
|
||||
},
|
||||
detailRow = { Text("Relays and Actions") },
|
||||
) { backgroundBubbleColor ->
|
||||
Text("Short note")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+77
-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.chatrooms
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.private
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Stable
|
||||
@@ -40,9 +40,11 @@ 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.stringRes
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash
|
||||
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.nip30CustomEmoji.emojis
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
@@ -53,6 +55,7 @@ import kotlinx.coroutines.launch
|
||||
open class ChatFileUploadModel : ViewModel() {
|
||||
var account: Account? = null
|
||||
var chatroom: ChatroomKey? = null
|
||||
var isNip17: Boolean = false
|
||||
|
||||
var isUploadingImage by mutableStateOf(false)
|
||||
|
||||
@@ -69,6 +72,7 @@ open class ChatFileUploadModel : ViewModel() {
|
||||
open fun load(
|
||||
uris: ImmutableList<SelectedMedia>,
|
||||
chatroom: ChatroomKey,
|
||||
isNip17: Boolean,
|
||||
account: Account,
|
||||
) {
|
||||
this.chatroom = chatroom
|
||||
@@ -76,6 +80,7 @@ open class ChatFileUploadModel : ViewModel() {
|
||||
this.account = account
|
||||
this.multiOrchestrator = MultiOrchestrator(uris)
|
||||
this.selectedServer = defaultServer()
|
||||
this.isNip17 = isNip17
|
||||
}
|
||||
|
||||
fun isImage(
|
||||
@@ -87,6 +92,18 @@ open class ChatFileUploadModel : ViewModel() {
|
||||
onError: (title: String, message: String) -> Unit,
|
||||
context: Context,
|
||||
onceUploaded: () -> Unit,
|
||||
) {
|
||||
if (isNip17) {
|
||||
uploadNIP17(onError, context, onceUploaded)
|
||||
} else {
|
||||
uploadNIP04(onError, context, onceUploaded)
|
||||
}
|
||||
}
|
||||
|
||||
fun uploadNIP17(
|
||||
onError: (title: String, message: String) -> Unit,
|
||||
context: Context,
|
||||
onceUploaded: () -> Unit,
|
||||
) {
|
||||
val myAccount = account ?: return
|
||||
val mySelectedServer = selectedServer ?: return
|
||||
@@ -152,6 +169,65 @@ open class ChatFileUploadModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun uploadNIP04(
|
||||
onError: (title: String, message: String) -> Unit,
|
||||
context: Context,
|
||||
onceUploaded: () -> Unit,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
val myAccount = account ?: return@launch
|
||||
|
||||
val myMultiOrchestrator = multiOrchestrator ?: return@launch
|
||||
|
||||
val mySelectedServer = selectedServer ?: return@launch
|
||||
val myChatroom = chatroom ?: return@launch
|
||||
|
||||
isUploadingImage = true
|
||||
|
||||
val results =
|
||||
myMultiOrchestrator.upload(
|
||||
viewModelScope,
|
||||
caption,
|
||||
if (sensitiveContent) "" else null,
|
||||
MediaCompressor.intToCompressorQuality(mediaQualitySlider),
|
||||
mySelectedServer,
|
||||
myAccount,
|
||||
context,
|
||||
)
|
||||
|
||||
if (results.allGood) {
|
||||
results.successful.forEach {
|
||||
if (it.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
|
||||
val iMetaAttachments = IMetaAttachments()
|
||||
iMetaAttachments.add(it.result, caption, if (sensitiveContent) "" else null)
|
||||
|
||||
myAccount.sendPrivateMessage(
|
||||
message = it.result.url,
|
||||
toUser = myChatroom.users.first().let { LocalCache.getOrCreateUser(it).toPTag() },
|
||||
replyingTo = null,
|
||||
zapReceiver = null,
|
||||
contentWarningReason = null,
|
||||
zapRaiserAmount = null,
|
||||
geohash = null,
|
||||
imetas = iMetaAttachments.iMetaAttachments,
|
||||
emojis = null,
|
||||
draftTag = null,
|
||||
)
|
||||
}
|
||||
|
||||
onceUploaded()
|
||||
cancelModel()
|
||||
}
|
||||
} 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 cancelModel() {
|
||||
multiOrchestrator = null
|
||||
isUploadingImage = false
|
||||
+3
-4
@@ -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.chatrooms
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.private
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -148,9 +148,8 @@ fun ChatFileUploadView(
|
||||
postViewModel.upload(
|
||||
onError = accountViewModel::toast,
|
||||
context = context,
|
||||
) {
|
||||
onClose
|
||||
}
|
||||
onceUploaded = onClose,
|
||||
)
|
||||
|
||||
postViewModel.selectedServer?.let {
|
||||
if (it.type != ServerType.NIP95) {
|
||||
+640
@@ -0,0 +1,640 @@
|
||||
/**
|
||||
* 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.chatlist.private
|
||||
|
||||
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
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
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.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.quartz.nip01Core.core.AddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.references.references
|
||||
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.nip10Notes.content.findHashtags
|
||||
import com.vitorpamplona.quartz.nip10Notes.content.findNostrUris
|
||||
import com.vitorpamplona.quartz.nip10Notes.content.findURLs
|
||||
import com.vitorpamplona.quartz.nip14Subject.subject
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.BaseDMGroupEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group
|
||||
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.quotes.quotes
|
||||
import com.vitorpamplona.quartz.nip19Bech32.toNpub
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.emojis
|
||||
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup
|
||||
import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplitSetup
|
||||
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip92IMeta.imetas
|
||||
import com.vitorpamplona.quartz.utils.Hex
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.UUID
|
||||
|
||||
@Stable
|
||||
open class ChatNewMessageViewModel : ViewModel() {
|
||||
var draftTag: String by mutableStateOf(UUID.randomUUID().toString())
|
||||
|
||||
var accountViewModel: AccountViewModel? = null
|
||||
var account: Account? = null
|
||||
var room: ChatroomKey? = null
|
||||
|
||||
var requiresNIP17: Boolean = false
|
||||
|
||||
val replyTo = mutableStateOf<Note?>(null)
|
||||
|
||||
val iMetaAttachments = IMetaAttachments()
|
||||
|
||||
var message by mutableStateOf(TextFieldValue(""))
|
||||
var urlPreview by mutableStateOf<String?>(null)
|
||||
var isUploadingImage by mutableStateOf(false)
|
||||
|
||||
val userSuggestions = UserSuggestions()
|
||||
var userSuggestionsMainMessage: UserSuggestionAnchor? = null
|
||||
|
||||
val emojiSearch: MutableStateFlow<String> = MutableStateFlow("")
|
||||
val emojiSuggestions: StateFlow<List<Account.EmojiMedia>> by lazy {
|
||||
account!!
|
||||
.myEmojis
|
||||
.combine(emojiSearch) { list, search ->
|
||||
if (search.length == 1) {
|
||||
list
|
||||
} else if (search.isNotEmpty()) {
|
||||
val code = search.removePrefix(":")
|
||||
list.filter { it.code.startsWith(code) }
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(5000),
|
||||
emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// Forward Zap to
|
||||
var wantsForwardZapTo by mutableStateOf(false)
|
||||
var forwardZapTo by mutableStateOf<Split<User>>(Split())
|
||||
var forwardZapToEditting by mutableStateOf(TextFieldValue(""))
|
||||
|
||||
// NSFW, Sensitive
|
||||
var wantsToMarkAsSensitive by mutableStateOf(false)
|
||||
|
||||
// ZapRaiser
|
||||
var canAddZapRaiser by mutableStateOf(false)
|
||||
var wantsZapraiser by mutableStateOf(false)
|
||||
var zapRaiserAmount by mutableStateOf<Long?>(null)
|
||||
|
||||
// NIP17 Wrapped DMs / Group messages
|
||||
var nip17 by mutableStateOf(false)
|
||||
|
||||
val draftTextChanges = Channel<String>(Channel.CONFLATED)
|
||||
|
||||
fun lnAddress(): String? = account?.userProfile()?.info?.lnAddress()
|
||||
|
||||
fun hasLnAddress(): Boolean = account?.userProfile()?.info?.lnAddress() != null
|
||||
|
||||
fun user(): User? = account?.userProfile()
|
||||
|
||||
open fun init(accountVM: AccountViewModel) {
|
||||
this.accountViewModel = accountVM
|
||||
this.account = accountVM.account
|
||||
this.canAddInvoice = accountVM.userProfile().info?.lnAddress() != null
|
||||
this.canAddZapRaiser = accountVM.userProfile().info?.lnAddress() != null
|
||||
}
|
||||
|
||||
open fun load(room: ChatroomKey) {
|
||||
this.room = room
|
||||
this.requiresNIP17 = room.users.size > 1
|
||||
if (this.requiresNIP17) {
|
||||
this.nip17 = true
|
||||
}
|
||||
}
|
||||
|
||||
open fun reply(replyNote: Note) {
|
||||
replyTo.value = replyNote
|
||||
}
|
||||
|
||||
open fun quote(quote: Note) {
|
||||
message = TextFieldValue(message.text + "\nnostr:${quote.toNEvent()}")
|
||||
urlPreview = findUrlInMessage()
|
||||
|
||||
// creates a split with that author.
|
||||
val accountViewModel = accountViewModel ?: return
|
||||
val quotedAuthor = quote.author ?: return
|
||||
|
||||
if (quotedAuthor.pubkeyHex != accountViewModel.userProfile().pubkeyHex) {
|
||||
if (forwardZapTo.items.none { it.key.pubkeyHex == quotedAuthor.pubkeyHex }) {
|
||||
forwardZapTo.addItem(quotedAuthor)
|
||||
}
|
||||
if (forwardZapTo.items.none { it.key.pubkeyHex == accountViewModel.userProfile().pubkeyHex }) {
|
||||
forwardZapTo.addItem(accountViewModel.userProfile())
|
||||
}
|
||||
|
||||
val pos = forwardZapTo.items.indexOfFirst { it.key.pubkeyHex == quotedAuthor.pubkeyHex }
|
||||
forwardZapTo.updatePercentage(pos, 0.9f)
|
||||
|
||||
wantsForwardZapTo = true
|
||||
}
|
||||
}
|
||||
|
||||
open fun editFromDraft(draft: Note) {
|
||||
val noteEvent = draft.event
|
||||
val noteAuthor = draft.author
|
||||
|
||||
if (noteEvent is DraftEvent && noteAuthor != null) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
accountViewModel?.createTempDraftNote(noteEvent) { innerNote ->
|
||||
if (innerNote != null) {
|
||||
val oldTag = (draft.event as? AddressableEvent)?.dTag()
|
||||
if (oldTag != null) {
|
||||
draftTag = oldTag
|
||||
}
|
||||
loadFromDraft(innerNote)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadFromDraft(draft: Note) {
|
||||
Log.d("draft", draft.event!!.toJson())
|
||||
|
||||
val draftEvent = draft.event ?: return
|
||||
val accountViewModel = accountViewModel ?: return
|
||||
|
||||
val localfowardZapTo = draftEvent.tags.zapSplitSetup()
|
||||
val totalWeight = localfowardZapTo.sumOf { it.weight }
|
||||
forwardZapTo = Split()
|
||||
localfowardZapTo.forEach {
|
||||
if (it is ZapSplitSetup) {
|
||||
val user = LocalCache.getOrCreateUser(it.pubKeyHex)
|
||||
forwardZapTo.addItem(user, (it.weight / totalWeight).toFloat())
|
||||
}
|
||||
// don't support edditing old-style splits.
|
||||
}
|
||||
forwardZapToEditting = TextFieldValue("")
|
||||
wantsForwardZapTo = localfowardZapTo.isNotEmpty()
|
||||
|
||||
wantsToMarkAsSensitive = draftEvent.isSensitive()
|
||||
|
||||
val zapraiser = draftEvent.zapraiserAmount()
|
||||
wantsZapraiser = zapraiser != null
|
||||
zapRaiserAmount = null
|
||||
if (zapraiser != null) {
|
||||
zapRaiserAmount = zapraiser
|
||||
}
|
||||
|
||||
if (forwardZapTo.items.isNotEmpty()) {
|
||||
wantsForwardZapTo = true
|
||||
}
|
||||
|
||||
draftEvent.subject()?.let {
|
||||
subject = TextFieldValue()
|
||||
}
|
||||
|
||||
if (draftEvent is NIP17Group) {
|
||||
toUsers =
|
||||
TextFieldValue(
|
||||
draftEvent.groupMembers().mapNotNull { runCatching { Hex.decode(it).toNpub() }.getOrNull() }.joinToString(", ") { "@$it" },
|
||||
)
|
||||
} else if (draftEvent is PrivateDmEvent) {
|
||||
val recepientNpub = draftEvent.verifiedRecipientPubKey()?.let { Hex.decode(it).toNpub() }
|
||||
toUsers = TextFieldValue("@$recepientNpub")
|
||||
}
|
||||
|
||||
message =
|
||||
if (draftEvent is PrivateDmEvent) {
|
||||
TextFieldValue(draftEvent.cachedContentFor(accountViewModel.account.signer) ?: "")
|
||||
} else {
|
||||
TextFieldValue(draftEvent.content)
|
||||
}
|
||||
|
||||
requiresNIP17 = draftEvent is NIP17Group
|
||||
nip17 = draftEvent is NIP17Group
|
||||
|
||||
urlPreview = findUrlInMessage()
|
||||
}
|
||||
|
||||
fun sendPost() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
innerSendPost(null)
|
||||
accountViewModel?.deleteDraft(draftTag)
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendPostSync() {
|
||||
innerSendPost(null)
|
||||
accountViewModel?.deleteDraft(draftTag)
|
||||
cancel()
|
||||
}
|
||||
|
||||
fun sendDraft() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
sendDraftSync()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendDraftSync() {
|
||||
if (message.text.isBlank()) {
|
||||
account?.deleteDraft(draftTag)
|
||||
} else {
|
||||
innerSendPost(draftTag)
|
||||
}
|
||||
}
|
||||
|
||||
private fun innerSendPost(dTag: String?) {
|
||||
val room = room ?: return
|
||||
val accountViewModel = accountViewModel ?: return
|
||||
|
||||
val urls = findURLs(message.text)
|
||||
val usedAttachments = iMetaAttachments.filterIsIn(urls.toSet())
|
||||
val emojis = findEmoji(message.text, accountViewModel.account.myEmojis.value)
|
||||
|
||||
val message = message.text
|
||||
|
||||
if (nip17 || room.users.size > 1 || replyTo.value?.event is NIP17Group) {
|
||||
val replyHint = replyTo.value?.toEventHint<BaseDMGroupEvent>()
|
||||
|
||||
val template =
|
||||
if (replyHint == null) {
|
||||
ChatMessageEvent.build(message, room.users.map { LocalCache.getOrCreateUser(it).toPTag() }) {
|
||||
hashtags(findHashtags(message))
|
||||
references(findURLs(message))
|
||||
quotes(findNostrUris(message))
|
||||
|
||||
emojis(emojis)
|
||||
imetas(usedAttachments)
|
||||
}
|
||||
} else {
|
||||
ChatMessageEvent.reply(message, replyHint) {
|
||||
hashtags(findHashtags(message))
|
||||
references(findURLs(message))
|
||||
quotes(findNostrUris(message))
|
||||
|
||||
emojis(emojis)
|
||||
imetas(usedAttachments)
|
||||
}
|
||||
}
|
||||
|
||||
accountViewModel.account.sendNIP17PrivateMessage(template, dTag)
|
||||
} else {
|
||||
accountViewModel.account.sendPrivateMessage(
|
||||
message = message,
|
||||
toUser = room.users.first().let { LocalCache.getOrCreateUser(it).toPTag() },
|
||||
replyingTo = replyTo.value,
|
||||
contentWarningReason = null,
|
||||
imetas = usedAttachments,
|
||||
draftTag = dTag,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun findEmoji(
|
||||
message: String,
|
||||
myEmojiSet: List<Account.EmojiMedia>?,
|
||||
): List<EmojiUrlTag> {
|
||||
if (myEmojiSet == null) return emptyList()
|
||||
return CustomEmoji.findAllEmojiCodes(message).mapNotNull { possibleEmoji ->
|
||||
myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.url.url) }
|
||||
}
|
||||
}
|
||||
|
||||
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("")
|
||||
subject = TextFieldValue("")
|
||||
|
||||
replyTo.value = null
|
||||
|
||||
multiOrchestrator = null
|
||||
urlPreview = null
|
||||
isUploadingImage = false
|
||||
|
||||
wantsInvoice = false
|
||||
wantsZapraiser = false
|
||||
zapRaiserAmount = null
|
||||
|
||||
wantsForwardZapTo = false
|
||||
wantsToMarkAsSensitive = false
|
||||
|
||||
forwardZapTo = Split()
|
||||
forwardZapToEditting = TextFieldValue("")
|
||||
|
||||
userSuggestions.reset()
|
||||
userSuggestionsMainMessage = null
|
||||
|
||||
if (emojiSearch.value.isNotEmpty()) {
|
||||
emojiSearch.tryEmit("")
|
||||
}
|
||||
|
||||
draftTag = UUID.randomUUID().toString()
|
||||
|
||||
NostrSearchEventOrUserDataSource.clear()
|
||||
}
|
||||
|
||||
fun deleteDraft() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
accountViewModel?.deleteDraft(draftTag)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteMediaToUpload(selected: SelectedMediaProcessing) {
|
||||
this.multiOrchestrator?.remove(selected)
|
||||
}
|
||||
|
||||
open fun findUrlInMessage(): String? = RichTextParser().parseValidUrls(message.text).firstOrNull()
|
||||
|
||||
private fun saveDraft() {
|
||||
draftTextChanges.trySend("")
|
||||
}
|
||||
|
||||
open fun addToMessage(it: String) {
|
||||
updateMessage(TextFieldValue(message.text + " " + it))
|
||||
}
|
||||
|
||||
open fun updateMessage(newMessage: TextFieldValue) {
|
||||
message = newMessage
|
||||
urlPreview = findUrlInMessage()
|
||||
|
||||
if (newMessage.selection.collapsed) {
|
||||
val lastWord = newMessage.currentWord()
|
||||
|
||||
userSuggestionsMainMessage = UserSuggestionAnchor.MAIN_MESSAGE
|
||||
|
||||
accountViewModel?.let {
|
||||
userSuggestions.processCurrentWord(lastWord, it)
|
||||
}
|
||||
|
||||
if (lastWord.startsWith(":")) {
|
||||
emojiSearch.tryEmit(lastWord)
|
||||
} else {
|
||||
if (emojiSearch.value.isNotBlank()) {
|
||||
emojiSearch.tryEmit("")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
saveDraft()
|
||||
}
|
||||
|
||||
open fun updateToUsers(newToUsersValue: TextFieldValue) {
|
||||
toUsers = newToUsersValue
|
||||
|
||||
if (newToUsersValue.selection.collapsed) {
|
||||
val lastWord = newToUsersValue.currentWord()
|
||||
userSuggestionsMainMessage = UserSuggestionAnchor.TO_USERS
|
||||
|
||||
accountViewModel?.let {
|
||||
userSuggestions.processCurrentWord(lastWord, it)
|
||||
}
|
||||
}
|
||||
saveDraft()
|
||||
}
|
||||
|
||||
open fun updateSubject(it: TextFieldValue) {
|
||||
subject = it
|
||||
saveDraft()
|
||||
}
|
||||
|
||||
open fun updateZapForwardTo(newZapForwardTo: TextFieldValue) {
|
||||
forwardZapToEditting = newZapForwardTo
|
||||
if (newZapForwardTo.selection.collapsed) {
|
||||
val lastWord = newZapForwardTo.text
|
||||
userSuggestionsMainMessage = UserSuggestionAnchor.FORWARD_ZAPS
|
||||
accountViewModel?.let {
|
||||
userSuggestions.processCurrentWord(lastWord, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open fun autocompleteWithUser(item: User) {
|
||||
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
|
||||
val lastWord = message.currentWord()
|
||||
message = userSuggestions.replaceCurrentWord(message, lastWord, item)
|
||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
||||
forwardZapTo.addItem(item)
|
||||
forwardZapToEditting = TextFieldValue("")
|
||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.TO_USERS) {
|
||||
val lastWord = toUsers.currentWord()
|
||||
toUsers = userSuggestions.replaceCurrentWord(toUsers, lastWord, item)
|
||||
|
||||
val relayList = (LocalCache.getAddressableNoteIfExists(AdvertisedRelayListEvent.createAddressTag(item.pubkeyHex))?.event as? AdvertisedRelayListEvent)?.readRelays()
|
||||
nip17 = relayList != null
|
||||
}
|
||||
|
||||
userSuggestionsMainMessage = null
|
||||
userSuggestions.reset()
|
||||
|
||||
saveDraft()
|
||||
}
|
||||
|
||||
open fun autocompleteWithEmoji(item: Account.EmojiMedia) {
|
||||
val wordToInsert = ":${item.code}:"
|
||||
message = message.replaceCurrentWord(wordToInsert)
|
||||
|
||||
emojiSearch.tryEmit("")
|
||||
|
||||
saveDraft()
|
||||
}
|
||||
|
||||
open fun autocompleteWithEmojiUrl(item: Account.EmojiMedia) {
|
||||
val wordToInsert = item.url.url + " "
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
iMetaAttachments.downloadAndPrepare(
|
||||
item.url.url,
|
||||
accountViewModel?.account?.shouldUseTorForImageDownload() ?: false,
|
||||
)
|
||||
}
|
||||
|
||||
message = message.replaceCurrentWord(wordToInsert)
|
||||
|
||||
emojiSearch.tryEmit("")
|
||||
|
||||
urlPreview = findUrlInMessage()
|
||||
|
||||
saveDraft()
|
||||
}
|
||||
|
||||
private fun newStateMapPollOptions(): SnapshotStateMap<Int, String> = mutableStateMapOf(Pair(0, ""), Pair(1, ""))
|
||||
|
||||
fun canPost(): Boolean =
|
||||
message.text.isNotBlank() &&
|
||||
!isUploadingImage &&
|
||||
!wantsInvoice &&
|
||||
(!wantsZapraiser || zapRaiserAmount != null) &&
|
||||
(toUsers.text.isNotBlank()) &&
|
||||
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}")
|
||||
}
|
||||
|
||||
fun toggleNIP04And24() {
|
||||
if (requiresNIP17) {
|
||||
nip17 = true
|
||||
} else {
|
||||
nip17 = !nip17
|
||||
}
|
||||
if (message.text.isNotBlank()) {
|
||||
saveDraft()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateZapPercentage(
|
||||
index: Int,
|
||||
sliderValue: Float,
|
||||
) {
|
||||
forwardZapTo.updatePercentage(index, sliderValue)
|
||||
}
|
||||
|
||||
fun updateZapFromText() {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), null, accountViewModel!!)
|
||||
tagger.run()
|
||||
tagger.pTags?.forEach { taggedUser ->
|
||||
if (!forwardZapTo.items.any { it.key == taggedUser }) {
|
||||
forwardZapTo.addItem(taggedUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateZapRaiserAmount(newAmount: Long?) {
|
||||
zapRaiserAmount = newAmount
|
||||
saveDraft()
|
||||
}
|
||||
|
||||
fun toggleMarkAsSensitive() {
|
||||
wantsToMarkAsSensitive = !wantsToMarkAsSensitive
|
||||
saveDraft()
|
||||
}
|
||||
}
|
||||
+1
-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.chatrooms
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.private
|
||||
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.Row
|
||||
+19
-255
@@ -18,25 +18,16 @@
|
||||
* 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.chatrooms
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.private
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -44,17 +35,13 @@ import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
@@ -80,26 +67,15 @@ import com.vitorpamplona.amethyst.ui.note.timeAgoShort
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderEncryptedFile
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.ChatBubbleMaxSizeModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.ChatBubbleShapeMe
|
||||
import com.vitorpamplona.amethyst.ui.theme.ChatBubbleShapeThem
|
||||
import com.vitorpamplona.amethyst.ui.theme.ChatPaddingInnerQuoteModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.ChatPaddingModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font12SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfHalfVertPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.ReactionRowHeightChat
|
||||
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing
|
||||
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size18Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size5Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.chatAuthorBox
|
||||
import com.vitorpamplona.amethyst.ui.theme.chatBackground
|
||||
import com.vitorpamplona.amethyst.ui.theme.incognitoIconModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.messageBubbleLimits
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists
|
||||
@@ -188,6 +164,7 @@ fun NormalChatNote(
|
||||
|
||||
ChatBubbleLayout(
|
||||
isLoggedInUser = isLoggedInUser,
|
||||
isDraft = note.event is DraftEvent,
|
||||
innerQuote = innerQuote,
|
||||
isComplete = accountViewModel.settings.featureSet == FeatureSetType.COMPLETE,
|
||||
hasDetailsToShow = note.zaps.isNotEmpty() || note.zapPayments.isNotEmpty() || note.reactions.isNotEmpty(),
|
||||
@@ -223,27 +200,29 @@ fun NormalChatNote(
|
||||
)
|
||||
},
|
||||
detailRow = {
|
||||
if (note.isDraft()) {
|
||||
DisplayDraftChat()
|
||||
}
|
||||
IncognitoBadge(note)
|
||||
ChatTimeAgo(note)
|
||||
RelayBadgesHorizontal(note, accountViewModel, nav = nav)
|
||||
|
||||
Spacer(modifier = DoubleHorzSpacer)
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = RowColSpacing) {
|
||||
ReplyReaction(
|
||||
baseNote = note,
|
||||
grayTint = MaterialTheme.colorScheme.placeholderText,
|
||||
accountViewModel = accountViewModel,
|
||||
showCounter = false,
|
||||
iconSizeModifier = Size18Modifier,
|
||||
) {
|
||||
onWantsToReply(note)
|
||||
}
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
LikeReaction(note, MaterialTheme.colorScheme.placeholderText, accountViewModel, nav)
|
||||
if (!note.isDraft()) {
|
||||
ReplyReaction(
|
||||
baseNote = note,
|
||||
grayTint = MaterialTheme.colorScheme.placeholderText,
|
||||
accountViewModel = accountViewModel,
|
||||
showCounter = false,
|
||||
iconSizeModifier = Size18Modifier,
|
||||
) {
|
||||
onWantsToReply(note)
|
||||
}
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
LikeReaction(note, MaterialTheme.colorScheme.placeholderText, accountViewModel, nav)
|
||||
|
||||
ZapReaction(note, MaterialTheme.colorScheme.placeholderText, accountViewModel, nav = nav)
|
||||
ZapReaction(note, MaterialTheme.colorScheme.placeholderText, accountViewModel, nav = nav)
|
||||
} else {
|
||||
DisplayDraftChat()
|
||||
}
|
||||
}
|
||||
},
|
||||
) { backgroundBubbleColor ->
|
||||
@@ -260,221 +239,6 @@ fun NormalChatNote(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun ChatBubbleLayout(
|
||||
isLoggedInUser: 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 backgroundBubbleColor =
|
||||
remember {
|
||||
if (isLoggedInUser) {
|
||||
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() {
|
||||
val backgroundBubbleColor =
|
||||
remember {
|
||||
mutableStateOf<Color>(Color.Transparent)
|
||||
}
|
||||
|
||||
Column {
|
||||
ChatBubbleLayout(
|
||||
isLoggedInUser = false,
|
||||
innerQuote = false,
|
||||
isComplete = true,
|
||||
hasDetailsToShow = true,
|
||||
drawAuthorInfo = true,
|
||||
parentBackgroundColor = backgroundBubbleColor,
|
||||
onClick = { false },
|
||||
onAuthorClick = {},
|
||||
actionMenu = { onDismiss ->
|
||||
},
|
||||
drawAuthorLine = {
|
||||
UserDisplayNameLayout(
|
||||
picture = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Person,
|
||||
contentDescription = null,
|
||||
modifier =
|
||||
Modifier
|
||||
.size(Size20dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color.LightGray),
|
||||
)
|
||||
},
|
||||
name = {
|
||||
Text("Someone else", fontWeight = FontWeight.Bold)
|
||||
},
|
||||
)
|
||||
},
|
||||
detailRow = { Text("Relays and Actions") },
|
||||
) { backgroundBubbleColor ->
|
||||
Text("This is my note")
|
||||
}
|
||||
|
||||
ChatBubbleLayout(
|
||||
isLoggedInUser = true,
|
||||
innerQuote = false,
|
||||
isComplete = true,
|
||||
hasDetailsToShow = true,
|
||||
drawAuthorInfo = true,
|
||||
parentBackgroundColor = backgroundBubbleColor,
|
||||
onClick = { false },
|
||||
onAuthorClick = {},
|
||||
actionMenu = { onDismiss ->
|
||||
},
|
||||
drawAuthorLine = {
|
||||
UserDisplayNameLayout(
|
||||
picture = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Person,
|
||||
contentDescription = null,
|
||||
modifier =
|
||||
Modifier
|
||||
.size(Size20dp)
|
||||
.clip(CircleShape),
|
||||
)
|
||||
},
|
||||
name = {
|
||||
Text("Me", fontWeight = FontWeight.Bold)
|
||||
},
|
||||
)
|
||||
},
|
||||
detailRow = { Text("Relays and Actions") },
|
||||
) { backgroundBubbleColor ->
|
||||
Text("This is a very long long loong note")
|
||||
}
|
||||
|
||||
ChatBubbleLayout(
|
||||
isLoggedInUser = true,
|
||||
innerQuote = false,
|
||||
isComplete = false,
|
||||
hasDetailsToShow = false,
|
||||
drawAuthorInfo = false,
|
||||
parentBackgroundColor = backgroundBubbleColor,
|
||||
onClick = { false },
|
||||
onAuthorClick = {},
|
||||
actionMenu = { onDismiss ->
|
||||
},
|
||||
drawAuthorLine = {
|
||||
UserDisplayNameLayout(
|
||||
picture = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Person,
|
||||
contentDescription = null,
|
||||
modifier =
|
||||
Modifier
|
||||
.size(Size20dp)
|
||||
.clip(CircleShape),
|
||||
)
|
||||
},
|
||||
name = {
|
||||
Text("Me", fontWeight = FontWeight.Bold)
|
||||
},
|
||||
)
|
||||
},
|
||||
detailRow = { Text("Relays and Actions") },
|
||||
) { backgroundBubbleColor ->
|
||||
Text("Short note")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MessageBubbleLines(
|
||||
baseNote: Note,
|
||||
+64
-105
@@ -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.chatrooms
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.private
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -53,7 +53,6 @@ import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -80,11 +79,9 @@ 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.Note
|
||||
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.NewPostViewModel
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||
@@ -106,6 +103,11 @@ 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.chatlist.DisplayRoomSubject
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.DisplayUserSetAsSubject
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.LoadUser
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.utils.DisplayReplyingToNote
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.utils.MyTextField
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BottomTopHeight
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
@@ -119,18 +121,10 @@ 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.tags.hashtags.hashtags
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.references.references
|
||||
import com.vitorpamplona.quartz.nip10Notes.content.findHashtags
|
||||
import com.vitorpamplona.quartz.nip10Notes.content.findNostrUris
|
||||
import com.vitorpamplona.quartz.nip10Notes.content.findURLs
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group
|
||||
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.messages.changeSubject
|
||||
import com.vitorpamplona.quartz.nip18Reposts.quotes.quotes
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.emojis
|
||||
import com.vitorpamplona.quartz.nip92IMeta.imetas
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
@@ -144,6 +138,8 @@ import kotlinx.coroutines.launch
|
||||
fun ChatroomScreen(
|
||||
roomId: String?,
|
||||
draftMessage: String? = null,
|
||||
replyToNote: HexKey? = null,
|
||||
editFromDraft: HexKey? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
@@ -157,7 +153,7 @@ fun ChatroomScreen(
|
||||
accountViewModel = accountViewModel,
|
||||
) {
|
||||
Column(Modifier.padding(it)) {
|
||||
Chatroom(roomId, draftMessage, accountViewModel, nav)
|
||||
Chatroom(roomId, draftMessage, replyToNote, editFromDraft, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -238,6 +234,8 @@ private fun RenderRoomTopBar(
|
||||
fun Chatroom(
|
||||
roomId: String?,
|
||||
draftMessage: String? = null,
|
||||
replyToNote: HexKey? = null,
|
||||
editFromDraft: HexKey? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
@@ -248,6 +246,8 @@ fun Chatroom(
|
||||
PrepareChatroomViewModels(
|
||||
room = it,
|
||||
draftMessage = draftMessage,
|
||||
replyToNote = replyToNote,
|
||||
editFromDraft = editFromDraft,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
@@ -306,6 +306,8 @@ fun ChatroomByAuthor(
|
||||
PrepareChatroomViewModels(
|
||||
room = it,
|
||||
draftMessage = draftMessage,
|
||||
replyToNote = null,
|
||||
editFromDraft = null,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
@@ -356,6 +358,8 @@ fun LoadRoomByAuthor(
|
||||
fun PrepareChatroomViewModels(
|
||||
room: ChatroomKey,
|
||||
draftMessage: String?,
|
||||
replyToNote: HexKey? = null,
|
||||
editFromDraft: HexKey? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
@@ -369,29 +373,46 @@ fun PrepareChatroomViewModels(
|
||||
),
|
||||
)
|
||||
|
||||
val newPostModel: NewPostViewModel = viewModel()
|
||||
newPostModel.accountViewModel = accountViewModel
|
||||
newPostModel.account = accountViewModel.account
|
||||
newPostModel.requiresNIP17 = room.users.size > 1
|
||||
val newPostModel: ChatNewMessageViewModel = viewModel()
|
||||
newPostModel.init(accountViewModel)
|
||||
newPostModel.load(room)
|
||||
|
||||
if (newPostModel.requiresNIP17) {
|
||||
newPostModel.nip17 = true
|
||||
} else {
|
||||
if (room.users.size == 1) {
|
||||
ObserveRelayListForDMs(pubkey = room.users.first(), accountViewModel = accountViewModel) {
|
||||
if (it?.relays().isNullOrEmpty()) {
|
||||
newPostModel.nip17 = false
|
||||
} else {
|
||||
newPostModel.nip17 = true
|
||||
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)) }
|
||||
LaunchedEffect(key1 = draftMessage) {
|
||||
newPostModel.updateMessage(TextFieldValue(draftMessage))
|
||||
}
|
||||
}
|
||||
|
||||
ChatroomScreen(
|
||||
@@ -408,7 +429,7 @@ fun PrepareChatroomViewModels(
|
||||
fun ChatroomScreen(
|
||||
room: ChatroomKey,
|
||||
feedViewModel: NostrChatroomFeedViewModel,
|
||||
newPostModel: NewPostViewModel,
|
||||
newPostModel: ChatNewMessageViewModel,
|
||||
fileUpload: ChatFileUploadModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
@@ -444,7 +465,6 @@ fun ChatroomScreen(
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
val replyTo = remember { mutableStateOf<Note?>(null) }
|
||||
ObserveRelayListForDMsAndDisplayIfNotFound(accountViewModel, nav)
|
||||
|
||||
Column(
|
||||
@@ -460,18 +480,14 @@ fun ChatroomScreen(
|
||||
nav = nav,
|
||||
routeForLastRead = "Room/${room.hashCode()}",
|
||||
avoidDraft = newPostModel.draftTag,
|
||||
onWantsToReply = {
|
||||
replyTo.value = it
|
||||
},
|
||||
onWantsToEditDraft = {
|
||||
newPostModel.load(accountViewModel, null, null, null, null, it)
|
||||
},
|
||||
onWantsToReply = newPostModel::reply,
|
||||
onWantsToEditDraft = newPostModel::editFromDraft,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
replyTo.value?.let { DisplayReplyingToNote(it, accountViewModel, nav) { replyTo.value = null } }
|
||||
newPostModel.replyTo.value?.let { DisplayReplyingToNote(it, accountViewModel, nav) { newPostModel.replyTo.value = null } }
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
@@ -481,7 +497,7 @@ fun ChatroomScreen(
|
||||
.receiveAsFlow()
|
||||
.debounce(1000)
|
||||
.collectLatest {
|
||||
innerSendPost(newPostModel, room, replyTo, accountViewModel, newPostModel.draftTag)
|
||||
newPostModel.sendDraft()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -501,85 +517,29 @@ fun ChatroomScreen(
|
||||
accountViewModel,
|
||||
onSendNewMessage = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
innerSendPost(newPostModel, room, replyTo, accountViewModel, null)
|
||||
|
||||
accountViewModel.deleteDraft(newPostModel.draftTag)
|
||||
|
||||
newPostModel.message = TextFieldValue("")
|
||||
|
||||
replyTo.value = null
|
||||
newPostModel.sendPostSync()
|
||||
feedViewModel.sendToTop()
|
||||
}
|
||||
},
|
||||
onSendNewMedia = {
|
||||
fileUpload.load(it, room, accountViewModel.account)
|
||||
onSendNewMedia = { list, isNip17 ->
|
||||
fileUpload.load(list, room, isNip17, accountViewModel.account)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun innerSendPost(
|
||||
newPostModel: NewPostViewModel,
|
||||
room: ChatroomKey,
|
||||
replyTo: MutableState<Note?>,
|
||||
accountViewModel: AccountViewModel,
|
||||
dTag: String?,
|
||||
) {
|
||||
val urls = findURLs(newPostModel.message.text)
|
||||
val usedAttachments = newPostModel.iMetaAttachments.filter { it.url !in urls.toSet() }
|
||||
val emojis = newPostModel.findEmoji(newPostModel.message.text, accountViewModel.account.myEmojis.value)
|
||||
|
||||
val message = newPostModel.message.text
|
||||
|
||||
if (newPostModel.nip17 || room.users.size > 1 || replyTo.value?.event is NIP17Group) {
|
||||
val replyHint = replyTo.value?.toEventHint<ChatMessageEvent>()
|
||||
val template =
|
||||
if (replyHint == null) {
|
||||
ChatMessageEvent.build(message, room.users.map { LocalCache.getOrCreateUser(it).toPTag() }) {
|
||||
hashtags(findHashtags(message))
|
||||
references(findURLs(message))
|
||||
quotes(findNostrUris(message))
|
||||
|
||||
emojis(emojis)
|
||||
imetas(usedAttachments)
|
||||
}
|
||||
} else {
|
||||
ChatMessageEvent.reply(message, replyHint) {
|
||||
hashtags(findHashtags(message))
|
||||
references(findURLs(message))
|
||||
quotes(findNostrUris(message))
|
||||
|
||||
emojis(emojis)
|
||||
imetas(usedAttachments)
|
||||
}
|
||||
}
|
||||
|
||||
accountViewModel.account.sendNIP17PrivateMessage(template, dTag)
|
||||
} else {
|
||||
accountViewModel.account.sendPrivateMessage(
|
||||
message = newPostModel.message.text,
|
||||
toUser = room.users.first().let { LocalCache.getOrCreateUser(it).toPTag() },
|
||||
replyingTo = replyTo.value,
|
||||
mentions = null,
|
||||
contentWarningReason = null,
|
||||
imetas = usedAttachments,
|
||||
draftTag = dTag,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PrivateMessageEditFieldRow(
|
||||
channelScreenModel: NewPostViewModel,
|
||||
channelScreenModel: ChatNewMessageViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
onSendNewMessage: () -> Unit,
|
||||
onSendNewMedia: (ImmutableList<SelectedMedia>) -> Unit,
|
||||
onSendNewMedia: (ImmutableList<SelectedMedia>, isNip17: Boolean) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = EditFieldModifier,
|
||||
) {
|
||||
ShowUserSuggestionList(
|
||||
channelScreenModel.userSuggestions,
|
||||
channelScreenModel.userSuggestions.userSuggestions,
|
||||
channelScreenModel::autocompleteWithUser,
|
||||
accountViewModel,
|
||||
)
|
||||
@@ -623,11 +583,10 @@ fun PrivateMessageEditFieldRow(
|
||||
SelectFromGallery(
|
||||
isUploading = channelScreenModel.isUploadingImage,
|
||||
tint = MaterialTheme.colorScheme.placeholderText,
|
||||
modifier =
|
||||
Modifier
|
||||
.size(30.dp)
|
||||
.padding(start = 2.dp),
|
||||
onImageChosen = onSendNewMedia,
|
||||
modifier = Modifier.size(30.dp).padding(start = 2.dp),
|
||||
onImageChosen = {
|
||||
onSendNewMedia(it, channelScreenModel.nip17)
|
||||
},
|
||||
)
|
||||
|
||||
var wantsToActivateNIP17 by remember { mutableStateOf(false) }
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* 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.chatlist.private
|
||||
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.vitorpamplona.amethyst.service.uploads.FileHeader
|
||||
import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
|
||||
import com.vitorpamplona.quartz.nip92IMeta.IMetaTag
|
||||
import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder
|
||||
import com.vitorpamplona.quartz.nip92IMeta.imetaTagBuilder
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.alt
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.blurhash
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.dims
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.hash
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.magnet
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.mimeType
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.originalHash
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.sensitiveContent
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.size
|
||||
import java.util.Locale
|
||||
|
||||
class IMetaAttachments {
|
||||
var iMetaAttachments by mutableStateOf<List<IMetaTag>>(emptyList())
|
||||
|
||||
suspend fun downloadAndPrepare(
|
||||
url: String,
|
||||
forceProxy: Boolean,
|
||||
) {
|
||||
val fileExtension: String = MimeTypeMap.getFileExtensionFromUrl(url)
|
||||
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension.lowercase(Locale.getDefault()))
|
||||
|
||||
val imeta =
|
||||
FileHeader.prepare(url, mimeType, null, forceProxy).getOrNull()?.let {
|
||||
IMetaTagBuilder(url)
|
||||
.apply {
|
||||
hash(it.hash)
|
||||
size(it.size)
|
||||
it.mimeType?.let { mimeType(it) }
|
||||
it.dim?.let { dims(it) }
|
||||
it.blurHash?.let { blurhash(it.blurhash) }
|
||||
}.build()
|
||||
}
|
||||
|
||||
if (imeta != null) {
|
||||
iMetaAttachments += imeta
|
||||
}
|
||||
}
|
||||
|
||||
fun remove(url: String) {
|
||||
iMetaAttachments = iMetaAttachments.filter { it.url != url }
|
||||
}
|
||||
|
||||
fun replace(
|
||||
url: String,
|
||||
iMeta: IMetaTag,
|
||||
) {
|
||||
iMetaAttachments = iMetaAttachments.filter { it.url != url } + iMeta
|
||||
}
|
||||
|
||||
fun add(
|
||||
result: UploadOrchestrator.OrchestratorResult.ServerResult,
|
||||
alt: String?,
|
||||
contentWarningReason: String?,
|
||||
) {
|
||||
val iMeta =
|
||||
imetaTagBuilder(result.url) {
|
||||
hash(result.fileHeader.hash)
|
||||
size(result.fileHeader.size)
|
||||
result.fileHeader.mimeType?.let { mimeType(it) }
|
||||
result.fileHeader.dim?.let { dims(it) }
|
||||
result.fileHeader.blurHash?.let { blurhash(it.blurhash) }
|
||||
result.magnet?.let { magnet(it) }
|
||||
result.uploadedHash?.let { originalHash(it) }
|
||||
|
||||
alt?.let { alt(it) }
|
||||
contentWarningReason?.let { sensitiveContent(contentWarningReason) }
|
||||
}
|
||||
|
||||
replace(iMeta.url, iMeta)
|
||||
}
|
||||
|
||||
fun filterIsIn(urls: Set<String>) = iMetaAttachments.filter { it.url !in urls }
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 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.chatlist.private
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class UserSuggestions {
|
||||
var userSuggestions by mutableStateOf<List<User>>(emptyList())
|
||||
|
||||
fun reset() {
|
||||
userSuggestions = emptyList()
|
||||
}
|
||||
|
||||
fun processCurrentWord(
|
||||
word: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
if (word.startsWith("@") && word.length > 2) {
|
||||
val prefix = word.removePrefix("@")
|
||||
NostrSearchEventOrUserDataSource.search(prefix)
|
||||
accountViewModel.viewModelScope.launch(Dispatchers.IO) {
|
||||
userSuggestions = accountViewModel.findUsersStartingWithSync(prefix)
|
||||
}
|
||||
} else {
|
||||
NostrSearchEventOrUserDataSource.clear()
|
||||
userSuggestions = emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun replaceCurrentWord(
|
||||
message: TextFieldValue,
|
||||
word: String,
|
||||
item: User,
|
||||
): TextFieldValue {
|
||||
val lastWordStart = message.selection.end - word.length
|
||||
val wordToInsert = "@${item.pubkeyNpub()}"
|
||||
|
||||
return TextFieldValue(
|
||||
message.text.replaceRange(lastWordStart, message.selection.end, wordToInsert),
|
||||
TextRange(lastWordStart + wordToInsert.length, lastWordStart + wordToInsert.length),
|
||||
)
|
||||
}
|
||||
}
|
||||
+5
-175
@@ -18,19 +18,14 @@
|
||||
* 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.chatrooms
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.public
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
@@ -38,30 +33,20 @@ import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Cancel
|
||||
import androidx.compose.material.icons.filled.EditNote
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextFieldColors
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
@@ -71,25 +56,19 @@ 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.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
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.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -141,6 +120,10 @@ 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.chatlist.private.RefreshingChatroomFeedView
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.private.ThinSendButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.utils.DisplayReplyingToNote
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.utils.MyTextField
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CrossfadeCheckIfVideoIsOnline
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
@@ -501,52 +484,6 @@ private suspend fun innerSendPost(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayReplyingToNote(
|
||||
replyingNote: Note?,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
onCancel: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
Modifier
|
||||
.padding(horizontal = 10.dp)
|
||||
.heightIn(max = 100.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.animateContentSize(),
|
||||
) {
|
||||
if (replyingNote != null) {
|
||||
Column(remember { Modifier.weight(1f) }) {
|
||||
ChatroomMessageCompose(
|
||||
baseNote = replyingNote,
|
||||
null,
|
||||
innerQuote = true,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
onWantsToReply = {},
|
||||
onWantsToEditDraft = {},
|
||||
)
|
||||
}
|
||||
|
||||
Column(Modifier.padding(start = 5.dp)) {
|
||||
IconButton(
|
||||
modifier = Modifier.size(20.dp),
|
||||
onClick = onCancel,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Cancel,
|
||||
null,
|
||||
modifier =
|
||||
Modifier
|
||||
.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EditFieldRow(
|
||||
channelScreenModel: NewPostViewModel,
|
||||
@@ -624,113 +561,6 @@ fun EditFieldRow(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MyTextField(
|
||||
value: TextFieldValue,
|
||||
onValueChange: (TextFieldValue) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
readOnly: Boolean = false,
|
||||
textStyle: TextStyle = LocalTextStyle.current,
|
||||
label: @Composable (() -> Unit)? = null,
|
||||
placeholder: @Composable (() -> Unit)? = null,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
prefix: @Composable (() -> Unit)? = null,
|
||||
suffix: @Composable (() -> Unit)? = null,
|
||||
supportingText: @Composable (() -> Unit)? = null,
|
||||
isError: Boolean = false,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
singleLine: Boolean = false,
|
||||
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
|
||||
minLines: Int = 1,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
shape: Shape = TextFieldDefaults.shape,
|
||||
colors: TextFieldColors = TextFieldDefaults.colors(),
|
||||
contentPadding: PaddingValues =
|
||||
if (label == null) {
|
||||
TextFieldDefaults.contentPaddingWithoutLabel(
|
||||
start = 10.dp,
|
||||
top = 12.dp,
|
||||
end = 10.dp,
|
||||
bottom = 12.dp,
|
||||
)
|
||||
} else {
|
||||
TextFieldDefaults.contentPaddingWithLabel(
|
||||
start = 10.dp,
|
||||
top = 12.dp,
|
||||
end = 10.dp,
|
||||
bottom = 12.dp,
|
||||
)
|
||||
},
|
||||
) {
|
||||
// COPIED FROM TEXT FIELD
|
||||
// The only change is the contentPadding below
|
||||
val textColor =
|
||||
textStyle.color.takeOrElse {
|
||||
val focused by interactionSource.collectIsFocusedAsState()
|
||||
|
||||
val targetValue =
|
||||
when {
|
||||
!enabled -> MaterialTheme.colorScheme.placeholderText
|
||||
isError -> MaterialTheme.colorScheme.onSurface
|
||||
focused -> MaterialTheme.colorScheme.onSurface
|
||||
else -> MaterialTheme.colorScheme.onSurface
|
||||
}
|
||||
|
||||
rememberUpdatedState(targetValue).value
|
||||
}
|
||||
val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))
|
||||
|
||||
CompositionLocalProvider(LocalTextSelectionColors provides LocalTextSelectionColors.current) {
|
||||
BasicTextField(
|
||||
value = value,
|
||||
modifier =
|
||||
modifier.defaultMinSize(
|
||||
minWidth = TextFieldDefaults.MinWidth,
|
||||
minHeight = 36.dp,
|
||||
),
|
||||
onValueChange = onValueChange,
|
||||
enabled = enabled,
|
||||
readOnly = readOnly,
|
||||
textStyle = mergedTextStyle,
|
||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
||||
visualTransformation = visualTransformation,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
interactionSource = interactionSource,
|
||||
singleLine = singleLine,
|
||||
maxLines = maxLines,
|
||||
minLines = minLines,
|
||||
decorationBox =
|
||||
@Composable { innerTextField ->
|
||||
TextFieldDefaults.DecorationBox(
|
||||
value = value.text,
|
||||
visualTransformation = visualTransformation,
|
||||
innerTextField = innerTextField,
|
||||
placeholder = placeholder,
|
||||
label = label,
|
||||
leadingIcon = leadingIcon,
|
||||
trailingIcon = trailingIcon,
|
||||
prefix = prefix,
|
||||
suffix = suffix,
|
||||
supportingText = supportingText,
|
||||
shape = shape,
|
||||
singleLine = singleLine,
|
||||
enabled = enabled,
|
||||
isError = isError,
|
||||
interactionSource = interactionSource,
|
||||
colors = colors,
|
||||
contentPadding = contentPadding,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderChannelHeader(
|
||||
channelNote: Note,
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* 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.chatlist.utils
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Cancel
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
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.chatlist.private.ChatroomMessageCompose
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
|
||||
@Composable
|
||||
fun DisplayReplyingToNote(
|
||||
replyingNote: Note?,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
onCancel: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
Modifier
|
||||
.padding(horizontal = 10.dp)
|
||||
.heightIn(max = 100.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.animateContentSize(),
|
||||
) {
|
||||
if (replyingNote != null) {
|
||||
Column(remember { Modifier.weight(1f) }) {
|
||||
ChatroomMessageCompose(
|
||||
baseNote = replyingNote,
|
||||
null,
|
||||
innerQuote = true,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
onWantsToReply = {},
|
||||
onWantsToEditDraft = {},
|
||||
)
|
||||
}
|
||||
|
||||
Column(Modifier.padding(start = 5.dp)) {
|
||||
IconButton(
|
||||
modifier = Size20Modifier,
|
||||
onClick = onCancel,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Cancel,
|
||||
null,
|
||||
modifier = Size20Modifier,
|
||||
tint = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* 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.chatlist.utils
|
||||
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.TextFieldColors
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MyTextField(
|
||||
value: TextFieldValue,
|
||||
onValueChange: (TextFieldValue) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
readOnly: Boolean = false,
|
||||
textStyle: TextStyle = LocalTextStyle.current,
|
||||
label: @Composable (() -> Unit)? = null,
|
||||
placeholder: @Composable (() -> Unit)? = null,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
prefix: @Composable (() -> Unit)? = null,
|
||||
suffix: @Composable (() -> Unit)? = null,
|
||||
supportingText: @Composable (() -> Unit)? = null,
|
||||
isError: Boolean = false,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
singleLine: Boolean = false,
|
||||
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
|
||||
minLines: Int = 1,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
shape: Shape = TextFieldDefaults.shape,
|
||||
colors: TextFieldColors = TextFieldDefaults.colors(),
|
||||
contentPadding: PaddingValues =
|
||||
if (label == null) {
|
||||
TextFieldDefaults.contentPaddingWithoutLabel(
|
||||
start = 10.dp,
|
||||
top = 12.dp,
|
||||
end = 10.dp,
|
||||
bottom = 12.dp,
|
||||
)
|
||||
} else {
|
||||
TextFieldDefaults.contentPaddingWithLabel(
|
||||
start = 10.dp,
|
||||
top = 12.dp,
|
||||
end = 10.dp,
|
||||
bottom = 12.dp,
|
||||
)
|
||||
},
|
||||
) {
|
||||
// COPIED FROM TEXT FIELD
|
||||
// The only change is the contentPadding below
|
||||
val textColor =
|
||||
textStyle.color.takeOrElse {
|
||||
val focused by interactionSource.collectIsFocusedAsState()
|
||||
|
||||
val targetValue =
|
||||
when {
|
||||
!enabled -> MaterialTheme.colorScheme.placeholderText
|
||||
isError -> MaterialTheme.colorScheme.onSurface
|
||||
focused -> MaterialTheme.colorScheme.onSurface
|
||||
else -> MaterialTheme.colorScheme.onSurface
|
||||
}
|
||||
|
||||
rememberUpdatedState(targetValue).value
|
||||
}
|
||||
val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))
|
||||
|
||||
CompositionLocalProvider(LocalTextSelectionColors provides LocalTextSelectionColors.current) {
|
||||
BasicTextField(
|
||||
value = value,
|
||||
modifier =
|
||||
modifier.defaultMinSize(
|
||||
minWidth = TextFieldDefaults.MinWidth,
|
||||
minHeight = 36.dp,
|
||||
),
|
||||
onValueChange = onValueChange,
|
||||
enabled = enabled,
|
||||
readOnly = readOnly,
|
||||
textStyle = mergedTextStyle,
|
||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
||||
visualTransformation = visualTransformation,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
interactionSource = interactionSource,
|
||||
singleLine = singleLine,
|
||||
maxLines = maxLines,
|
||||
minLines = minLines,
|
||||
decorationBox =
|
||||
@Composable { innerTextField ->
|
||||
TextFieldDefaults.DecorationBox(
|
||||
value = value.text,
|
||||
visualTransformation = visualTransformation,
|
||||
innerTextField = innerTextField,
|
||||
placeholder = placeholder,
|
||||
label = label,
|
||||
leadingIcon = leadingIcon,
|
||||
trailingIcon = trailingIcon,
|
||||
prefix = prefix,
|
||||
suffix = suffix,
|
||||
supportingText = supportingText,
|
||||
shape = shape,
|
||||
singleLine = singleLine,
|
||||
enabled = enabled,
|
||||
isError = isError,
|
||||
interactionSource = interactionSource,
|
||||
colors = colors,
|
||||
contentPadding = contentPadding,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
+2
-5
@@ -50,8 +50,6 @@ import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size16Modifier
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceSuccessResponse
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun DisplayLNAddress(
|
||||
@@ -71,9 +69,8 @@ fun DisplayLNAddress(
|
||||
title = stringRes(id = R.string.error_dialog_zap_error),
|
||||
textContent = showErrorMessageDialog ?: "",
|
||||
onClickStartMessage = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val route = routeToMessage(userHex, showErrorMessageDialog, accountViewModel)
|
||||
nav.nav(route)
|
||||
nav.nav {
|
||||
routeToMessage(userHex, showErrorMessageDialog, accountViewModel = accountViewModel)
|
||||
}
|
||||
},
|
||||
onDismiss = { showErrorMessageDialog = null },
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@ import com.vitorpamplona.amethyst.ui.note.UserCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.ObserveRelayListForSearchAndDisplayIfNotFound
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.DisappearingScaffold
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ChannelName
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.ChannelName
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
|
||||
+9
-8
@@ -52,7 +52,6 @@ import androidx.compose.runtime.State
|
||||
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
|
||||
@@ -148,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.chatrooms.ChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ThinSendButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.private.ThinSendButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.public.ChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
@@ -213,7 +212,6 @@ import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent
|
||||
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@@ -858,7 +856,6 @@ private fun RenderClassifiedsReaderForThread(
|
||||
}
|
||||
|
||||
var message by remember { mutableStateOf(TextFieldValue(msg)) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
TextField(
|
||||
value = message,
|
||||
@@ -881,9 +878,13 @@ private fun RenderClassifiedsReaderForThread(
|
||||
isActive = message.text.isNotBlank(),
|
||||
modifier = EditFieldTrailingIconModifier,
|
||||
) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
note.author?.let {
|
||||
nav.nav(routeToMessage(it, note.toNostrUri() + "\n\n" + msg, accountViewModel))
|
||||
note.author?.let {
|
||||
nav.nav {
|
||||
routeToMessage(
|
||||
it,
|
||||
note.toNostrUri() + "\n\n" + msg,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,7 +266,7 @@ val chatAuthorImage = Modifier.size(20.dp).clip(shape = CircleShape)
|
||||
val AuthorInfoVideoFeed = Modifier.width(75.dp).padding(end = 15.dp)
|
||||
|
||||
val messageDetailsModifier = Modifier.height(Size25dp)
|
||||
val messageBubbleLimits = Modifier.padding(start = 10.dp, end = 10.dp, top = 5.dp, bottom = 5.dp)
|
||||
val messageBubbleLimits = Modifier.padding(start = 10.dp, end = 10.dp, top = 6.dp, bottom = 5.dp)
|
||||
|
||||
val inlinePlaceholder =
|
||||
Placeholder(
|
||||
|
||||
@@ -119,6 +119,9 @@ private val LightSubtleBorder = LightColorPalette.onSurface.copy(alpha = 0.05f)
|
||||
private val DarkChatBackground = DarkColorPalette.onSurface.copy(alpha = 0.12f)
|
||||
private val LightChatBackground = LightColorPalette.onSurface.copy(alpha = 0.08f)
|
||||
|
||||
private val DarkChatDraftBackground = DarkColorPalette.onSurface.copy(alpha = 0.15f)
|
||||
private val LightChatDraftBackground = LightColorPalette.onSurface.copy(alpha = 0.15f)
|
||||
|
||||
private val DarkOverPictureBackground = DarkColorPalette.background.copy(0.62f)
|
||||
private val LightOverPictureBackground = LightColorPalette.background.copy(0.62f)
|
||||
|
||||
@@ -387,6 +390,9 @@ val ColorScheme.subtleBorder: Color
|
||||
val ColorScheme.chatBackground: Color
|
||||
get() = if (isLight) LightChatBackground else DarkChatBackground
|
||||
|
||||
val ColorScheme.chatDraftBackground: Color
|
||||
get() = if (isLight) LightChatDraftBackground else DarkChatDraftBackground
|
||||
|
||||
val ColorScheme.subtleButton: Color
|
||||
get() = if (isLight) LightSubtleButton else DarkSubtleButton
|
||||
|
||||
|
||||
+53
@@ -22,6 +22,8 @@ package com.vitorpamplona.amethyst.commons.compose
|
||||
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
fun TextFieldValue.insertUrlAtCursor(url: String): TextFieldValue {
|
||||
var toInsert = url.trim()
|
||||
@@ -41,3 +43,54 @@ fun TextFieldValue.insertUrlAtCursor(url: String): TextFieldValue {
|
||||
TextRange(endOfUrlIndex, endOfUrlIndex),
|
||||
)
|
||||
}
|
||||
|
||||
fun TextFieldValue.replaceCurrentWord(wordToInsert: String): TextFieldValue {
|
||||
val lastWordStart = currentWordStartIdx()
|
||||
val lastWordEnd = currentWordEndIdx()
|
||||
val cursor = lastWordStart + wordToInsert.length
|
||||
return TextFieldValue(
|
||||
text.replaceRange(lastWordStart, lastWordEnd, wordToInsert),
|
||||
TextRange(cursor, cursor),
|
||||
)
|
||||
}
|
||||
|
||||
fun TextFieldValue.currentWordStartIdx(): Int {
|
||||
val previousNewLine = text.lastIndexOf('\n', selection.start - 1)
|
||||
val previousSpace = text.lastIndexOf(' ', selection.start - 1)
|
||||
|
||||
return max(
|
||||
previousNewLine,
|
||||
previousSpace,
|
||||
) + 1
|
||||
}
|
||||
|
||||
fun TextFieldValue.currentWordEndIdx(): Int {
|
||||
val nextNewLine = text.indexOf('\n', selection.end)
|
||||
val nextSpace = text.indexOf(' ', selection.end)
|
||||
|
||||
if (nextSpace < 0 && nextNewLine < 0) return selection.end
|
||||
if (nextSpace > 0 && nextNewLine > 0) {
|
||||
return min(
|
||||
nextNewLine,
|
||||
nextSpace,
|
||||
)
|
||||
}
|
||||
if (nextSpace > 0) {
|
||||
return nextSpace
|
||||
}
|
||||
return nextNewLine
|
||||
}
|
||||
|
||||
fun TextFieldValue.currentWord(): String {
|
||||
if (selection.end != selection.start) return ""
|
||||
|
||||
val start = currentWordStartIdx()
|
||||
val end = currentWordEndIdx()
|
||||
|
||||
return if (start < end) {
|
||||
val word = text.substring(start, end)
|
||||
word
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ data class Address(
|
||||
fun parse(addressId: String): Address? =
|
||||
try {
|
||||
val parts = addressId.split(":", limit = 3)
|
||||
if (parts[1].length == 64 && Hex.isHex(parts[1])) {
|
||||
if (parts.size > 1 && parts[1].length == 64 && Hex.isHex(parts[1])) {
|
||||
Address(parts[0].toInt(), parts[1], parts[2])
|
||||
} else {
|
||||
Log.w("AddressableId", "Error parsing. Pubkey is not hex: $addressId")
|
||||
|
||||
@@ -60,11 +60,12 @@ class ChatMessageEvent(
|
||||
|
||||
fun reply(
|
||||
msg: String,
|
||||
reply: EventHintBundle<ChatMessageEvent>,
|
||||
reply: EventHintBundle<BaseDMGroupEvent>,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<ChatMessageEvent>.() -> Unit = {},
|
||||
) = eventTemplate(KIND, msg, createdAt) {
|
||||
alt(ALT_DESCRIPTION)
|
||||
reply(reply)
|
||||
group((reply.event.recipients() + reply.toPTag()).distinctBy { it.pubKey })
|
||||
initializer()
|
||||
}
|
||||
|
||||
+2
-1
@@ -27,10 +27,11 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.pTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.pTags
|
||||
import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag
|
||||
import com.vitorpamplona.quartz.nip14Subject.SubjectTag
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.BaseDMGroupEvent
|
||||
|
||||
fun TagArrayBuilder<ChatMessageEvent>.reply(msg: MarkedETag) = add(msg.toTagArray())
|
||||
|
||||
fun TagArrayBuilder<ChatMessageEvent>.reply(msg: EventHintBundle<ChatMessageEvent>) = reply(msg.toMarkedETag(MarkedETag.MARKER.REPLY))
|
||||
fun TagArrayBuilder<ChatMessageEvent>.reply(msg: EventHintBundle<BaseDMGroupEvent>) = reply(msg.toMarkedETag(MarkedETag.MARKER.REPLY))
|
||||
|
||||
fun TagArrayBuilder<ChatMessageEvent>.group(list: List<PTag>) = pTags(list)
|
||||
|
||||
|
||||
@@ -29,13 +29,15 @@ data class EmojiUrlTag(
|
||||
) {
|
||||
fun encode(): String = ":$code:$url"
|
||||
|
||||
fun toContentEncode(): String = ":$code"
|
||||
fun toContentEncode(): String = contentEncode(code)
|
||||
|
||||
fun toTagArray() = arrayOf("emoji", code, url)
|
||||
|
||||
companion object {
|
||||
const val TAG_NAME = "emoji"
|
||||
|
||||
fun contentEncode(code: String): String = ":$code:"
|
||||
|
||||
fun decode(encodedEmojiSetup: String): EmojiUrlTag? {
|
||||
val emojiParts = encodedEmojiSetup.split(":", limit = 3)
|
||||
return if (emojiParts.size > 2) {
|
||||
|
||||
@@ -35,3 +35,8 @@ class IMetaTagBuilder(
|
||||
|
||||
fun build() = IMetaTag(url, properties)
|
||||
}
|
||||
|
||||
fun imetaTagBuilder(
|
||||
url: String,
|
||||
init: IMetaTagBuilder.() -> Unit,
|
||||
) = IMetaTagBuilder(url).apply(init).build()
|
||||
|
||||
Reference in New Issue
Block a user