- 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,
|
message: String,
|
||||||
toUser: User,
|
toUser: User,
|
||||||
replyingTo: Note? = null,
|
replyingTo: Note? = null,
|
||||||
mentions: List<User>?,
|
|
||||||
zapReceiver: List<ZapSplitSetup>? = null,
|
zapReceiver: List<ZapSplitSetup>? = null,
|
||||||
contentWarningReason: String? = null,
|
contentWarningReason: String? = null,
|
||||||
zapRaiserAmount: Long? = null,
|
zapRaiserAmount: Long? = null,
|
||||||
@@ -2590,7 +2589,6 @@ class Account(
|
|||||||
message,
|
message,
|
||||||
toUser.toPTag(),
|
toUser.toPTag(),
|
||||||
replyingTo,
|
replyingTo,
|
||||||
mentions,
|
|
||||||
zapReceiver,
|
zapReceiver,
|
||||||
contentWarningReason,
|
contentWarningReason,
|
||||||
zapRaiserAmount,
|
zapRaiserAmount,
|
||||||
@@ -2605,7 +2603,6 @@ class Account(
|
|||||||
message: String,
|
message: String,
|
||||||
toUser: PTag,
|
toUser: PTag,
|
||||||
replyingTo: Note? = null,
|
replyingTo: Note? = null,
|
||||||
mentions: List<User>?,
|
|
||||||
zapReceiver: List<ZapSplitSetup>? = null,
|
zapReceiver: List<ZapSplitSetup>? = null,
|
||||||
contentWarningReason: String? = null,
|
contentWarningReason: String? = null,
|
||||||
zapRaiserAmount: Long? = null,
|
zapRaiserAmount: Long? = null,
|
||||||
@@ -3183,10 +3180,16 @@ class Account(
|
|||||||
fun cachedDecryptContent(event: Event?): String? {
|
fun cachedDecryptContent(event: Event?): String? {
|
||||||
if (event == null) return null
|
if (event == null) return null
|
||||||
|
|
||||||
return if (event is PrivateDmEvent && isWriteable()) {
|
return if (isWriteable()) {
|
||||||
event.cachedContentFor(signer)
|
if (event is PrivateDmEvent) {
|
||||||
} else if (event is LnZapRequestEvent && event.isPrivateZap() && isWriteable()) {
|
event.cachedContentFor(signer)
|
||||||
event.cachedPrivateZap()?.content
|
} else if (event is LnZapRequestEvent && event.isPrivateZap()) {
|
||||||
|
event.cachedPrivateZap()?.content
|
||||||
|
} else if (event is DraftEvent) {
|
||||||
|
event.preCachedDraft(signer)?.content
|
||||||
|
} else {
|
||||||
|
event.content
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
event.content
|
event.content
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -236,16 +236,13 @@ class User(
|
|||||||
}.flatten()
|
}.flatten()
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
private fun getOrCreatePrivateChatroomSync(key: ChatroomKey): Chatroom {
|
private fun getOrCreatePrivateChatroomSync(key: ChatroomKey): Chatroom =
|
||||||
checkNotInMainThread()
|
privateChatrooms[key]
|
||||||
|
|
||||||
return privateChatrooms[key]
|
|
||||||
?: run {
|
?: run {
|
||||||
val privateChatroom = Chatroom()
|
val privateChatroom = Chatroom()
|
||||||
privateChatrooms = privateChatrooms + Pair(key, privateChatroom)
|
privateChatrooms = privateChatrooms + Pair(key, privateChatroom)
|
||||||
privateChatroom
|
privateChatroom
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun getOrCreatePrivateChatroom(user: User): Chatroom {
|
private fun getOrCreatePrivateChatroom(user: User): Chatroom {
|
||||||
val key = ChatroomKey(persistentSetOf(user.pubkeyHex))
|
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.note.UsernameDisplay
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
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.screen.loggedIn.search.SearchBarViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
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.notify
|
||||||
import com.vitorpamplona.quartz.nip10Notes.tags.positionalMarkedTags
|
import com.vitorpamplona.quartz.nip10Notes.tags.positionalMarkedTags
|
||||||
import com.vitorpamplona.quartz.nip14Subject.subject
|
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.base.NIP17Group
|
||||||
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
||||||
import com.vitorpamplona.quartz.nip17Dm.messages.changeSubject
|
import com.vitorpamplona.quartz.nip17Dm.messages.changeSubject
|
||||||
@@ -818,7 +819,6 @@ open class NewPostViewModel : ViewModel() {
|
|||||||
message = tagger.message,
|
message = tagger.message,
|
||||||
toUser = originalNote!!.author!!,
|
toUser = originalNote!!.author!!,
|
||||||
replyingTo = originalNote!!,
|
replyingTo = originalNote!!,
|
||||||
mentions = tagger.pTags,
|
|
||||||
zapReceiver = zapReceiver,
|
zapReceiver = zapReceiver,
|
||||||
contentWarningReason = contentWarningReason,
|
contentWarningReason = contentWarningReason,
|
||||||
zapRaiserAmount = localZapRaiserAmount,
|
zapRaiserAmount = localZapRaiserAmount,
|
||||||
@@ -827,7 +827,7 @@ open class NewPostViewModel : ViewModel() {
|
|||||||
draftTag = localDraft,
|
draftTag = localDraft,
|
||||||
)
|
)
|
||||||
} else if (originalNote?.event is NIP17Group) {
|
} else if (originalNote?.event is NIP17Group) {
|
||||||
val replyHint = originalNote?.toEventHint<ChatMessageEvent>()
|
val replyHint = originalNote?.toEventHint<BaseDMGroupEvent>()
|
||||||
|
|
||||||
val template =
|
val template =
|
||||||
if (replyHint == null) {
|
if (replyHint == null) {
|
||||||
@@ -868,7 +868,7 @@ open class NewPostViewModel : ViewModel() {
|
|||||||
account?.sendNIP17PrivateMessage(template, localDraft)
|
account?.sendNIP17PrivateMessage(template, localDraft)
|
||||||
} else if (!dmUsers.isNullOrEmpty()) {
|
} else if (!dmUsers.isNullOrEmpty()) {
|
||||||
if (nip17 || dmUsers.size > 1) {
|
if (nip17 || dmUsers.size > 1) {
|
||||||
val replyHint = originalNote?.toEventHint<ChatMessageEvent>()
|
val replyHint = originalNote?.toEventHint<BaseDMGroupEvent>()
|
||||||
val template =
|
val template =
|
||||||
if (replyHint == null) {
|
if (replyHint == null) {
|
||||||
ChatMessageEvent.build(tagger.message, dmUsers.map { it.toPTag() }) {
|
ChatMessageEvent.build(tagger.message, dmUsers.map { it.toPTag() }) {
|
||||||
@@ -910,7 +910,6 @@ open class NewPostViewModel : ViewModel() {
|
|||||||
message = tagger.message,
|
message = tagger.message,
|
||||||
toUser = dmUsers.first(),
|
toUser = dmUsers.first(),
|
||||||
replyingTo = originalNote,
|
replyingTo = originalNote,
|
||||||
mentions = tagger.pTags,
|
|
||||||
contentWarningReason = contentWarningReason,
|
contentWarningReason = contentWarningReason,
|
||||||
zapReceiver = zapReceiver,
|
zapReceiver = zapReceiver,
|
||||||
zapRaiserAmount = localZapRaiserAmount,
|
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.note.timeAgo
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
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.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
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.NoteCompose
|
||||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
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.screen.loggedIn.mockAccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding
|
import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding
|
||||||
import com.vitorpamplona.amethyst.ui.theme.inlinePlaceholder
|
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 androidx.compose.ui.unit.dp
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo
|
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.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
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.LoadRedirectScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.NewPostScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.NewPostScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarks.BookmarkListScreen
|
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.chatlist.ChatroomListScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ChatroomListScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.private.ChatroomScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ChatroomScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.private.ChatroomScreenByAuthor
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.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.communities.CommunityScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.DiscoverScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.DiscoverScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.drafts.DraftListScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.drafts.DraftListScreen
|
||||||
@@ -94,6 +94,16 @@ fun NavBackStackEntry.message(): String? =
|
|||||||
URLDecoder.decode(it, "utf-8")
|
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
|
@Composable
|
||||||
fun AppNavigation(
|
fun AppNavigation(
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
@@ -219,6 +229,8 @@ fun AppNavigation(
|
|||||||
ChatroomScreen(
|
ChatroomScreen(
|
||||||
roomId = it.id(),
|
roomId = it.id(),
|
||||||
draftMessage = it.message(),
|
draftMessage = it.message(),
|
||||||
|
replyToNote = it.replyId(),
|
||||||
|
editFromDraft = it.draftId(),
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ interface INav {
|
|||||||
|
|
||||||
fun nav(route: String)
|
fun nav(route: String)
|
||||||
|
|
||||||
|
fun nav(computeRoute: suspend () -> String)
|
||||||
|
|
||||||
fun newStack(route: String)
|
fun newStack(route: String)
|
||||||
|
|
||||||
fun popBack()
|
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) {
|
override fun newStack(route: String) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
controller.navigate(route) {
|
controller.navigate(route) {
|
||||||
@@ -135,6 +146,9 @@ object EmptyNav : INav {
|
|||||||
override fun nav(route: String) {
|
override fun nav(route: String) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun nav(computeRoute: suspend () -> String) {
|
||||||
|
}
|
||||||
|
|
||||||
override fun newStack(route: String) {
|
override fun newStack(route: String) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,6 +175,11 @@ class ObservableNavigate(
|
|||||||
nav.nav(route)
|
nav.nav(route)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun nav(computeRoute: suspend () -> String) {
|
||||||
|
onNavigate()
|
||||||
|
nav.nav(computeRoute)
|
||||||
|
}
|
||||||
|
|
||||||
override fun newStack(route: String) {
|
override fun newStack(route: String) {
|
||||||
onNavigate()
|
onNavigate()
|
||||||
nav.newStack(route)
|
nav.newStack(route)
|
||||||
|
|||||||
@@ -21,8 +21,10 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.navigation
|
package com.vitorpamplona.amethyst.ui.navigation
|
||||||
|
|
||||||
import com.vitorpamplona.amethyst.model.Channel
|
import com.vitorpamplona.amethyst.model.Channel
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache.users
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource.user
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
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.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||||
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
|
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
|
||||||
import kotlinx.collections.immutable.persistentSetOf
|
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
|
|
||||||
fun routeFor(
|
fun routeFor(
|
||||||
@@ -110,23 +111,71 @@ fun routeFor(
|
|||||||
fun routeToMessage(
|
fun routeToMessage(
|
||||||
user: HexKey,
|
user: HexKey,
|
||||||
draftMessage: String?,
|
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,
|
accountViewModel: AccountViewModel,
|
||||||
): String {
|
): String {
|
||||||
val withKey = ChatroomKey(persistentSetOf(user))
|
accountViewModel.account.userProfile().createChatroom(room)
|
||||||
accountViewModel.account.userProfile().createChatroom(withKey)
|
|
||||||
return if (draftMessage != null) {
|
val params =
|
||||||
val encodedMessage = URLEncoder.encode(draftMessage, "utf-8")
|
listOfNotNull(
|
||||||
"Room/${withKey.hashCode()}?message=$encodedMessage"
|
draftMessage?.let {
|
||||||
} else {
|
"message=${URLEncoder.encode(it, "utf-8")}"
|
||||||
"Room/${withKey.hashCode()}"
|
},
|
||||||
|
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(
|
fun routeToMessage(
|
||||||
user: User,
|
user: User,
|
||||||
draftMessage: String?,
|
draftMessage: String?,
|
||||||
|
replyId: HexKey? = null,
|
||||||
|
quoteId: HexKey? = null,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
): String = routeToMessage(user.pubkeyHex, draftMessage, accountViewModel)
|
): String = routeToMessage(user.pubkeyHex, draftMessage, replyId, quoteId, accountViewModel)
|
||||||
|
|
||||||
fun routeFor(note: Channel): String = "Channel/${note.idHex}"
|
fun routeFor(note: Channel): String = "Channel/${note.idHex}"
|
||||||
|
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ sealed class Route(
|
|||||||
|
|
||||||
object Room :
|
object Room :
|
||||||
Route(
|
Route(
|
||||||
route = "Room/{id}?message={message}",
|
route = "Room/{id}?message={message}&replyId={replyId}&draftId={draftId}",
|
||||||
icon = R.drawable.ic_moments,
|
icon = R.drawable.ic_moments,
|
||||||
arguments =
|
arguments =
|
||||||
listOf(
|
listOf(
|
||||||
@@ -179,6 +179,16 @@ sealed class Route(
|
|||||||
nullable = true
|
nullable = true
|
||||||
defaultValue = null
|
defaultValue = null
|
||||||
},
|
},
|
||||||
|
navArgument("replyId") {
|
||||||
|
type = NavType.StringType
|
||||||
|
nullable = true
|
||||||
|
defaultValue = null
|
||||||
|
},
|
||||||
|
navArgument("draftId") {
|
||||||
|
type = NavType.StringType
|
||||||
|
nullable = true
|
||||||
|
defaultValue = null
|
||||||
|
},
|
||||||
).toImmutableList(),
|
).toImmutableList(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -78,11 +78,11 @@ import com.vitorpamplona.amethyst.ui.layouts.LeftPictureLayout
|
|||||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||||
import com.vitorpamplona.amethyst.ui.note.elements.BannerImage
|
import com.vitorpamplona.amethyst.ui.note.elements.BannerImage
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
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.screen.loggedIn.chatrooms.EndedFlag
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.public.EndedFlag
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.LiveFlag
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.public.LiveFlag
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.OfflineFlag
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.public.OfflineFlag
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ScheduledFlag
|
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.dvms.observeAppDefinition
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CheckIfVideoIsOnline
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CheckIfVideoIsOnline
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
|
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.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.produceState
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@@ -73,15 +72,15 @@ fun LoadDecryptedContentOrNull(
|
|||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
inner: @Composable (String?) -> Unit,
|
inner: @Composable (String?) -> Unit,
|
||||||
) {
|
) {
|
||||||
@Suppress("ProduceStateDoesNotAssignValue")
|
var decryptedContent by remember(note.event?.id) { mutableStateOf(accountViewModel.cachedDecrypt(note)) }
|
||||||
val decryptedContent by
|
|
||||||
produceState(initialValue = accountViewModel.cachedDecrypt(note), key1 = note.event?.id) {
|
LaunchedEffect(note.event?.id) {
|
||||||
accountViewModel.decrypt(note) {
|
accountViewModel.decrypt(note) {
|
||||||
if (value != it) {
|
if (decryptedContent != it) {
|
||||||
value = it
|
decryptedContent = it
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
inner(decryptedContent)
|
inner(decryptedContent)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
@@ -74,7 +73,6 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@@ -202,15 +200,14 @@ fun ErrorRow(
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
errorState.user?.let {
|
errorState.user?.let {
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
Column(Modifier.width(Size40dp), horizontalAlignment = Alignment.Start) {
|
Column(Modifier.width(Size40dp), horizontalAlignment = Alignment.Start) {
|
||||||
UserPicture(errorState.user, Size30dp, Modifier, accountViewModel, nav)
|
UserPicture(errorState.user, Size30dp, Modifier, accountViewModel, nav)
|
||||||
Spacer(StdVertSpacer)
|
Spacer(StdVertSpacer)
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = Size30Modifier,
|
modifier = Size30Modifier,
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch(Dispatchers.IO) {
|
nav.nav {
|
||||||
nav.nav(routeToMessage(it, errorState.error, accountViewModel))
|
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.RenderWikiContent
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.VideoDisplay
|
import com.vitorpamplona.amethyst.ui.note.types.VideoDisplay
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
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.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Font12SP
|
import com.vitorpamplona.amethyst.ui.theme.Font12SP
|
||||||
import com.vitorpamplona.amethyst.ui.theme.HalfDoubleVertSpacer
|
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.HalfPadding
|
||||||
import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
|
import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
|
||||||
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing10dp
|
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing10dp
|
||||||
@@ -1022,7 +1021,7 @@ fun DisplayDraftChat() {
|
|||||||
Text(
|
Text(
|
||||||
"Draft",
|
"Draft",
|
||||||
color = MaterialTheme.colorScheme.placeholderText,
|
color = MaterialTheme.colorScheme.placeholderText,
|
||||||
modifier = HalfEndPadding,
|
modifier = Modifier,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = Font12SP,
|
fontSize = Font12SP,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
|
|||||||
@@ -627,7 +627,11 @@ fun ZapVote(
|
|||||||
title = toast.title,
|
title = toast.title,
|
||||||
textContent = toast.msg,
|
textContent = toast.msg,
|
||||||
onClickStartMessage = {
|
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 },
|
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.components.InLineIconRenderer
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.buildNewPostRoute
|
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.note.types.EditState
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
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.reactionBox
|
||||||
import com.vitorpamplona.amethyst.ui.theme.ripple24dp
|
import com.vitorpamplona.amethyst.ui.theme.ripple24dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.selectedReactionBoxModifier
|
import com.vitorpamplona.amethyst.ui.theme.selectedReactionBoxModifier
|
||||||
|
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
||||||
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
|
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
|
||||||
|
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
|
||||||
import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji
|
import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji
|
||||||
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount
|
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
@@ -209,13 +212,16 @@ private fun InnerReactionRow(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
three = {
|
three = {
|
||||||
BoostWithDialog(
|
val isDM = baseNote.event is ChatroomKeyable
|
||||||
baseNote,
|
if (!isDM) {
|
||||||
editState,
|
BoostWithDialog(
|
||||||
MaterialTheme.colorScheme.placeholderText,
|
baseNote,
|
||||||
accountViewModel,
|
editState,
|
||||||
nav,
|
MaterialTheme.colorScheme.placeholderText,
|
||||||
)
|
accountViewModel,
|
||||||
|
nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
four = {
|
four = {
|
||||||
LikeReaction(baseNote, MaterialTheme.colorScheme.placeholderText, accountViewModel, nav)
|
LikeReaction(baseNote, MaterialTheme.colorScheme.placeholderText, accountViewModel, nav)
|
||||||
@@ -558,56 +564,46 @@ private fun BoostWithDialog(
|
|||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
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(
|
BoostReaction(
|
||||||
baseNote,
|
baseNote,
|
||||||
grayTint,
|
grayTint,
|
||||||
accountViewModel,
|
accountViewModel,
|
||||||
onQuotePress = { wantsToQuote = baseNote },
|
onQuotePress = {
|
||||||
onForkPress = { wantsToFork = baseNote },
|
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,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
var wantsToReplyTo by remember { mutableStateOf<Note?>(null) }
|
ReplyReaction(baseNote, grayTint, accountViewModel) {
|
||||||
|
val noteEvent = baseNote.event
|
||||||
if (wantsToReplyTo != null) {
|
if (noteEvent is PrivateDmEvent) {
|
||||||
val route =
|
nav.nav {
|
||||||
buildNewPostRoute(
|
routeToMessage(
|
||||||
baseReplyTo = wantsToReplyTo?.idHex,
|
room = noteEvent.chatroomKey(accountViewModel.userProfile().pubkeyHex),
|
||||||
quote = null,
|
draftMessage = null,
|
||||||
)
|
replyId = noteEvent.id,
|
||||||
nav.nav(route)
|
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
|
@Composable
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage
|
|||||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
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.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
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.components.GenericLoadable
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
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.StdVertSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||||
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
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.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
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.StdVertSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
|
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.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
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.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding
|
import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
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.base.ChatroomKeyable
|
||||||
import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent
|
import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent
|
||||||
import com.vitorpamplona.quartz.nip17Dm.files.encryption.AESGCM
|
import com.vitorpamplona.quartz.nip17Dm.files.encryption.AESGCM
|
||||||
import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri
|
|
||||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
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.DisplayUncitedHashtags
|
||||||
import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton
|
import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.JoinCommunityButton
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.public.JoinCommunityButton
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.LeaveCommunityButton
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.public.LeaveCommunityButton
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.NormalTimeAgo
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.public.NormalTimeAgo
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
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.DisplayAuthorBanner
|
||||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.LiveFlag
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.public.LiveFlag
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ScheduledFlag
|
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.CheckIfVideoIsOnline
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CrossfadeCheckIfVideoIsOnline
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CrossfadeCheckIfVideoIsOnline
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
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.components.GenericLoadable
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
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.StdVertSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||||
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
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.LoadDecryptedContent
|
||||||
import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags
|
import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
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.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
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()
|
fun relayStatusFlow() = Amethyst.instance.client.relayStatusFlow()
|
||||||
|
|
||||||
val draftNoteCache = CachedDraftNotes(this)
|
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.UsernameDisplay
|
||||||
import com.vitorpamplona.amethyst.ui.note.WatchAndLoadMyEmojiList
|
import com.vitorpamplona.amethyst.ui.note.WatchAndLoadMyEmojiList
|
||||||
import com.vitorpamplona.amethyst.ui.note.ZapSplitIcon
|
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.screen.loggedIn.settings.SettingsRow
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
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
|
* 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.
|
* 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.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
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
|
* 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.
|
* 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.background
|
||||||
import androidx.compose.foundation.layout.Box
|
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
|
* 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.
|
* 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.animation.core.tween
|
||||||
import androidx.compose.foundation.layout.Row
|
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
|
* 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.
|
* 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.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.layout.Box
|
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.navigation.Route
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.DisappearingScaffold
|
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.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
|
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
|
||||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -121,6 +124,7 @@ fun ChatroomListScreen(
|
|||||||
|
|
||||||
class TwoPaneNav(
|
class TwoPaneNav(
|
||||||
val nav: INav,
|
val nav: INav,
|
||||||
|
val scope: CoroutineScope,
|
||||||
) : INav {
|
) : INav {
|
||||||
override val drawerState: DrawerState = nav.drawerState
|
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) {
|
override fun newStack(route: String) {
|
||||||
nav.newStack(route)
|
nav.newStack(route)
|
||||||
}
|
}
|
||||||
@@ -172,7 +187,8 @@ fun ChatroomListTwoPane(
|
|||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
/** The index of the currently selected word, or `null` if none is selected */
|
/** 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 =
|
val strategy =
|
||||||
remember {
|
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
|
* 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.
|
* 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 android.content.Context
|
||||||
import androidx.compose.runtime.Stable
|
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.SelectedMedia
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
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.base.ChatroomKey
|
||||||
import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent
|
import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent
|
||||||
import com.vitorpamplona.quartz.nip17Dm.files.encryption.AESGCM
|
import com.vitorpamplona.quartz.nip17Dm.files.encryption.AESGCM
|
||||||
|
import com.vitorpamplona.quartz.nip30CustomEmoji.emojis
|
||||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||||
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning
|
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
@@ -53,6 +55,7 @@ import kotlinx.coroutines.launch
|
|||||||
open class ChatFileUploadModel : ViewModel() {
|
open class ChatFileUploadModel : ViewModel() {
|
||||||
var account: Account? = null
|
var account: Account? = null
|
||||||
var chatroom: ChatroomKey? = null
|
var chatroom: ChatroomKey? = null
|
||||||
|
var isNip17: Boolean = false
|
||||||
|
|
||||||
var isUploadingImage by mutableStateOf(false)
|
var isUploadingImage by mutableStateOf(false)
|
||||||
|
|
||||||
@@ -69,6 +72,7 @@ open class ChatFileUploadModel : ViewModel() {
|
|||||||
open fun load(
|
open fun load(
|
||||||
uris: ImmutableList<SelectedMedia>,
|
uris: ImmutableList<SelectedMedia>,
|
||||||
chatroom: ChatroomKey,
|
chatroom: ChatroomKey,
|
||||||
|
isNip17: Boolean,
|
||||||
account: Account,
|
account: Account,
|
||||||
) {
|
) {
|
||||||
this.chatroom = chatroom
|
this.chatroom = chatroom
|
||||||
@@ -76,6 +80,7 @@ open class ChatFileUploadModel : ViewModel() {
|
|||||||
this.account = account
|
this.account = account
|
||||||
this.multiOrchestrator = MultiOrchestrator(uris)
|
this.multiOrchestrator = MultiOrchestrator(uris)
|
||||||
this.selectedServer = defaultServer()
|
this.selectedServer = defaultServer()
|
||||||
|
this.isNip17 = isNip17
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isImage(
|
fun isImage(
|
||||||
@@ -87,6 +92,18 @@ open class ChatFileUploadModel : ViewModel() {
|
|||||||
onError: (title: String, message: String) -> Unit,
|
onError: (title: String, message: String) -> Unit,
|
||||||
context: Context,
|
context: Context,
|
||||||
onceUploaded: () -> Unit,
|
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 myAccount = account ?: return
|
||||||
val mySelectedServer = selectedServer ?: 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() {
|
open fun cancelModel() {
|
||||||
multiOrchestrator = null
|
multiOrchestrator = null
|
||||||
isUploadingImage = false
|
isUploadingImage = false
|
||||||
+3
-4
@@ -18,7 +18,7 @@
|
|||||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
* 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.
|
* 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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -148,9 +148,8 @@ fun ChatFileUploadView(
|
|||||||
postViewModel.upload(
|
postViewModel.upload(
|
||||||
onError = accountViewModel::toast,
|
onError = accountViewModel::toast,
|
||||||
context = context,
|
context = context,
|
||||||
) {
|
onceUploaded = onClose,
|
||||||
onClose
|
)
|
||||||
}
|
|
||||||
|
|
||||||
postViewModel.selectedServer?.let {
|
postViewModel.selectedServer?.let {
|
||||||
if (it.type != ServerType.NIP95) {
|
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
|
* 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.
|
* 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.animation.core.tween
|
||||||
import androidx.compose.foundation.layout.Row
|
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
|
* 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.
|
* 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.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.size
|
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.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -44,17 +35,13 @@ import androidx.compose.runtime.MutableState
|
|||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.compositeOver
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
import com.vitorpamplona.amethyst.model.FeatureSetType
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
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.note.types.RenderEncryptedFile
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
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.DoubleHorzSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Font12SP
|
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.RowColSpacing
|
||||||
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing5dp
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size18Modifier
|
import com.vitorpamplona.amethyst.ui.theme.Size18Modifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size5Modifier
|
import com.vitorpamplona.amethyst.ui.theme.Size5Modifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.chatAuthorBox
|
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.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.amethyst.ui.theme.placeholderText
|
||||||
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
|
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
|
||||||
import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists
|
import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists
|
||||||
@@ -188,6 +164,7 @@ fun NormalChatNote(
|
|||||||
|
|
||||||
ChatBubbleLayout(
|
ChatBubbleLayout(
|
||||||
isLoggedInUser = isLoggedInUser,
|
isLoggedInUser = isLoggedInUser,
|
||||||
|
isDraft = note.event is DraftEvent,
|
||||||
innerQuote = innerQuote,
|
innerQuote = innerQuote,
|
||||||
isComplete = accountViewModel.settings.featureSet == FeatureSetType.COMPLETE,
|
isComplete = accountViewModel.settings.featureSet == FeatureSetType.COMPLETE,
|
||||||
hasDetailsToShow = note.zaps.isNotEmpty() || note.zapPayments.isNotEmpty() || note.reactions.isNotEmpty(),
|
hasDetailsToShow = note.zaps.isNotEmpty() || note.zapPayments.isNotEmpty() || note.reactions.isNotEmpty(),
|
||||||
@@ -223,27 +200,29 @@ fun NormalChatNote(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
detailRow = {
|
detailRow = {
|
||||||
if (note.isDraft()) {
|
|
||||||
DisplayDraftChat()
|
|
||||||
}
|
|
||||||
IncognitoBadge(note)
|
IncognitoBadge(note)
|
||||||
ChatTimeAgo(note)
|
ChatTimeAgo(note)
|
||||||
RelayBadgesHorizontal(note, accountViewModel, nav = nav)
|
RelayBadgesHorizontal(note, accountViewModel, nav = nav)
|
||||||
|
|
||||||
Spacer(modifier = DoubleHorzSpacer)
|
Spacer(modifier = DoubleHorzSpacer)
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = RowColSpacing) {
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = RowColSpacing) {
|
||||||
ReplyReaction(
|
if (!note.isDraft()) {
|
||||||
baseNote = note,
|
ReplyReaction(
|
||||||
grayTint = MaterialTheme.colorScheme.placeholderText,
|
baseNote = note,
|
||||||
accountViewModel = accountViewModel,
|
grayTint = MaterialTheme.colorScheme.placeholderText,
|
||||||
showCounter = false,
|
accountViewModel = accountViewModel,
|
||||||
iconSizeModifier = Size18Modifier,
|
showCounter = false,
|
||||||
) {
|
iconSizeModifier = Size18Modifier,
|
||||||
onWantsToReply(note)
|
) {
|
||||||
}
|
onWantsToReply(note)
|
||||||
Spacer(modifier = StdHorzSpacer)
|
}
|
||||||
LikeReaction(note, MaterialTheme.colorScheme.placeholderText, accountViewModel, nav)
|
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 ->
|
) { 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
|
@Composable
|
||||||
private fun MessageBubbleLines(
|
private fun MessageBubbleLines(
|
||||||
baseNote: Note,
|
baseNote: Note,
|
||||||
+64
-105
@@ -18,7 +18,7 @@
|
|||||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
* 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.
|
* 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.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -53,7 +53,6 @@ import androidx.compose.material3.TextFieldDefaults
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -80,11 +79,9 @@ import androidx.lifecycle.map
|
|||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.NostrChatroomDataSource
|
import com.vitorpamplona.amethyst.service.NostrChatroomDataSource
|
||||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
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.UrlUserTagTransformation
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
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.CloseButton
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.DisappearingScaffold
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.DisappearingScaffold
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.PostButton
|
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.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.BottomTopHeight
|
import com.vitorpamplona.amethyst.ui.theme.BottomTopHeight
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
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.StdPadding
|
||||||
import com.vitorpamplona.amethyst.ui.theme.ZeroPadding
|
import com.vitorpamplona.amethyst.ui.theme.ZeroPadding
|
||||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
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.nip17Dm.base.ChatroomKey
|
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.ChatMessageEvent
|
||||||
import com.vitorpamplona.quartz.nip17Dm.messages.changeSubject
|
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.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentSetOf
|
import kotlinx.collections.immutable.persistentSetOf
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
@@ -144,6 +138,8 @@ import kotlinx.coroutines.launch
|
|||||||
fun ChatroomScreen(
|
fun ChatroomScreen(
|
||||||
roomId: String?,
|
roomId: String?,
|
||||||
draftMessage: String? = null,
|
draftMessage: String? = null,
|
||||||
|
replyToNote: HexKey? = null,
|
||||||
|
editFromDraft: HexKey? = null,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
@@ -157,7 +153,7 @@ fun ChatroomScreen(
|
|||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
) {
|
) {
|
||||||
Column(Modifier.padding(it)) {
|
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(
|
fun Chatroom(
|
||||||
roomId: String?,
|
roomId: String?,
|
||||||
draftMessage: String? = null,
|
draftMessage: String? = null,
|
||||||
|
replyToNote: HexKey? = null,
|
||||||
|
editFromDraft: HexKey? = null,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
@@ -248,6 +246,8 @@ fun Chatroom(
|
|||||||
PrepareChatroomViewModels(
|
PrepareChatroomViewModels(
|
||||||
room = it,
|
room = it,
|
||||||
draftMessage = draftMessage,
|
draftMessage = draftMessage,
|
||||||
|
replyToNote = replyToNote,
|
||||||
|
editFromDraft = editFromDraft,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
)
|
)
|
||||||
@@ -306,6 +306,8 @@ fun ChatroomByAuthor(
|
|||||||
PrepareChatroomViewModels(
|
PrepareChatroomViewModels(
|
||||||
room = it,
|
room = it,
|
||||||
draftMessage = draftMessage,
|
draftMessage = draftMessage,
|
||||||
|
replyToNote = null,
|
||||||
|
editFromDraft = null,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
)
|
)
|
||||||
@@ -356,6 +358,8 @@ fun LoadRoomByAuthor(
|
|||||||
fun PrepareChatroomViewModels(
|
fun PrepareChatroomViewModels(
|
||||||
room: ChatroomKey,
|
room: ChatroomKey,
|
||||||
draftMessage: String?,
|
draftMessage: String?,
|
||||||
|
replyToNote: HexKey? = null,
|
||||||
|
editFromDraft: HexKey? = null,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
@@ -369,29 +373,46 @@ fun PrepareChatroomViewModels(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
val newPostModel: NewPostViewModel = viewModel()
|
val newPostModel: ChatNewMessageViewModel = viewModel()
|
||||||
newPostModel.accountViewModel = accountViewModel
|
newPostModel.init(accountViewModel)
|
||||||
newPostModel.account = accountViewModel.account
|
newPostModel.load(room)
|
||||||
newPostModel.requiresNIP17 = room.users.size > 1
|
|
||||||
|
|
||||||
if (newPostModel.requiresNIP17) {
|
if (replyToNote != null) {
|
||||||
newPostModel.nip17 = true
|
LaunchedEffect(key1 = replyToNote) {
|
||||||
} else {
|
accountViewModel.checkGetOrCreateNote(replyToNote) {
|
||||||
if (room.users.size == 1) {
|
if (it != null) {
|
||||||
ObserveRelayListForDMs(pubkey = room.users.first(), accountViewModel = accountViewModel) {
|
newPostModel.reply(it)
|
||||||
if (it?.relays().isNullOrEmpty()) {
|
|
||||||
newPostModel.nip17 = false
|
|
||||||
} else {
|
|
||||||
newPostModel.nip17 = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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()
|
val imageUpload: ChatFileUploadModel = viewModel()
|
||||||
|
|
||||||
if (draftMessage != null) {
|
if (draftMessage != null) {
|
||||||
LaunchedEffect(key1 = draftMessage) { newPostModel.updateMessage(TextFieldValue(draftMessage)) }
|
LaunchedEffect(key1 = draftMessage) {
|
||||||
|
newPostModel.updateMessage(TextFieldValue(draftMessage))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatroomScreen(
|
ChatroomScreen(
|
||||||
@@ -408,7 +429,7 @@ fun PrepareChatroomViewModels(
|
|||||||
fun ChatroomScreen(
|
fun ChatroomScreen(
|
||||||
room: ChatroomKey,
|
room: ChatroomKey,
|
||||||
feedViewModel: NostrChatroomFeedViewModel,
|
feedViewModel: NostrChatroomFeedViewModel,
|
||||||
newPostModel: NewPostViewModel,
|
newPostModel: ChatNewMessageViewModel,
|
||||||
fileUpload: ChatFileUploadModel,
|
fileUpload: ChatFileUploadModel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
@@ -444,7 +465,6 @@ fun ChatroomScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Column(Modifier.fillMaxHeight()) {
|
Column(Modifier.fillMaxHeight()) {
|
||||||
val replyTo = remember { mutableStateOf<Note?>(null) }
|
|
||||||
ObserveRelayListForDMsAndDisplayIfNotFound(accountViewModel, nav)
|
ObserveRelayListForDMsAndDisplayIfNotFound(accountViewModel, nav)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
@@ -460,18 +480,14 @@ fun ChatroomScreen(
|
|||||||
nav = nav,
|
nav = nav,
|
||||||
routeForLastRead = "Room/${room.hashCode()}",
|
routeForLastRead = "Room/${room.hashCode()}",
|
||||||
avoidDraft = newPostModel.draftTag,
|
avoidDraft = newPostModel.draftTag,
|
||||||
onWantsToReply = {
|
onWantsToReply = newPostModel::reply,
|
||||||
replyTo.value = it
|
onWantsToEditDraft = newPostModel::editFromDraft,
|
||||||
},
|
|
||||||
onWantsToEditDraft = {
|
|
||||||
newPostModel.load(accountViewModel, null, null, null, null, it)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(10.dp))
|
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()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
@@ -481,7 +497,7 @@ fun ChatroomScreen(
|
|||||||
.receiveAsFlow()
|
.receiveAsFlow()
|
||||||
.debounce(1000)
|
.debounce(1000)
|
||||||
.collectLatest {
|
.collectLatest {
|
||||||
innerSendPost(newPostModel, room, replyTo, accountViewModel, newPostModel.draftTag)
|
newPostModel.sendDraft()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -501,85 +517,29 @@ fun ChatroomScreen(
|
|||||||
accountViewModel,
|
accountViewModel,
|
||||||
onSendNewMessage = {
|
onSendNewMessage = {
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
innerSendPost(newPostModel, room, replyTo, accountViewModel, null)
|
newPostModel.sendPostSync()
|
||||||
|
|
||||||
accountViewModel.deleteDraft(newPostModel.draftTag)
|
|
||||||
|
|
||||||
newPostModel.message = TextFieldValue("")
|
|
||||||
|
|
||||||
replyTo.value = null
|
|
||||||
feedViewModel.sendToTop()
|
feedViewModel.sendToTop()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSendNewMedia = {
|
onSendNewMedia = { list, isNip17 ->
|
||||||
fileUpload.load(it, room, accountViewModel.account)
|
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
|
@Composable
|
||||||
fun PrivateMessageEditFieldRow(
|
fun PrivateMessageEditFieldRow(
|
||||||
channelScreenModel: NewPostViewModel,
|
channelScreenModel: ChatNewMessageViewModel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
onSendNewMessage: () -> Unit,
|
onSendNewMessage: () -> Unit,
|
||||||
onSendNewMedia: (ImmutableList<SelectedMedia>) -> Unit,
|
onSendNewMedia: (ImmutableList<SelectedMedia>, isNip17: Boolean) -> Unit,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = EditFieldModifier,
|
modifier = EditFieldModifier,
|
||||||
) {
|
) {
|
||||||
ShowUserSuggestionList(
|
ShowUserSuggestionList(
|
||||||
channelScreenModel.userSuggestions,
|
channelScreenModel.userSuggestions.userSuggestions,
|
||||||
channelScreenModel::autocompleteWithUser,
|
channelScreenModel::autocompleteWithUser,
|
||||||
accountViewModel,
|
accountViewModel,
|
||||||
)
|
)
|
||||||
@@ -623,11 +583,10 @@ fun PrivateMessageEditFieldRow(
|
|||||||
SelectFromGallery(
|
SelectFromGallery(
|
||||||
isUploading = channelScreenModel.isUploadingImage,
|
isUploading = channelScreenModel.isUploadingImage,
|
||||||
tint = MaterialTheme.colorScheme.placeholderText,
|
tint = MaterialTheme.colorScheme.placeholderText,
|
||||||
modifier =
|
modifier = Modifier.size(30.dp).padding(start = 2.dp),
|
||||||
Modifier
|
onImageChosen = {
|
||||||
.size(30.dp)
|
onSendNewMedia(it, channelScreenModel.nip17)
|
||||||
.padding(start = 2.dp),
|
},
|
||||||
onImageChosen = onSendNewMedia,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var wantsToActivateNIP17 by remember { mutableStateOf(false) }
|
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
|
* 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.
|
* 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.background
|
||||||
import androidx.compose.foundation.clickable
|
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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.defaultMinSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
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.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
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.KeyboardOptions
|
||||||
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.icons.Icons
|
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.EditNote
|
||||||
import androidx.compose.material.icons.filled.Share
|
import androidx.compose.material.icons.filled.Share
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.LocalTextStyle
|
import androidx.compose.material3.LocalTextStyle
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextFieldColors
|
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
@@ -71,25 +56,19 @@ import androidx.compose.runtime.livedata.observeAsState
|
|||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
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.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
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.TextDirection
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
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.NostrChannelFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.DisappearingScaffold
|
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.home.CrossfadeCheckIfVideoIsOnline
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
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
|
@Composable
|
||||||
fun EditFieldRow(
|
fun EditFieldRow(
|
||||||
channelScreenModel: NewPostViewModel,
|
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
|
@Composable
|
||||||
fun RenderChannelHeader(
|
fun RenderChannelHeader(
|
||||||
channelNote: Note,
|
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.amethyst.ui.theme.Size16Modifier
|
||||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceErrorResponse
|
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceErrorResponse
|
||||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceSuccessResponse
|
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceSuccessResponse
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DisplayLNAddress(
|
fun DisplayLNAddress(
|
||||||
@@ -71,9 +69,8 @@ fun DisplayLNAddress(
|
|||||||
title = stringRes(id = R.string.error_dialog_zap_error),
|
title = stringRes(id = R.string.error_dialog_zap_error),
|
||||||
textContent = showErrorMessageDialog ?: "",
|
textContent = showErrorMessageDialog ?: "",
|
||||||
onClickStartMessage = {
|
onClickStartMessage = {
|
||||||
scope.launch(Dispatchers.IO) {
|
nav.nav {
|
||||||
val route = routeToMessage(userHex, showErrorMessageDialog, accountViewModel)
|
routeToMessage(userHex, showErrorMessageDialog, accountViewModel = accountViewModel)
|
||||||
nav.nav(route)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDismiss = { showErrorMessageDialog = null },
|
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.note.elements.ObserveRelayListForSearchAndDisplayIfNotFound
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.DisappearingScaffold
|
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.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
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.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.LevelFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.RenderFeedState
|
import com.vitorpamplona.amethyst.ui.screen.RenderFeedState
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
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.private.ThinSendButton
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ThinSendButton
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.public.ChannelHeader
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||||
@@ -213,7 +212,6 @@ import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent
|
|||||||
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
|
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@@ -858,7 +856,6 @@ private fun RenderClassifiedsReaderForThread(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var message by remember { mutableStateOf(TextFieldValue(msg)) }
|
var message by remember { mutableStateOf(TextFieldValue(msg)) }
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
TextField(
|
TextField(
|
||||||
value = message,
|
value = message,
|
||||||
@@ -881,9 +878,13 @@ private fun RenderClassifiedsReaderForThread(
|
|||||||
isActive = message.text.isNotBlank(),
|
isActive = message.text.isNotBlank(),
|
||||||
modifier = EditFieldTrailingIconModifier,
|
modifier = EditFieldTrailingIconModifier,
|
||||||
) {
|
) {
|
||||||
scope.launch(Dispatchers.IO) {
|
note.author?.let {
|
||||||
note.author?.let {
|
nav.nav {
|
||||||
nav.nav(routeToMessage(it, note.toNostrUri() + "\n\n" + msg, accountViewModel))
|
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 AuthorInfoVideoFeed = Modifier.width(75.dp).padding(end = 15.dp)
|
||||||
|
|
||||||
val messageDetailsModifier = Modifier.height(Size25dp)
|
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 =
|
val inlinePlaceholder =
|
||||||
Placeholder(
|
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 DarkChatBackground = DarkColorPalette.onSurface.copy(alpha = 0.12f)
|
||||||
private val LightChatBackground = LightColorPalette.onSurface.copy(alpha = 0.08f)
|
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 DarkOverPictureBackground = DarkColorPalette.background.copy(0.62f)
|
||||||
private val LightOverPictureBackground = LightColorPalette.background.copy(0.62f)
|
private val LightOverPictureBackground = LightColorPalette.background.copy(0.62f)
|
||||||
|
|
||||||
@@ -387,6 +390,9 @@ val ColorScheme.subtleBorder: Color
|
|||||||
val ColorScheme.chatBackground: Color
|
val ColorScheme.chatBackground: Color
|
||||||
get() = if (isLight) LightChatBackground else DarkChatBackground
|
get() = if (isLight) LightChatBackground else DarkChatBackground
|
||||||
|
|
||||||
|
val ColorScheme.chatDraftBackground: Color
|
||||||
|
get() = if (isLight) LightChatDraftBackground else DarkChatDraftBackground
|
||||||
|
|
||||||
val ColorScheme.subtleButton: Color
|
val ColorScheme.subtleButton: Color
|
||||||
get() = if (isLight) LightSubtleButton else DarkSubtleButton
|
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.TextRange
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
fun TextFieldValue.insertUrlAtCursor(url: String): TextFieldValue {
|
fun TextFieldValue.insertUrlAtCursor(url: String): TextFieldValue {
|
||||||
var toInsert = url.trim()
|
var toInsert = url.trim()
|
||||||
@@ -41,3 +43,54 @@ fun TextFieldValue.insertUrlAtCursor(url: String): TextFieldValue {
|
|||||||
TextRange(endOfUrlIndex, endOfUrlIndex),
|
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? =
|
fun parse(addressId: String): Address? =
|
||||||
try {
|
try {
|
||||||
val parts = addressId.split(":", limit = 3)
|
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])
|
Address(parts[0].toInt(), parts[1], parts[2])
|
||||||
} else {
|
} else {
|
||||||
Log.w("AddressableId", "Error parsing. Pubkey is not hex: $addressId")
|
Log.w("AddressableId", "Error parsing. Pubkey is not hex: $addressId")
|
||||||
|
|||||||
@@ -60,11 +60,12 @@ class ChatMessageEvent(
|
|||||||
|
|
||||||
fun reply(
|
fun reply(
|
||||||
msg: String,
|
msg: String,
|
||||||
reply: EventHintBundle<ChatMessageEvent>,
|
reply: EventHintBundle<BaseDMGroupEvent>,
|
||||||
createdAt: Long = TimeUtils.now(),
|
createdAt: Long = TimeUtils.now(),
|
||||||
initializer: TagArrayBuilder<ChatMessageEvent>.() -> Unit = {},
|
initializer: TagArrayBuilder<ChatMessageEvent>.() -> Unit = {},
|
||||||
) = eventTemplate(KIND, msg, createdAt) {
|
) = eventTemplate(KIND, msg, createdAt) {
|
||||||
alt(ALT_DESCRIPTION)
|
alt(ALT_DESCRIPTION)
|
||||||
|
reply(reply)
|
||||||
group((reply.event.recipients() + reply.toPTag()).distinctBy { it.pubKey })
|
group((reply.event.recipients() + reply.toPTag()).distinctBy { it.pubKey })
|
||||||
initializer()
|
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.nip01Core.tags.people.pTags
|
||||||
import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag
|
import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag
|
||||||
import com.vitorpamplona.quartz.nip14Subject.SubjectTag
|
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: 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)
|
fun TagArrayBuilder<ChatMessageEvent>.group(list: List<PTag>) = pTags(list)
|
||||||
|
|
||||||
|
|||||||
@@ -29,13 +29,15 @@ data class EmojiUrlTag(
|
|||||||
) {
|
) {
|
||||||
fun encode(): String = ":$code:$url"
|
fun encode(): String = ":$code:$url"
|
||||||
|
|
||||||
fun toContentEncode(): String = ":$code"
|
fun toContentEncode(): String = contentEncode(code)
|
||||||
|
|
||||||
fun toTagArray() = arrayOf("emoji", code, url)
|
fun toTagArray() = arrayOf("emoji", code, url)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG_NAME = "emoji"
|
const val TAG_NAME = "emoji"
|
||||||
|
|
||||||
|
fun contentEncode(code: String): String = ":$code:"
|
||||||
|
|
||||||
fun decode(encodedEmojiSetup: String): EmojiUrlTag? {
|
fun decode(encodedEmojiSetup: String): EmojiUrlTag? {
|
||||||
val emojiParts = encodedEmojiSetup.split(":", limit = 3)
|
val emojiParts = encodedEmojiSetup.split(":", limit = 3)
|
||||||
return if (emojiParts.size > 2) {
|
return if (emojiParts.size > 2) {
|
||||||
|
|||||||
@@ -35,3 +35,8 @@ class IMetaTagBuilder(
|
|||||||
|
|
||||||
fun build() = IMetaTag(url, properties)
|
fun build() = IMetaTag(url, properties)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun imetaTagBuilder(
|
||||||
|
url: String,
|
||||||
|
init: IMetaTagBuilder.() -> Unit,
|
||||||
|
) = IMetaTagBuilder(url).apply(init).build()
|
||||||
|
|||||||
Reference in New Issue
Block a user