Starts the migration of public chats to the new structure
Starts the migration of drafts to the new structure
This commit is contained in:
@@ -135,6 +135,7 @@ import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEve
|
|||||||
import com.vitorpamplona.quartz.nip30CustomEmoji.taggedEmojis
|
import com.vitorpamplona.quartz.nip30CustomEmoji.taggedEmojis
|
||||||
import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent
|
import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent
|
||||||
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning
|
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning
|
||||||
|
import com.vitorpamplona.quartz.nip37Drafts.DraftBuilder
|
||||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||||
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
|
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
|
||||||
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
|
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
|
||||||
@@ -2101,6 +2102,27 @@ class Account(
|
|||||||
Amethyst.instance.client.send(signedEvent, relayList = relayList)
|
Amethyst.instance.client.send(signedEvent, relayList = relayList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun sendNip95Privately(
|
||||||
|
data: FileStorageEvent,
|
||||||
|
signedEvent: FileStorageHeaderEvent,
|
||||||
|
relayList: List<String>,
|
||||||
|
) {
|
||||||
|
val connect =
|
||||||
|
relayList.map {
|
||||||
|
val normalizedUrl = RelayUrlFormatter.normalize(it)
|
||||||
|
RelaySetupInfoToConnect(
|
||||||
|
normalizedUrl,
|
||||||
|
shouldUseTorForClean(normalizedUrl),
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
setOf(FeedType.GLOBAL),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Amethyst.instance.client.sendPrivately(data, relayList = connect)
|
||||||
|
Amethyst.instance.client.sendPrivately(signedEvent, relayList = connect)
|
||||||
|
}
|
||||||
|
|
||||||
fun sendHeader(
|
fun sendHeader(
|
||||||
signedEvent: Event,
|
signedEvent: Event,
|
||||||
relayList: List<RelaySetupInfo>,
|
relayList: List<RelaySetupInfo>,
|
||||||
@@ -2284,6 +2306,29 @@ class Account(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <T : Event> signAndSendPrivately(
|
||||||
|
template: EventTemplate<T>,
|
||||||
|
relayList: List<String>,
|
||||||
|
) {
|
||||||
|
signer.sign(template) {
|
||||||
|
LocalCache.justConsume(it, null)
|
||||||
|
Amethyst.instance.client.sendPrivately(it, relayList = convertRelayList(relayList))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T : Event> signAndSend(
|
||||||
|
template: EventTemplate<T>,
|
||||||
|
relayList: List<RelaySetupInfo>,
|
||||||
|
broadcastNotes: Set<Note>,
|
||||||
|
) {
|
||||||
|
signer.sign(template) {
|
||||||
|
LocalCache.justConsume(it, null)
|
||||||
|
Amethyst.instance.client.send(it, relayList = relayList)
|
||||||
|
|
||||||
|
broadcastNotes.forEach { it.event?.let { Amethyst.instance.client.send(it, relayList = relayList) } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun <T : Event> signAndSend(
|
fun <T : Event> signAndSend(
|
||||||
draftTag: String?,
|
draftTag: String?,
|
||||||
template: EventTemplate<T>,
|
template: EventTemplate<T>,
|
||||||
@@ -2357,6 +2402,16 @@ class Account(
|
|||||||
signAndSend(draftTag, template, relayList, broadcastNotes)
|
signAndSend(draftTag, template, relayList, broadcastNotes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createAndSendDraft(
|
||||||
|
draftTag: String,
|
||||||
|
template: EventTemplate<out Event>,
|
||||||
|
) {
|
||||||
|
val rumor = signer.assembleRumor(template)
|
||||||
|
DraftBuilder.encryptAndSign(draftTag, rumor, signer) { draftEvent ->
|
||||||
|
sendDraftEvent(draftEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun deleteDraft(draftTag: String) {
|
fun deleteDraft(draftTag: String) {
|
||||||
val key = DraftEvent.createAddressTag(userProfile().pubkeyHex, draftTag)
|
val key = DraftEvent.createAddressTag(userProfile().pubkeyHex, draftTag)
|
||||||
LocalCache.getAddressableNoteIfExists(key)?.let { note ->
|
LocalCache.getAddressableNoteIfExists(key)?.let { note ->
|
||||||
@@ -2680,6 +2735,18 @@ class Account(
|
|||||||
LocalCache.justConsume(draftEvent, null)
|
LocalCache.justConsume(draftEvent, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun convertRelayList(broadcast: List<String>): List<RelaySetupInfoToConnect> =
|
||||||
|
broadcast.map {
|
||||||
|
val normalizedUrl = RelayUrlFormatter.normalize(it)
|
||||||
|
RelaySetupInfoToConnect(
|
||||||
|
normalizedUrl,
|
||||||
|
shouldUseTorForClean(normalizedUrl),
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
setOf(FeedType.GLOBAL),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun broadcastPrivately(signedEvents: NIP17Factory.Result) {
|
fun broadcastPrivately(signedEvents: NIP17Factory.Result) {
|
||||||
val mine = signedEvents.wraps.filter { (it.recipientPubKey() == signer.pubKey) }
|
val mine = signedEvents.wraps.filter { (it.recipientPubKey() == signer.pubKey) }
|
||||||
|
|
||||||
|
|||||||
@@ -2399,9 +2399,12 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
is ChannelMessageEvent -> {
|
is ChannelMessageEvent -> {
|
||||||
draft.channelId()?.let { channelId ->
|
draft.channelId()?.let { channelId ->
|
||||||
checkGetOrCreateChannel(channelId)?.let { channel ->
|
checkGetOrCreateChannel(channelId)?.addNote(note, null)
|
||||||
channel.addNote(note, null)
|
}
|
||||||
}
|
}
|
||||||
|
is LiveActivitiesChatMessageEvent -> {
|
||||||
|
draft.activityAddress()?.let { channelId ->
|
||||||
|
checkGetOrCreateChannel(channelId.toValue())?.addNote(note, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is TextNoteEvent -> {
|
is TextNoteEvent -> {
|
||||||
|
|||||||
@@ -1,155 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2024 Vitor Pamplona
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
* this software and associated documentation files (the "Software"), to deal in
|
|
||||||
* the Software without restriction, including without limitation the rights to use,
|
|
||||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
|
||||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
|
||||||
* subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
||||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
||||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
|
||||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
||||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
package com.vitorpamplona.amethyst.ui.actions
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material3.LocalTextStyle
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
|
||||||
import androidx.compose.ui.text.style.TextDirection
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.window.Dialog
|
|
||||||
import androidx.compose.ui.window.DialogProperties
|
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import com.vitorpamplona.amethyst.R
|
|
||||||
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.PostButton
|
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun NewChannelView(
|
|
||||||
onClose: () -> Unit,
|
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
channel: PublicChatChannel? = null,
|
|
||||||
) {
|
|
||||||
val postViewModel: NewChannelViewModel = viewModel()
|
|
||||||
postViewModel.load(accountViewModel.account, channel)
|
|
||||||
|
|
||||||
Dialog(
|
|
||||||
onDismissRequest = { onClose() },
|
|
||||||
properties =
|
|
||||||
DialogProperties(
|
|
||||||
dismissOnClickOutside = false,
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
Surface {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(10.dp).verticalScroll(rememberScrollState()),
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
CloseButton(
|
|
||||||
onPress = {
|
|
||||||
postViewModel.clear()
|
|
||||||
onClose()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
PostButton(
|
|
||||||
onPost = {
|
|
||||||
postViewModel.create()
|
|
||||||
onClose()
|
|
||||||
},
|
|
||||||
postViewModel.channelName.value.text
|
|
||||||
.isNotBlank(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(15.dp))
|
|
||||||
|
|
||||||
OutlinedTextField(
|
|
||||||
label = { Text(text = stringRes(R.string.channel_name)) },
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
value = postViewModel.channelName.value,
|
|
||||||
onValueChange = { postViewModel.channelName.value = it },
|
|
||||||
placeholder = {
|
|
||||||
Text(
|
|
||||||
text = stringRes(R.string.my_awesome_group),
|
|
||||||
color = MaterialTheme.colorScheme.placeholderText,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
keyboardOptions =
|
|
||||||
KeyboardOptions.Default.copy(
|
|
||||||
capitalization = KeyboardCapitalization.Sentences,
|
|
||||||
),
|
|
||||||
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(15.dp))
|
|
||||||
|
|
||||||
OutlinedTextField(
|
|
||||||
label = { Text(text = stringRes(R.string.picture_url)) },
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
value = postViewModel.channelPicture.value,
|
|
||||||
onValueChange = { postViewModel.channelPicture.value = it },
|
|
||||||
placeholder = {
|
|
||||||
Text(
|
|
||||||
text = "http://mygroup.com/logo.jpg",
|
|
||||||
color = MaterialTheme.colorScheme.placeholderText,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(15.dp))
|
|
||||||
|
|
||||||
OutlinedTextField(
|
|
||||||
label = { Text(text = stringRes(R.string.description)) },
|
|
||||||
modifier = Modifier.fillMaxWidth().height(100.dp),
|
|
||||||
value = postViewModel.channelDescription.value,
|
|
||||||
onValueChange = { postViewModel.channelDescription.value = it },
|
|
||||||
placeholder = {
|
|
||||||
Text(
|
|
||||||
text = stringRes(R.string.about_us),
|
|
||||||
color = MaterialTheme.colorScheme.placeholderText,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
keyboardOptions =
|
|
||||||
KeyboardOptions.Default.copy(
|
|
||||||
capitalization = KeyboardCapitalization.Sentences,
|
|
||||||
),
|
|
||||||
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
|
||||||
maxLines = 10,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -63,7 +63,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarks.BookmarkListScree
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.list.MessagesScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.list.MessagesScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomByAuthorScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomByAuthorScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.public.ChannelScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.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
|
||||||
|
|||||||
@@ -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.chats.public.ChannelHeader
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ChannelHeader
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.public.EndedFlag
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.EndedFlag
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.public.LiveFlag
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveFlag
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.public.OfflineFlag
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.OfflineFlag
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.public.ScheduledFlag
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.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
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ 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.chats.public.RenderChannelHeader
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.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
|
||||||
|
|||||||
@@ -23,12 +23,18 @@ package com.vitorpamplona.amethyst.ui.note.elements
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
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.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.ui.note.timeAgo
|
import com.vitorpamplona.amethyst.ui.note.timeAgo
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.timeAgoShort
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -48,3 +54,21 @@ fun TimeAgo(time: Long) {
|
|||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NormalTimeAgo(
|
||||||
|
baseNote: Note,
|
||||||
|
modifier: Modifier,
|
||||||
|
) {
|
||||||
|
val nowStr = stringRes(id = R.string.now)
|
||||||
|
|
||||||
|
val time by
|
||||||
|
remember(baseNote) { derivedStateOf { timeAgoShort(baseNote.createdAt() ?: 0, nowStr) } }
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = time,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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.chats.public.ChannelHeader
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.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
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ import androidx.compose.foundation.layout.Spacer
|
|||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -36,9 +38,11 @@ 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.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
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
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
|
||||||
@@ -60,12 +64,12 @@ import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
|||||||
import com.vitorpamplona.amethyst.ui.note.ZapReaction
|
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.note.elements.NormalTimeAgo
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.public.JoinCommunityButton
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.public.LeaveCommunityButton
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.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.ButtonBorder
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.ButtonPadding
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier
|
import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing
|
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing
|
||||||
@@ -82,6 +86,8 @@ import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefiniti
|
|||||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.ModeratorTag
|
import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.ModeratorTag
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -359,3 +365,47 @@ fun WatchAddressableNoteFollows(
|
|||||||
|
|
||||||
onFollowChanges(state.addresses.contains(note.idHex))
|
onFollowChanges(state.addresses.contains(note.idHex))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun JoinCommunityButton(
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
note: AddressableNote,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.padding(horizontal = 3.dp),
|
||||||
|
onClick = { scope.launch(Dispatchers.IO) { accountViewModel.account.follow(note) } },
|
||||||
|
shape = ButtonBorder,
|
||||||
|
colors =
|
||||||
|
ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
contentPadding = ButtonPadding,
|
||||||
|
) {
|
||||||
|
Text(text = stringRes(R.string.join), color = Color.White)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LeaveCommunityButton(
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
note: AddressableNote,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.padding(horizontal = 3.dp),
|
||||||
|
onClick = { scope.launch(Dispatchers.IO) { accountViewModel.account.unfollow(note) } },
|
||||||
|
shape = ButtonBorder,
|
||||||
|
colors =
|
||||||
|
ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
contentPadding = ButtonPadding,
|
||||||
|
) {
|
||||||
|
Text(text = stringRes(R.string.leave), color = Color.White)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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.chats.public.LiveFlag
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveFlag
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.public.ScheduledFlag
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.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.chats.public.ChannelHeader
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.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
|
||||||
|
|||||||
+11
@@ -79,6 +79,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event
|
|||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||||
import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata
|
import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag
|
import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
|
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
|
||||||
@@ -860,6 +861,14 @@ class AccountViewModel(
|
|||||||
account.decryptZapContentAuthor(note, onReady)
|
account.decryptZapContentAuthor(note, onReady)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun follow(channel: Channel) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) { account.follow(channel) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unfollow(channel: Channel) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) { account.unfollow(channel) }
|
||||||
|
}
|
||||||
|
|
||||||
fun follow(user: User) {
|
fun follow(user: User) {
|
||||||
viewModelScope.launch(Dispatchers.IO) { account.follow(user) }
|
viewModelScope.launch(Dispatchers.IO) { account.follow(user) }
|
||||||
}
|
}
|
||||||
@@ -1054,6 +1063,8 @@ class AccountViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <T : Event> createRumor(template: EventTemplate<T>) = account.signer.assembleRumor<T>(template)
|
||||||
|
|
||||||
fun retrieveRelayDocument(
|
fun retrieveRelayDocument(
|
||||||
dirtyUrl: String,
|
dirtyUrl: String,
|
||||||
onInfo: (Nip11RelayInformation) -> Unit,
|
onInfo: (Nip11RelayInformation) -> Unit,
|
||||||
|
|||||||
+2
-2
@@ -48,10 +48,10 @@ import androidx.compose.ui.graphics.graphicsLayer
|
|||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
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.actions.NewChannelView
|
|
||||||
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.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.metadata.ChannelMetadataDialog
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Font12SP
|
import com.vitorpamplona.amethyst.ui.theme.Font12SP
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||||
@@ -66,7 +66,7 @@ fun ChannelFabColumn(
|
|||||||
var wantsToCreateChannel by remember { mutableStateOf(false) }
|
var wantsToCreateChannel by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
if (wantsToCreateChannel) {
|
if (wantsToCreateChannel) {
|
||||||
NewChannelView({ wantsToCreateChannel = false }, accountViewModel = accountViewModel)
|
ChannelMetadataDialog({ wantsToCreateChannel = false }, accountViewModel = accountViewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
|
|||||||
+2
-2
@@ -44,7 +44,7 @@ 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.chats.list.ChannelFabColumn
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.list.ChannelFabColumn
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.Chatroom
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.Chatroom
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.public.Channel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ChannelView
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -113,7 +113,7 @@ fun MessagesTwoPane(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (it.route == "Channel") {
|
if (it.route == "Channel") {
|
||||||
Channel(
|
ChannelView(
|
||||||
channelId = it.id,
|
channelId = it.id,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
|
|||||||
+2
-2
@@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
@@ -45,6 +44,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.feed.RefreshingChatroomFeedView
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.feed.RefreshingChatroomFeedView
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.ChatNewMessageViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.ChatNewMessageViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.PrivateMessageEditFieldRow
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.PrivateMessageEditFieldRow
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -176,7 +176,7 @@ fun ChatroomViewUI(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(10.dp))
|
Spacer(modifier = DoubleVertSpacer)
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -69,6 +69,7 @@ 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.Font12SP
|
import com.vitorpamplona.amethyst.ui.theme.Font12SP
|
||||||
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
|
||||||
@@ -379,7 +380,7 @@ private fun RenderDraftEvent(
|
|||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
ObserveDraftEvent(note, accountViewModel) {
|
ObserveDraftEvent(note, accountViewModel) {
|
||||||
Column {
|
Column(verticalArrangement = RowColSpacing5dp) {
|
||||||
RenderReplyRow(
|
RenderReplyRow(
|
||||||
note = it,
|
note = it,
|
||||||
innerQuote = innerQuote,
|
innerQuote = innerQuote,
|
||||||
|
|||||||
+27
-2
@@ -57,6 +57,7 @@ import com.vitorpamplona.quartz.nip14Subject.subject
|
|||||||
import com.vitorpamplona.quartz.nip17Dm.base.BaseDMGroupEvent
|
import com.vitorpamplona.quartz.nip17Dm.base.BaseDMGroupEvent
|
||||||
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.base.NIP17Group
|
||||||
|
import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent
|
||||||
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
||||||
import com.vitorpamplona.quartz.nip18Reposts.quotes.quotes
|
import com.vitorpamplona.quartz.nip18Reposts.quotes.quotes
|
||||||
import com.vitorpamplona.quartz.nip19Bech32.toNpub
|
import com.vitorpamplona.quartz.nip19Bech32.toNpub
|
||||||
@@ -179,6 +180,12 @@ open class ChatNewMessageViewModel : ViewModel() {
|
|||||||
|
|
||||||
open fun reply(replyNote: Note) {
|
open fun reply(replyNote: Note) {
|
||||||
replyTo.value = replyNote
|
replyTo.value = replyNote
|
||||||
|
saveDraft()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearReply() {
|
||||||
|
replyTo.value = null
|
||||||
|
saveDraft()
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun quote(quote: Note) {
|
open fun quote(quote: Note) {
|
||||||
@@ -224,8 +231,6 @@ open class ChatNewMessageViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun loadFromDraft(draft: Note) {
|
private fun loadFromDraft(draft: Note) {
|
||||||
Log.d("draft", draft.event!!.toJson())
|
|
||||||
|
|
||||||
val draftEvent = draft.event ?: return
|
val draftEvent = draft.event ?: return
|
||||||
val accountViewModel = accountViewModel ?: return
|
val accountViewModel = accountViewModel ?: return
|
||||||
|
|
||||||
@@ -264,9 +269,29 @@ open class ChatNewMessageViewModel : ViewModel() {
|
|||||||
TextFieldValue(
|
TextFieldValue(
|
||||||
draftEvent.groupMembers().mapNotNull { runCatching { Hex.decode(it).toNpub() }.getOrNull() }.joinToString(", ") { "@$it" },
|
draftEvent.groupMembers().mapNotNull { runCatching { Hex.decode(it).toNpub() }.getOrNull() }.joinToString(", ") { "@$it" },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val replyId =
|
||||||
|
when (draftEvent) {
|
||||||
|
is ChatMessageEvent -> draftEvent.replyTo().lastOrNull()
|
||||||
|
is ChatMessageEncryptedFileHeaderEvent -> draftEvent.replyTo().lastOrNull()
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (replyId != null) {
|
||||||
|
accountViewModel.checkGetOrCreateNote(replyId) {
|
||||||
|
replyTo.value = it
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (draftEvent is PrivateDmEvent) {
|
} else if (draftEvent is PrivateDmEvent) {
|
||||||
val recepientNpub = draftEvent.verifiedRecipientPubKey()?.let { Hex.decode(it).toNpub() }
|
val recepientNpub = draftEvent.verifiedRecipientPubKey()?.let { Hex.decode(it).toNpub() }
|
||||||
toUsers = TextFieldValue("@$recepientNpub")
|
toUsers = TextFieldValue("@$recepientNpub")
|
||||||
|
|
||||||
|
val replyId = draftEvent.replyTo()
|
||||||
|
if (replyId != null) {
|
||||||
|
accountViewModel.checkGetOrCreateNote(replyId) {
|
||||||
|
replyTo.value = it
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message =
|
message =
|
||||||
|
|||||||
+30
-7
@@ -42,12 +42,15 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
|
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
|
||||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
|
||||||
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
|
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||||
import com.vitorpamplona.amethyst.ui.note.IncognitoIconOff
|
import com.vitorpamplona.amethyst.ui.note.IncognitoIconOff
|
||||||
import com.vitorpamplona.amethyst.ui.note.IncognitoIconOn
|
import com.vitorpamplona.amethyst.ui.note.IncognitoIconOn
|
||||||
@@ -58,11 +61,14 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.ChatFileUploadDialog
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.ChatFileUploadDialog
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ThinSendButton
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ThinSendButton
|
||||||
|
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.EditFieldBorder
|
import com.vitorpamplona.amethyst.ui.theme.EditFieldBorder
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.EditFieldLeadingIconModifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.EditFieldModifier
|
import com.vitorpamplona.amethyst.ui.theme.EditFieldModifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.EditFieldTrailingIconModifier
|
import com.vitorpamplona.amethyst.ui.theme.EditFieldTrailingIconModifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size30Modifier
|
import com.vitorpamplona.amethyst.ui.theme.Size30Modifier
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
||||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
@@ -70,6 +76,23 @@ import kotlinx.coroutines.flow.debounce
|
|||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun PrivateMessageEditFieldRow() {
|
||||||
|
val channelScreenModel: ChatNewMessageViewModel = viewModel()
|
||||||
|
val accountViewModel = mockAccountViewModel()
|
||||||
|
channelScreenModel.init(accountViewModel)
|
||||||
|
|
||||||
|
ThemeComparisonRow {
|
||||||
|
PrivateMessageEditFieldRow(
|
||||||
|
channelScreenModel = channelScreenModel,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
onSendNewMessage = {},
|
||||||
|
nav = EmptyNav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PrivateMessageEditFieldRow(
|
fun PrivateMessageEditFieldRow(
|
||||||
channelScreenModel: ChatNewMessageViewModel,
|
channelScreenModel: ChatNewMessageViewModel,
|
||||||
@@ -77,7 +100,11 @@ fun PrivateMessageEditFieldRow(
|
|||||||
onSendNewMessage: () -> Unit,
|
onSendNewMessage: () -> Unit,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
channelScreenModel.replyTo.value?.let { DisplayReplyingToNote(it, accountViewModel, nav) { channelScreenModel.replyTo.value = null } }
|
channelScreenModel.replyTo.value?.let {
|
||||||
|
DisplayReplyingToNote(it, accountViewModel, nav) {
|
||||||
|
channelScreenModel.clearReply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(key1 = channelScreenModel.draftTag) {
|
LaunchedEffect(key1 = channelScreenModel.draftTag) {
|
||||||
launch(Dispatchers.IO) {
|
launch(Dispatchers.IO) {
|
||||||
@@ -148,8 +175,7 @@ fun PrivateMessageEditFieldRow(
|
|||||||
},
|
},
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
ThinSendButton(
|
ThinSendButton(
|
||||||
isActive =
|
isActive = channelScreenModel.message.text.isNotBlank() && !channelScreenModel.isUploadingImage,
|
||||||
channelScreenModel.message.text.isNotBlank() && !channelScreenModel.isUploadingImage,
|
|
||||||
modifier = EditFieldTrailingIconModifier,
|
modifier = EditFieldTrailingIconModifier,
|
||||||
) {
|
) {
|
||||||
channelScreenModel.sendPost(onSendNewMessage)
|
channelScreenModel.sendPost(onSendNewMessage)
|
||||||
@@ -163,10 +189,7 @@ fun PrivateMessageEditFieldRow(
|
|||||||
SelectFromGallery(
|
SelectFromGallery(
|
||||||
isUploading = channelScreenModel.isUploadingImage,
|
isUploading = channelScreenModel.isUploadingImage,
|
||||||
tint = MaterialTheme.colorScheme.placeholderText,
|
tint = MaterialTheme.colorScheme.placeholderText,
|
||||||
modifier =
|
modifier = EditFieldLeadingIconModifier,
|
||||||
Modifier
|
|
||||||
.size(30.dp)
|
|
||||||
.padding(start = 2.dp),
|
|
||||||
onImageChosen = channelScreenModel::pickedMedia,
|
onImageChosen = channelScreenModel::pickedMedia,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -208,8 +208,8 @@ private fun ImageVideoPostChat(
|
|||||||
title = R.string.add_sensitive_content_label,
|
title = R.string.add_sensitive_content_label,
|
||||||
description = R.string.add_sensitive_content_description,
|
description = R.string.add_sensitive_content_description,
|
||||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||||
checked = fileUploadState.sensitiveContent,
|
checked = fileUploadState.contentWarning,
|
||||||
onCheckedChange = { fileUploadState.sensitiveContent = it },
|
onCheckedChange = fileUploadState::updateContentWarning,
|
||||||
)
|
)
|
||||||
|
|
||||||
SettingsRow(R.string.file_server, R.string.file_server_description) {
|
SettingsRow(R.string.file_server, R.string.file_server_description) {
|
||||||
|
|||||||
+14
-1
@@ -40,7 +40,11 @@ class ChatFileUploadState(
|
|||||||
|
|
||||||
var selectedServer by mutableStateOf(defaultServer)
|
var selectedServer by mutableStateOf(defaultServer)
|
||||||
var caption by mutableStateOf("")
|
var caption by mutableStateOf("")
|
||||||
var sensitiveContent by mutableStateOf(false)
|
|
||||||
|
var contentWarning by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var contentWarningReason by mutableStateOf<String?>(null)
|
||||||
|
|
||||||
// Images and Videos
|
// Images and Videos
|
||||||
var multiOrchestrator by mutableStateOf<MultiOrchestrator?>(null)
|
var multiOrchestrator by mutableStateOf<MultiOrchestrator?>(null)
|
||||||
@@ -72,4 +76,13 @@ class ChatFileUploadState(
|
|||||||
fun canPost(): Boolean = !isUploadingImage && multiOrchestrator != null
|
fun canPost(): Boolean = !isUploadingImage && multiOrchestrator != null
|
||||||
|
|
||||||
fun hasPickedMedia() = multiOrchestrator != null
|
fun hasPickedMedia() = multiOrchestrator != null
|
||||||
|
|
||||||
|
fun updateContentWarning(value: Boolean) {
|
||||||
|
contentWarning = value
|
||||||
|
if (value) {
|
||||||
|
contentWarningReason = ""
|
||||||
|
} else {
|
||||||
|
contentWarningReason = null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-6
@@ -59,7 +59,7 @@ class ChatFileUploader(
|
|||||||
orchestrator.uploadEncrypted(
|
orchestrator.uploadEncrypted(
|
||||||
scope,
|
scope,
|
||||||
viewState.caption,
|
viewState.caption,
|
||||||
if (viewState.sensitiveContent) "" else null,
|
viewState.contentWarningReason,
|
||||||
MediaCompressor.intToCompressorQuality(viewState.mediaQualitySlider),
|
MediaCompressor.intToCompressorQuality(viewState.mediaQualitySlider),
|
||||||
cipher,
|
cipher,
|
||||||
viewState.selectedServer,
|
viewState.selectedServer,
|
||||||
@@ -88,9 +88,7 @@ class ChatFileUploader(
|
|||||||
alt(viewState.caption)
|
alt(viewState.caption)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (viewState.sensitiveContent) {
|
viewState.contentWarningReason?.let { contentWarning(it) }
|
||||||
contentWarning("")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
account.sendNIP17EncryptedFile(template)
|
account.sendNIP17EncryptedFile(template)
|
||||||
@@ -125,7 +123,7 @@ class ChatFileUploader(
|
|||||||
orchestrator.upload(
|
orchestrator.upload(
|
||||||
scope,
|
scope,
|
||||||
viewState.caption,
|
viewState.caption,
|
||||||
if (viewState.sensitiveContent) "" else null,
|
viewState.contentWarningReason,
|
||||||
MediaCompressor.intToCompressorQuality(viewState.mediaQualitySlider),
|
MediaCompressor.intToCompressorQuality(viewState.mediaQualitySlider),
|
||||||
viewState.selectedServer,
|
viewState.selectedServer,
|
||||||
account,
|
account,
|
||||||
@@ -136,7 +134,7 @@ class ChatFileUploader(
|
|||||||
results.successful.forEach {
|
results.successful.forEach {
|
||||||
if (it.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
|
if (it.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
|
||||||
val iMetaAttachments = IMetaAttachments()
|
val iMetaAttachments = IMetaAttachments()
|
||||||
iMetaAttachments.add(it.result, viewState.caption, if (viewState.sensitiveContent) "" else null)
|
iMetaAttachments.add(it.result, viewState.caption, viewState.contentWarningReason)
|
||||||
|
|
||||||
account.sendPrivateMessage(
|
account.sendPrivateMessage(
|
||||||
message = it.result.url,
|
message = it.result.url,
|
||||||
|
|||||||
-1275
File diff suppressed because it is too large
Load Diff
+91
@@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.LoadChannel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.PublicChatChannelHeader
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveActivitiesChannelHeader
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.innerPostModifier
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RenderChannelHeader(
|
||||||
|
channelNote: Note,
|
||||||
|
showVideo: Boolean,
|
||||||
|
sendToChannel: Boolean,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
channelNote.channelHex()?.let {
|
||||||
|
ChannelHeader(
|
||||||
|
channelHex = it,
|
||||||
|
showVideo = showVideo,
|
||||||
|
sendToChannel = sendToChannel,
|
||||||
|
modifier = MaterialTheme.colorScheme.innerPostModifier.padding(Size10dp),
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChannelHeader(
|
||||||
|
channelHex: String,
|
||||||
|
showVideo: Boolean,
|
||||||
|
showFlag: Boolean = true,
|
||||||
|
sendToChannel: Boolean = false,
|
||||||
|
modifier: Modifier = StdPadding,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
LoadChannel(channelHex, accountViewModel) {
|
||||||
|
when (it) {
|
||||||
|
is LiveActivitiesChannel ->
|
||||||
|
LiveActivitiesChannelHeader(
|
||||||
|
it,
|
||||||
|
showVideo,
|
||||||
|
showFlag,
|
||||||
|
sendToChannel,
|
||||||
|
modifier,
|
||||||
|
accountViewModel,
|
||||||
|
nav,
|
||||||
|
)
|
||||||
|
is PublicChatChannel ->
|
||||||
|
PublicChatChannelHeader(
|
||||||
|
it,
|
||||||
|
sendToChannel,
|
||||||
|
modifier,
|
||||||
|
accountViewModel,
|
||||||
|
nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||||
|
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.LoadChannel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.DisappearingScaffold
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.PublicChatTopBar
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.header.LiveActivityTopBar
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChannelScreen(
|
||||||
|
channelId: String?,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
if (channelId == null) return
|
||||||
|
|
||||||
|
DisappearingScaffold(
|
||||||
|
isInvertedLayout = true,
|
||||||
|
topBar = {
|
||||||
|
LoadChannel(channelId, accountViewModel) {
|
||||||
|
when (it) {
|
||||||
|
is PublicChatChannel -> PublicChatTopBar(it, accountViewModel, nav)
|
||||||
|
is LiveActivitiesChannel -> LiveActivityTopBar(it, accountViewModel, nav)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
) {
|
||||||
|
Column(Modifier.padding(it)) {
|
||||||
|
ChannelView(channelId, accountViewModel, nav)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+181
@@ -0,0 +1,181 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.vitorpamplona.amethyst.model.Channel
|
||||||
|
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||||
|
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
|
||||||
|
import com.vitorpamplona.amethyst.service.NostrChannelDataSource.channel
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.LoadChannel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.NostrChannelFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.feed.RefreshingChatroomFeedView
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.ShowVideoStreaming
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.ChannelNewMessageViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.EditFieldRow
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChannelView(
|
||||||
|
channelId: String?,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
if (channelId == null) return
|
||||||
|
|
||||||
|
LoadChannel(channelId, accountViewModel) {
|
||||||
|
PrepareChannelViewModels(
|
||||||
|
baseChannel = it,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PrepareChannelViewModels(
|
||||||
|
baseChannel: Channel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
val feedViewModel: NostrChannelFeedViewModel =
|
||||||
|
viewModel(
|
||||||
|
key = baseChannel.idHex + "ChannelFeedViewModel",
|
||||||
|
factory =
|
||||||
|
NostrChannelFeedViewModel.Factory(
|
||||||
|
baseChannel,
|
||||||
|
accountViewModel.account,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val channelScreenModel: ChannelNewMessageViewModel = viewModel()
|
||||||
|
channelScreenModel.init(accountViewModel)
|
||||||
|
channelScreenModel.load(baseChannel)
|
||||||
|
|
||||||
|
ChannelView(
|
||||||
|
channel = baseChannel,
|
||||||
|
feedViewModel = feedViewModel,
|
||||||
|
newPostModel = channelScreenModel,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChannelView(
|
||||||
|
channel: Channel,
|
||||||
|
feedViewModel: NostrChannelFeedViewModel,
|
||||||
|
newPostModel: ChannelNewMessageViewModel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
NostrChannelDataSource.loadMessagesBetween(accountViewModel.account, channel)
|
||||||
|
|
||||||
|
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||||
|
|
||||||
|
DisposableEffect(accountViewModel) {
|
||||||
|
NostrChannelDataSource.loadMessagesBetween(accountViewModel.account, channel)
|
||||||
|
NostrChannelDataSource.start()
|
||||||
|
feedViewModel.invalidateData(true)
|
||||||
|
|
||||||
|
onDispose {
|
||||||
|
NostrChannelDataSource.clear()
|
||||||
|
NostrChannelDataSource.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DisposableEffect(lifeCycleOwner) {
|
||||||
|
val observer =
|
||||||
|
LifecycleEventObserver { _, event ->
|
||||||
|
if (event == Lifecycle.Event.ON_RESUME) {
|
||||||
|
println("Channel Start")
|
||||||
|
NostrChannelDataSource.start()
|
||||||
|
feedViewModel.invalidateData(true)
|
||||||
|
}
|
||||||
|
if (event == Lifecycle.Event.ON_PAUSE) {
|
||||||
|
println("Channel Stop")
|
||||||
|
|
||||||
|
NostrChannelDataSource.clear()
|
||||||
|
NostrChannelDataSource.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lifeCycleOwner.lifecycle.addObserver(observer)
|
||||||
|
onDispose { lifeCycleOwner.lifecycle.removeObserver(observer) }
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(Modifier.fillMaxHeight()) {
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
remember {
|
||||||
|
Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.padding(vertical = 0.dp)
|
||||||
|
.weight(1f, true)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
if (channel is LiveActivitiesChannel) {
|
||||||
|
ShowVideoStreaming(channel, accountViewModel)
|
||||||
|
}
|
||||||
|
RefreshingChatroomFeedView(
|
||||||
|
viewModel = feedViewModel,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
routeForLastRead = "Channel/${channel.idHex}",
|
||||||
|
avoidDraft = newPostModel.draftTag,
|
||||||
|
onWantsToReply = newPostModel::reply,
|
||||||
|
onWantsToEditDraft = newPostModel::editFromDraft,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = DoubleVertSpacer)
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
// LAST ROW
|
||||||
|
EditFieldRow(
|
||||||
|
newPostModel,
|
||||||
|
accountViewModel,
|
||||||
|
onSendNewMessage = {
|
||||||
|
scope.launch {
|
||||||
|
feedViewModel.sendToTop()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+120
@@ -0,0 +1,120 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.lifecycle.distinctUntilChanged
|
||||||
|
import androidx.lifecycle.map
|
||||||
|
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.LikeReaction
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.ZapReaction
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions.EditButton
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions.JoinChatButton
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions.LeaveChatButton
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.tags.events.isTaggedEvent
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ShortChannelActionOptions(
|
||||||
|
channel: PublicChatChannel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
LoadNote(baseNoteHex = channel.idHex, accountViewModel) {
|
||||||
|
it?.let {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = RowColSpacing) {
|
||||||
|
LikeReaction(
|
||||||
|
baseNote = it,
|
||||||
|
grayTint = MaterialTheme.colorScheme.onSurface,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav,
|
||||||
|
)
|
||||||
|
ZapReaction(
|
||||||
|
baseNote = it,
|
||||||
|
grayTint = MaterialTheme.colorScheme.onSurface,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
Spacer(modifier = StdHorzSpacer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WatchChannelFollows(channel, accountViewModel) { isFollowing ->
|
||||||
|
if (!isFollowing) {
|
||||||
|
JoinChatButton(channel, accountViewModel, nav)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LongChannelActionOptions(
|
||||||
|
channel: PublicChatChannel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
val isMe by
|
||||||
|
remember(accountViewModel) {
|
||||||
|
derivedStateOf { channel.creator == accountViewModel.account.userProfile() }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMe) {
|
||||||
|
EditButton(channel, accountViewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
WatchChannelFollows(channel, accountViewModel) { isFollowing ->
|
||||||
|
if (isFollowing) {
|
||||||
|
LeaveChatButton(channel, accountViewModel, nav)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun WatchChannelFollows(
|
||||||
|
channel: PublicChatChannel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
content: @Composable (Boolean) -> Unit,
|
||||||
|
) {
|
||||||
|
val isFollowing by
|
||||||
|
accountViewModel
|
||||||
|
.userProfile()
|
||||||
|
.live()
|
||||||
|
.follows
|
||||||
|
.map { it.user.latestContactList?.isTaggedEvent(channel.idHex) ?: false }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.observeAsState(
|
||||||
|
accountViewModel.userProfile().latestContactList?.isTaggedEvent(channel.idHex) ?: false,
|
||||||
|
)
|
||||||
|
|
||||||
|
content(isFollowing)
|
||||||
|
}
|
||||||
+123
@@ -0,0 +1,123 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.elements.NormalTimeAgo
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||||
|
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LongPublicChatChannelHeader(
|
||||||
|
baseChannel: PublicChatChannel,
|
||||||
|
lineModifier: Modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp),
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
val channelState by baseChannel.live.observeAsState()
|
||||||
|
val channel = channelState?.channel as? PublicChatChannel ?: return
|
||||||
|
|
||||||
|
Row(lineModifier) {
|
||||||
|
val summary = remember(channelState) { channel.summary()?.ifBlank { null } }
|
||||||
|
|
||||||
|
Column(Modifier.weight(1f)) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
val defaultBackground = MaterialTheme.colorScheme.background
|
||||||
|
val background = remember { mutableStateOf(defaultBackground) }
|
||||||
|
|
||||||
|
TranslatableRichTextViewer(
|
||||||
|
content = summary ?: stringRes(id = R.string.groups_no_descriptor),
|
||||||
|
canPreview = false,
|
||||||
|
quotesLeft = 1,
|
||||||
|
tags = EmptyTagList,
|
||||||
|
backgroundColor = background,
|
||||||
|
id = baseChannel.idHex,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(DoubleHorzSpacer)
|
||||||
|
LongChannelActionOptions(channel, accountViewModel, nav)
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadNote(baseNoteHex = channel.idHex, accountViewModel) { loadingNote ->
|
||||||
|
loadingNote?.let { note ->
|
||||||
|
Row(
|
||||||
|
lineModifier,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringRes(id = R.string.owner),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.width(75.dp),
|
||||||
|
)
|
||||||
|
Spacer(DoubleHorzSpacer)
|
||||||
|
NoteAuthorPicture(note, nav, accountViewModel, Size25dp)
|
||||||
|
Spacer(DoubleHorzSpacer)
|
||||||
|
NoteUsernameDisplay(note, Modifier.weight(1f), accountViewModel = accountViewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
lineModifier,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringRes(id = R.string.created_at),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.width(75.dp),
|
||||||
|
)
|
||||||
|
Spacer(DoubleHorzSpacer)
|
||||||
|
NormalTimeAgo(note, remember { Modifier.weight(1f) })
|
||||||
|
MoreOptionsButton(note, null, accountViewModel, nav)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+70
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PublicChatChannelHeader(
|
||||||
|
baseChannel: PublicChatChannel,
|
||||||
|
sendToChannel: Boolean = false,
|
||||||
|
modifier: Modifier = StdPadding,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
Column(Modifier.fillMaxWidth()) {
|
||||||
|
val expanded = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
modifier =
|
||||||
|
modifier.clickable {
|
||||||
|
if (sendToChannel) {
|
||||||
|
nav.nav(routeFor(baseChannel))
|
||||||
|
} else {
|
||||||
|
expanded.value = !expanded.value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
ShortPublicChatChannelHeader(
|
||||||
|
baseChannel = baseChannel,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (expanded.value) {
|
||||||
|
LongPublicChatChannelHeader(baseChannel = baseChannel, accountViewModel = accountViewModel, nav = nav)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.TopBarExtensibleWithBackButton
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PublicChatTopBar(
|
||||||
|
baseChannel: PublicChatChannel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
TopBarExtensibleWithBackButton(
|
||||||
|
title = {
|
||||||
|
ShortPublicChatChannelHeader(
|
||||||
|
baseChannel = baseChannel,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
extendableRow = {
|
||||||
|
LongPublicChatChannelHeader(baseChannel = baseChannel, accountViewModel = accountViewModel, nav = nav)
|
||||||
|
},
|
||||||
|
popBack = nav::popBack,
|
||||||
|
)
|
||||||
|
}
|
||||||
+96
@@ -0,0 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.FeatureSetType
|
||||||
|
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ShortPublicChatChannelHeader(
|
||||||
|
baseChannel: PublicChatChannel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
val channelState by baseChannel.live.observeAsState()
|
||||||
|
val channel = channelState?.channel as? PublicChatChannel ?: return
|
||||||
|
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
channel.profilePicture()?.let {
|
||||||
|
RobohashFallbackAsyncImage(
|
||||||
|
robot = channel.idHex,
|
||||||
|
model = it,
|
||||||
|
contentDescription = stringRes(R.string.profile_image),
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = HeaderPictureModifier,
|
||||||
|
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
||||||
|
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.padding(start = 10.dp)
|
||||||
|
.height(35.dp)
|
||||||
|
.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = remember(channelState) { channel.toBestDisplayName() },
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.height(Size35dp)
|
||||||
|
.padding(start = 5.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
ShortChannelActionOptions(channel, accountViewModel, nav)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+69
@@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.EditNote
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.metadata.ChannelMetadataDialog
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.ZeroPadding
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EditButton(
|
||||||
|
channel: PublicChatChannel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
|
var wantsToPost by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (wantsToPost) {
|
||||||
|
ChannelMetadataDialog({ wantsToPost = false }, accountViewModel, channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(horizontal = 3.dp)
|
||||||
|
.width(50.dp),
|
||||||
|
onClick = { wantsToPost = true },
|
||||||
|
contentPadding = ZeroPadding,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
tint = Color.White,
|
||||||
|
imageVector = Icons.Default.EditNote,
|
||||||
|
contentDescription = stringRes(R.string.edits_the_channel_metadata),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions
|
||||||
|
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.ButtonPadding
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.HalfHalfHorzModifier
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun JoinChatButton(
|
||||||
|
channel: PublicChatChannel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
modifier = HalfHalfHorzModifier,
|
||||||
|
onClick = { accountViewModel.follow(channel) },
|
||||||
|
contentPadding = ButtonPadding,
|
||||||
|
) {
|
||||||
|
Text(text = stringRes(R.string.join), color = Color.White)
|
||||||
|
}
|
||||||
|
}
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions
|
||||||
|
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.ButtonPadding
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.HalfHalfHorzModifier
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LeaveChatButton(
|
||||||
|
channel: PublicChatChannel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
modifier = HalfHalfHorzModifier,
|
||||||
|
onClick = { accountViewModel.unfollow(channel) },
|
||||||
|
contentPadding = ButtonPadding,
|
||||||
|
) {
|
||||||
|
Text(text = stringRes(R.string.leave), color = Color.White)
|
||||||
|
}
|
||||||
|
}
|
||||||
+169
@@ -0,0 +1,169 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.metadata
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
|
import androidx.compose.ui.text.style.TextDirection
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.PostButton
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChannelMetadataDialog(
|
||||||
|
onClose: () -> Unit,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
channel: PublicChatChannel? = null,
|
||||||
|
) {
|
||||||
|
val postViewModel: ChannelMetadataViewModel = viewModel()
|
||||||
|
postViewModel.load(accountViewModel.account, channel)
|
||||||
|
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = { onClose() },
|
||||||
|
properties =
|
||||||
|
DialogProperties(
|
||||||
|
dismissOnClickOutside = false,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
DialogContent(postViewModel, onClose)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DialogContent(
|
||||||
|
postViewModel: ChannelMetadataViewModel,
|
||||||
|
onClose: () -> Unit,
|
||||||
|
) {
|
||||||
|
Surface {
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(10.dp)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
CloseButton(
|
||||||
|
onPress = {
|
||||||
|
postViewModel.clear()
|
||||||
|
onClose()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
PostButton(
|
||||||
|
onPost = {
|
||||||
|
postViewModel.create()
|
||||||
|
onClose()
|
||||||
|
},
|
||||||
|
postViewModel.channelName.value.text
|
||||||
|
.isNotBlank(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(15.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
label = { Text(text = stringRes(R.string.channel_name)) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = postViewModel.channelName.value,
|
||||||
|
onValueChange = { postViewModel.channelName.value = it },
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = stringRes(R.string.my_awesome_group),
|
||||||
|
color = MaterialTheme.colorScheme.placeholderText,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
keyboardOptions =
|
||||||
|
KeyboardOptions.Default.copy(
|
||||||
|
capitalization = KeyboardCapitalization.Sentences,
|
||||||
|
),
|
||||||
|
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(15.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
label = { Text(text = stringRes(R.string.picture_url)) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = postViewModel.channelPicture.value,
|
||||||
|
onValueChange = { postViewModel.channelPicture.value = it },
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = "http://mygroup.com/logo.jpg",
|
||||||
|
color = MaterialTheme.colorScheme.placeholderText,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(15.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
label = { Text(text = stringRes(R.string.description)) },
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(100.dp),
|
||||||
|
value = postViewModel.channelDescription.value,
|
||||||
|
onValueChange = { postViewModel.channelDescription.value = it },
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = stringRes(R.string.about_us),
|
||||||
|
color = MaterialTheme.colorScheme.placeholderText,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
keyboardOptions =
|
||||||
|
KeyboardOptions.Default.copy(
|
||||||
|
capitalization = KeyboardCapitalization.Sentences,
|
||||||
|
),
|
||||||
|
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||||
|
maxLines = 10,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-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.actions
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.metadata
|
||||||
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
@@ -33,7 +33,7 @@ import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class NewChannelViewModel : ViewModel() {
|
class ChannelMetadataViewModel : ViewModel() {
|
||||||
private var account: Account? = null
|
private var account: Account? = null
|
||||||
private var originalChannel: PublicChatChannel? = null
|
private var originalChannel: PublicChatChannel? = null
|
||||||
|
|
||||||
+77
@@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LiveActivitiesChannelHeader(
|
||||||
|
baseChannel: LiveActivitiesChannel,
|
||||||
|
showVideo: Boolean,
|
||||||
|
showFlag: Boolean = true,
|
||||||
|
sendToChannel: Boolean = false,
|
||||||
|
modifier: Modifier = StdPadding,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
Column(Modifier.fillMaxWidth()) {
|
||||||
|
if (showVideo) {
|
||||||
|
ShowVideoStreaming(baseChannel, accountViewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
val expanded = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
modifier =
|
||||||
|
modifier.clickable {
|
||||||
|
if (sendToChannel) {
|
||||||
|
nav.nav(routeFor(baseChannel))
|
||||||
|
} else {
|
||||||
|
expanded.value = !expanded.value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
ShortLiveActivityChannelHeader(
|
||||||
|
baseChannel = baseChannel,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
showFlag = showFlag,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (expanded.value) {
|
||||||
|
LongLiveActivityChannelHeader(baseChannel, accountViewModel = accountViewModel, nav = nav)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+77
@@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.LikeReaction
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.ZapReaction
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||||
|
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.StatusTag
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LiveChannelActionOptions(
|
||||||
|
channel: LiveActivitiesChannel,
|
||||||
|
showFlag: Boolean = true,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
val isLive by remember(channel) { derivedStateOf { channel.info?.status() == StatusTag.STATUS.LIVE.code } }
|
||||||
|
|
||||||
|
val note = remember(channel.idHex) { LocalCache.getNoteIfExists(channel.idHex) }
|
||||||
|
|
||||||
|
note?.let {
|
||||||
|
if (showFlag && isLive) {
|
||||||
|
LiveFlag()
|
||||||
|
Spacer(modifier = StdHorzSpacer)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = RowColSpacing,
|
||||||
|
) {
|
||||||
|
LikeReaction(
|
||||||
|
baseNote = it,
|
||||||
|
grayTint = MaterialTheme.colorScheme.onSurface,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = StdHorzSpacer)
|
||||||
|
ZapReaction(
|
||||||
|
baseNote = it,
|
||||||
|
grayTint = MaterialTheme.colorScheme.onSurface,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+195
@@ -0,0 +1,195 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.elements.NormalTimeAgo
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags
|
||||||
|
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
|
||||||
|
import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists
|
||||||
|
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LongLiveActivityChannelHeader(
|
||||||
|
baseChannel: LiveActivitiesChannel,
|
||||||
|
lineModifier: Modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp),
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
val channelState by baseChannel.live.observeAsState()
|
||||||
|
val channel = channelState?.channel as? LiveActivitiesChannel ?: return
|
||||||
|
|
||||||
|
Row(
|
||||||
|
lineModifier,
|
||||||
|
) {
|
||||||
|
val summary = remember(channelState) { channel.summary()?.ifBlank { null } }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
Modifier.weight(1f),
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
val defaultBackground = MaterialTheme.colorScheme.background
|
||||||
|
val background = remember { mutableStateOf(defaultBackground) }
|
||||||
|
|
||||||
|
val tags = remember(channelState) { baseChannel.info?.tags?.toImmutableListOfLists() ?: EmptyTagList }
|
||||||
|
|
||||||
|
TranslatableRichTextViewer(
|
||||||
|
content = summary ?: stringRes(id = R.string.groups_no_descriptor),
|
||||||
|
canPreview = false,
|
||||||
|
quotesLeft = 1,
|
||||||
|
tags = tags,
|
||||||
|
backgroundColor = background,
|
||||||
|
id = baseChannel.idHex,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (summary != null) {
|
||||||
|
baseChannel.info?.let {
|
||||||
|
if (it.hasHashtags()) {
|
||||||
|
DisplayUncitedHashtags(it, summary, nav)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadNote(baseNoteHex = channel.idHex, accountViewModel) { loadingNote ->
|
||||||
|
loadingNote?.let { note ->
|
||||||
|
Row(
|
||||||
|
lineModifier,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringRes(id = R.string.owner),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.width(75.dp),
|
||||||
|
)
|
||||||
|
Spacer(DoubleHorzSpacer)
|
||||||
|
NoteAuthorPicture(note, nav, accountViewModel, Size25dp)
|
||||||
|
Spacer(DoubleHorzSpacer)
|
||||||
|
NoteUsernameDisplay(note, Modifier.weight(1f), accountViewModel = accountViewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
lineModifier,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringRes(id = R.string.created_at),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.width(75.dp),
|
||||||
|
)
|
||||||
|
Spacer(DoubleHorzSpacer)
|
||||||
|
NormalTimeAgo(note, remember { Modifier.weight(1f) })
|
||||||
|
MoreOptionsButton(note, null, accountViewModel, nav)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var participantUsers by remember(baseChannel) {
|
||||||
|
mutableStateOf<ImmutableList<Pair<ParticipantTag, User>>>(
|
||||||
|
persistentListOf(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = channelState) {
|
||||||
|
launch(Dispatchers.IO) {
|
||||||
|
val newParticipantUsers =
|
||||||
|
channel.info
|
||||||
|
?.participants()
|
||||||
|
?.mapNotNull { part ->
|
||||||
|
LocalCache.checkGetOrCreateUser(part.pubKey)?.let { Pair(part, it) }
|
||||||
|
}?.toImmutableList()
|
||||||
|
|
||||||
|
if (
|
||||||
|
newParticipantUsers != null && !equalImmutableLists(newParticipantUsers, participantUsers)
|
||||||
|
) {
|
||||||
|
participantUsers = newParticipantUsers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
participantUsers.forEach {
|
||||||
|
Row(
|
||||||
|
lineModifier.clickable { nav.nav("User/${it.second.pubkeyHex}") },
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
it.first.role?.let { it1 ->
|
||||||
|
Text(
|
||||||
|
text = it1.capitalize(Locale.ROOT),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.width(55.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(DoubleHorzSpacer)
|
||||||
|
ClickableUserPicture(it.second, Size25dp, accountViewModel)
|
||||||
|
Spacer(DoubleHorzSpacer)
|
||||||
|
UsernameDisplay(it.second, Modifier.weight(1f), accountViewModel = accountViewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+91
@@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size34dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ShortLiveActivityChannelHeader(
|
||||||
|
baseChannel: LiveActivitiesChannel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
showFlag: Boolean,
|
||||||
|
) {
|
||||||
|
val channelState by baseChannel.live.observeAsState()
|
||||||
|
val channel = channelState?.channel as? LiveActivitiesChannel ?: return
|
||||||
|
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
channel.creator?.let {
|
||||||
|
UserPicture(
|
||||||
|
user = it,
|
||||||
|
size = Size34dp,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(start = 10.dp)
|
||||||
|
.height(35.dp)
|
||||||
|
.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = remember(channelState) { channel.toBestDisplayName() },
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.height(Size35dp)
|
||||||
|
.padding(start = 5.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
LiveChannelActionOptions(channel, showFlag, accountViewModel, nav)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+89
@@ -0,0 +1,89 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.lifecycle.distinctUntilChanged
|
||||||
|
import androidx.lifecycle.map
|
||||||
|
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
|
||||||
|
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CrossfadeCheckIfVideoIsOnline
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.StreamingHeaderModifier
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ShowVideoStreaming(
|
||||||
|
baseChannel: LiveActivitiesChannel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
|
baseChannel.info?.let {
|
||||||
|
SensitivityWarning(
|
||||||
|
event = it,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
) {
|
||||||
|
val streamingInfoEvent by
|
||||||
|
baseChannel.live
|
||||||
|
.map {
|
||||||
|
(it.channel as? LiveActivitiesChannel)?.info
|
||||||
|
}.distinctUntilChanged()
|
||||||
|
.observeAsState(baseChannel.info)
|
||||||
|
|
||||||
|
streamingInfoEvent?.let { event ->
|
||||||
|
event.streaming()?.let { url ->
|
||||||
|
CrossfadeCheckIfVideoIsOnline(url, accountViewModel) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
modifier = StreamingHeaderModifier,
|
||||||
|
) {
|
||||||
|
val zoomableUrlVideo =
|
||||||
|
remember(streamingInfoEvent) {
|
||||||
|
MediaUrlVideo(
|
||||||
|
url = url,
|
||||||
|
description = baseChannel.toBestDisplayName(),
|
||||||
|
artworkUri = event.image(),
|
||||||
|
authorName = baseChannel.creatorName(),
|
||||||
|
uri = baseChannel.toNAddr(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ZoomableContentView(
|
||||||
|
content = zoomableUrlVideo,
|
||||||
|
roundedCorner = false,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+108
@@ -0,0 +1,108 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.SmallBorder
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.liveStreamTag
|
||||||
|
import java.text.DateFormat
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LiveFlag() {
|
||||||
|
Text(
|
||||||
|
text = stringRes(id = R.string.live_stream_live_tag),
|
||||||
|
color = Color.White,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
modifier =
|
||||||
|
remember {
|
||||||
|
Modifier
|
||||||
|
.clip(SmallBorder)
|
||||||
|
.background(Color.Red)
|
||||||
|
.padding(horizontal = 5.dp)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EndedFlag() {
|
||||||
|
Text(
|
||||||
|
text = stringRes(id = R.string.live_stream_ended_tag),
|
||||||
|
color = Color.White,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier =
|
||||||
|
remember {
|
||||||
|
Modifier
|
||||||
|
.clip(SmallBorder)
|
||||||
|
.background(Color.Black)
|
||||||
|
.padding(horizontal = 5.dp)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun OfflineFlag() {
|
||||||
|
Text(
|
||||||
|
text = stringRes(id = R.string.live_stream_offline_tag),
|
||||||
|
color = Color.White,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier =
|
||||||
|
remember {
|
||||||
|
Modifier
|
||||||
|
.clip(SmallBorder)
|
||||||
|
.background(Color.Black)
|
||||||
|
.padding(horizontal = 5.dp)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ScheduledFlag(starts: Long?) {
|
||||||
|
val startsIn =
|
||||||
|
starts?.let {
|
||||||
|
SimpleDateFormat
|
||||||
|
.getDateTimeInstance(
|
||||||
|
DateFormat.SHORT,
|
||||||
|
DateFormat.SHORT,
|
||||||
|
).format(Date(starts * 1000))
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = startsIn ?: stringRes(id = R.string.live_stream_planned_tag),
|
||||||
|
color = Color.White,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = liveStreamTag,
|
||||||
|
)
|
||||||
|
}
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.header
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.TopBarExtensibleWithBackButton
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LongLiveActivityChannelHeader
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.ShortLiveActivityChannelHeader
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LiveActivityTopBar(
|
||||||
|
baseChannel: LiveActivitiesChannel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
TopBarExtensibleWithBackButton(
|
||||||
|
title = {
|
||||||
|
ShortLiveActivityChannelHeader(
|
||||||
|
baseChannel = baseChannel,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
showFlag = true,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
extendableRow = {
|
||||||
|
LongLiveActivityChannelHeader(baseChannel = baseChannel, accountViewModel = accountViewModel, nav = nav)
|
||||||
|
},
|
||||||
|
popBack = nav::popBack,
|
||||||
|
)
|
||||||
|
}
|
||||||
+653
@@ -0,0 +1,653 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
|
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.Channel
|
||||||
|
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||||
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
import com.vitorpamplona.amethyst.service.LocationState
|
||||||
|
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
||||||
|
import com.vitorpamplona.amethyst.service.uploads.MediaCompressor
|
||||||
|
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.DEFAULT_MEDIA_SERVERS
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.Split
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.UserSuggestions
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.ChatFileUploadState
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent
|
||||||
|
import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.tags.geohash.getGeoHash
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.tags.references.references
|
||||||
|
import com.vitorpamplona.quartz.nip10Notes.content.findHashtags
|
||||||
|
import com.vitorpamplona.quartz.nip10Notes.content.findNostrUris
|
||||||
|
import com.vitorpamplona.quartz.nip10Notes.content.findURLs
|
||||||
|
import com.vitorpamplona.quartz.nip18Reposts.quotes.quotes
|
||||||
|
import com.vitorpamplona.quartz.nip28PublicChat.base.notify
|
||||||
|
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
||||||
|
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.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
||||||
|
import com.vitorpamplona.quartz.nip53LiveActivities.chat.notify
|
||||||
|
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.nip92IMeta.imetas
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
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 ChannelNewMessageViewModel : ViewModel() {
|
||||||
|
var draftTag: String by mutableStateOf(UUID.randomUUID().toString())
|
||||||
|
|
||||||
|
var accountViewModel: AccountViewModel? = null
|
||||||
|
var account: Account? = null
|
||||||
|
var channel: Channel? = null
|
||||||
|
|
||||||
|
val replyTo = mutableStateOf<Note?>(null)
|
||||||
|
|
||||||
|
var uploadState by mutableStateOf<ChatFileUploadState?>(null)
|
||||||
|
val iMetaAttachments = IMetaAttachments()
|
||||||
|
var nip95attachments by mutableStateOf<List<Pair<FileStorageEvent, FileStorageHeaderEvent>>>(emptyList())
|
||||||
|
|
||||||
|
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(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
// GeoHash
|
||||||
|
var wantsToAddGeoHash by mutableStateOf(false)
|
||||||
|
var location: StateFlow<LocationState.LocationResult>? = null
|
||||||
|
var wantsExclusiveGeoPost by mutableStateOf(false)
|
||||||
|
|
||||||
|
// ZapRaiser
|
||||||
|
var canAddZapRaiser by mutableStateOf(false)
|
||||||
|
var wantsZapraiser by mutableStateOf(false)
|
||||||
|
var zapRaiserAmount by mutableStateOf<Long?>(null)
|
||||||
|
|
||||||
|
val draftTextChanges = kotlinx.coroutines.channels.Channel<String>(kotlinx.coroutines.channels.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 = hasLnAddress()
|
||||||
|
this.canAddZapRaiser = hasLnAddress()
|
||||||
|
|
||||||
|
this.uploadState =
|
||||||
|
ChatFileUploadState(
|
||||||
|
account?.settings?.defaultFileServer ?: DEFAULT_MEDIA_SERVERS[0],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun load(channel: Channel) {
|
||||||
|
this.channel = channel
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun reply(replyNote: Note) {
|
||||||
|
replyTo.value = replyNote
|
||||||
|
saveDraft()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearReply() {
|
||||||
|
replyTo.value = null
|
||||||
|
saveDraft()
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
val draftEvent = draft.event ?: 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 geohash = draftEvent.getGeoHash()
|
||||||
|
wantsToAddGeoHash = geohash != null
|
||||||
|
|
||||||
|
val zapraiser = draftEvent.zapraiserAmount()
|
||||||
|
wantsZapraiser = zapraiser != null
|
||||||
|
zapRaiserAmount = null
|
||||||
|
if (zapraiser != null) {
|
||||||
|
zapRaiserAmount = zapraiser
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forwardZapTo.items.isNotEmpty()) {
|
||||||
|
wantsForwardZapTo = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (draftEvent as? ChannelMessageEvent != null) {
|
||||||
|
val replyId = draftEvent.reply()?.eventId
|
||||||
|
if (replyId != null) {
|
||||||
|
accountViewModel?.checkGetOrCreateNote(replyId) {
|
||||||
|
replyTo.value = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (draftEvent as? LiveActivitiesChatMessageEvent != null) {
|
||||||
|
val replyId = draftEvent.reply()?.eventId
|
||||||
|
if (replyId != null) {
|
||||||
|
accountViewModel?.checkGetOrCreateNote(replyId) {
|
||||||
|
replyTo.value = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message = TextFieldValue(draftEvent.content)
|
||||||
|
|
||||||
|
urlPreview = findUrlInMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendPost(onDone: () -> Unit) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
sendPostSync()
|
||||||
|
onDone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun sendPostSync() {
|
||||||
|
val template = createTemplate() ?: return
|
||||||
|
val channelRelays = channel?.relays() ?: emptyList()
|
||||||
|
|
||||||
|
accountViewModel?.account?.signAndSendPrivately(template, channelRelays)
|
||||||
|
|
||||||
|
accountViewModel?.deleteDraft(draftTag)
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendDraft() {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
sendDraftSync()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun sendDraftSync() {
|
||||||
|
val accountViewModel = accountViewModel ?: return
|
||||||
|
|
||||||
|
if (message.text.isBlank()) {
|
||||||
|
account?.deleteDraft(draftTag)
|
||||||
|
} else {
|
||||||
|
val template = createTemplate() ?: return
|
||||||
|
accountViewModel.account.createAndSendDraft(draftTag, template)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pickedMedia(list: ImmutableList<SelectedMedia>) {
|
||||||
|
uploadState?.load(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun upload(
|
||||||
|
onError: (title: String, message: String) -> Unit,
|
||||||
|
context: Context,
|
||||||
|
) {
|
||||||
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
|
val myAccount = account ?: return@launch
|
||||||
|
val uploadState = uploadState ?: return@launch
|
||||||
|
|
||||||
|
val myMultiOrchestrator = uploadState.multiOrchestrator ?: return@launch
|
||||||
|
|
||||||
|
isUploadingImage = true
|
||||||
|
|
||||||
|
val results =
|
||||||
|
myMultiOrchestrator.upload(
|
||||||
|
viewModelScope,
|
||||||
|
uploadState.caption,
|
||||||
|
uploadState.contentWarningReason,
|
||||||
|
MediaCompressor.intToCompressorQuality(uploadState.mediaQualitySlider),
|
||||||
|
uploadState.selectedServer,
|
||||||
|
myAccount,
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (results.allGood) {
|
||||||
|
results.successful.forEach {
|
||||||
|
if (it.result is UploadOrchestrator.OrchestratorResult.NIP95Result) {
|
||||||
|
account?.createNip95(it.result.bytes, headerInfo = it.result.fileHeader, uploadState.caption, uploadState.contentWarningReason) { nip95 ->
|
||||||
|
nip95attachments = nip95attachments + nip95
|
||||||
|
val note = nip95.let { it1 -> account?.consumeNip95(it1.first, it1.second) }
|
||||||
|
|
||||||
|
note?.let {
|
||||||
|
message = message.insertUrlAtCursor(it.toNostrUri())
|
||||||
|
}
|
||||||
|
|
||||||
|
urlPreview = findUrlInMessage()
|
||||||
|
}
|
||||||
|
} else if (it.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
|
||||||
|
iMetaAttachments.add(it.result, uploadState.caption, uploadState.contentWarningReason)
|
||||||
|
|
||||||
|
message = message.insertUrlAtCursor(it.result.url)
|
||||||
|
urlPreview = findUrlInMessage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadState.reset()
|
||||||
|
} else {
|
||||||
|
val errorMessages = results.errors.map { stringRes(context, it.errorResource, *it.params) }.distinct()
|
||||||
|
|
||||||
|
onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
isUploadingImage = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun createTemplate(): EventTemplate<out Event>? {
|
||||||
|
val channel = channel ?: return null
|
||||||
|
val accountViewModel = accountViewModel ?: return null
|
||||||
|
|
||||||
|
val tagger =
|
||||||
|
NewMessageTagger(
|
||||||
|
message = message.text,
|
||||||
|
pTags = listOfNotNull(replyTo.value?.author),
|
||||||
|
eTags = listOfNotNull(replyTo.value),
|
||||||
|
channelHex = channel.idHex,
|
||||||
|
dao = accountViewModel,
|
||||||
|
)
|
||||||
|
tagger.run()
|
||||||
|
|
||||||
|
val urls = findURLs(message.text)
|
||||||
|
val usedAttachments = iMetaAttachments.filterIsIn(urls.toSet())
|
||||||
|
val emojis = findEmoji(message.text, accountViewModel.account.myEmojis.value)
|
||||||
|
|
||||||
|
val channelRelays = channel.relays()
|
||||||
|
val geoHash = (location?.value as? LocationState.LocationResult.Success)?.geoHash?.toString()
|
||||||
|
|
||||||
|
return if (channel is PublicChatChannel) {
|
||||||
|
val replyingToEvent = replyTo.value?.toEventHint<ChannelMessageEvent>()
|
||||||
|
val channelEvent = channel.event
|
||||||
|
|
||||||
|
if (replyingToEvent != null) {
|
||||||
|
ChannelMessageEvent.reply(tagger.message, replyingToEvent) {
|
||||||
|
notify(replyingToEvent.toPTag())
|
||||||
|
|
||||||
|
hashtags(findHashtags(tagger.message))
|
||||||
|
references(findURLs(tagger.message))
|
||||||
|
quotes(findNostrUris(tagger.message))
|
||||||
|
|
||||||
|
geoHash?.let { geohash(it) }
|
||||||
|
|
||||||
|
emojis(emojis)
|
||||||
|
imetas(usedAttachments)
|
||||||
|
}
|
||||||
|
} else if (channelEvent != null) {
|
||||||
|
val hint = EventHintBundle(channelEvent, channelRelays.firstOrNull())
|
||||||
|
ChannelMessageEvent.message(tagger.message, hint) {
|
||||||
|
hashtags(findHashtags(tagger.message))
|
||||||
|
references(findURLs(tagger.message))
|
||||||
|
quotes(findNostrUris(tagger.message))
|
||||||
|
|
||||||
|
geoHash?.let { geohash(it) }
|
||||||
|
|
||||||
|
emojis(emojis)
|
||||||
|
imetas(usedAttachments)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ChannelMessageEvent.message(tagger.message, ETag(channel.idHex, channelRelays.firstOrNull())) {
|
||||||
|
hashtags(findHashtags(tagger.message))
|
||||||
|
references(findURLs(tagger.message))
|
||||||
|
quotes(findNostrUris(tagger.message))
|
||||||
|
|
||||||
|
geoHash?.let { geohash(it) }
|
||||||
|
|
||||||
|
emojis(emojis)
|
||||||
|
imetas(usedAttachments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (channel is LiveActivitiesChannel) {
|
||||||
|
val replyingToEvent = replyTo.value?.toEventHint<LiveActivitiesChatMessageEvent>()
|
||||||
|
val activity = channel.info
|
||||||
|
|
||||||
|
if (replyingToEvent != null) {
|
||||||
|
LiveActivitiesChatMessageEvent.reply(tagger.message, replyingToEvent) {
|
||||||
|
notify(replyingToEvent.toPTag())
|
||||||
|
|
||||||
|
hashtags(findHashtags(tagger.message))
|
||||||
|
references(findURLs(tagger.message))
|
||||||
|
quotes(findNostrUris(tagger.message))
|
||||||
|
|
||||||
|
emojis(emojis)
|
||||||
|
imetas(usedAttachments)
|
||||||
|
}
|
||||||
|
} else if (activity != null) {
|
||||||
|
val hint = EventHintBundle(activity, channelRelays.firstOrNull() ?: replyingToEvent?.relay)
|
||||||
|
|
||||||
|
LiveActivitiesChatMessageEvent.message(tagger.message, hint) {
|
||||||
|
hashtags(findHashtags(tagger.message))
|
||||||
|
references(findURLs(tagger.message))
|
||||||
|
quotes(findNostrUris(tagger.message))
|
||||||
|
|
||||||
|
emojis(emojis)
|
||||||
|
imetas(usedAttachments)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LiveActivitiesChatMessageEvent.message(tagger.message, channel.toATag()) {
|
||||||
|
hashtags(findHashtags(tagger.message))
|
||||||
|
references(findURLs(tagger.message))
|
||||||
|
quotes(findNostrUris(tagger.message))
|
||||||
|
|
||||||
|
emojis(emojis)
|
||||||
|
imetas(usedAttachments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun cancel() {
|
||||||
|
message = TextFieldValue("")
|
||||||
|
|
||||||
|
replyTo.value = null
|
||||||
|
|
||||||
|
urlPreview = null
|
||||||
|
|
||||||
|
wantsInvoice = false
|
||||||
|
wantsZapraiser = false
|
||||||
|
zapRaiserAmount = null
|
||||||
|
|
||||||
|
wantsForwardZapTo = false
|
||||||
|
wantsToMarkAsSensitive = false
|
||||||
|
|
||||||
|
wantsToAddGeoHash = 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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("")
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun canPost(): Boolean =
|
||||||
|
message.text.isNotBlank() &&
|
||||||
|
uploadState?.isUploadingImage != true &&
|
||||||
|
!wantsInvoice &&
|
||||||
|
(!wantsZapraiser || zapRaiserAmount != null) &&
|
||||||
|
uploadState?.multiOrchestrator == null
|
||||||
|
|
||||||
|
fun insertAtCursor(newElement: String) {
|
||||||
|
message = message.insertUrlAtCursor(newElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun locationFlow(): StateFlow<LocationState.LocationResult> {
|
||||||
|
if (location == null) {
|
||||||
|
location = Amethyst.instance.locationManager.geohashStateFlow
|
||||||
|
}
|
||||||
|
|
||||||
|
return location!!
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
Log.d("Init", "OnCleared: ${this.javaClass.simpleName}")
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
+141
@@ -0,0 +1,141 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
|
import androidx.compose.ui.text.style.TextDirection
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.ShowEmojiSuggestionList
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.ShowUserSuggestionList
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ThinSendButton
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.EditFieldBorder
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.EditFieldLeadingIconModifier
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.EditFieldModifier
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.EditFieldTrailingIconModifier
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(FlowPreview::class)
|
||||||
|
@Composable
|
||||||
|
fun EditFieldRow(
|
||||||
|
channelScreenModel: ChannelNewMessageViewModel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
onSendNewMessage: () -> Unit,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
channelScreenModel.replyTo.value?.let {
|
||||||
|
DisplayReplyingToNote(it, accountViewModel, nav) {
|
||||||
|
channelScreenModel.clearReply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
launch(Dispatchers.IO) {
|
||||||
|
channelScreenModel.draftTextChanges
|
||||||
|
.receiveAsFlow()
|
||||||
|
.debounce(1000)
|
||||||
|
.collectLatest {
|
||||||
|
channelScreenModel.sendDraft()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = EditFieldModifier,
|
||||||
|
) {
|
||||||
|
ShowUserSuggestionList(
|
||||||
|
channelScreenModel.userSuggestions.userSuggestions,
|
||||||
|
channelScreenModel::autocompleteWithUser,
|
||||||
|
accountViewModel,
|
||||||
|
)
|
||||||
|
|
||||||
|
ShowEmojiSuggestionList(
|
||||||
|
channelScreenModel.emojiSuggestions,
|
||||||
|
channelScreenModel::autocompleteWithEmoji,
|
||||||
|
channelScreenModel::autocompleteWithEmojiUrl,
|
||||||
|
accountViewModel,
|
||||||
|
)
|
||||||
|
|
||||||
|
ThinPaddingTextField(
|
||||||
|
value = channelScreenModel.message,
|
||||||
|
onValueChange = { channelScreenModel.updateMessage(it) },
|
||||||
|
keyboardOptions =
|
||||||
|
KeyboardOptions.Default.copy(
|
||||||
|
capitalization = KeyboardCapitalization.Sentences,
|
||||||
|
),
|
||||||
|
shape = EditFieldBorder,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = stringRes(R.string.reply_here),
|
||||||
|
color = MaterialTheme.colorScheme.placeholderText,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||||
|
trailingIcon = {
|
||||||
|
ThinSendButton(
|
||||||
|
isActive =
|
||||||
|
channelScreenModel.message.text.isNotBlank() && !channelScreenModel.isUploadingImage,
|
||||||
|
modifier = EditFieldTrailingIconModifier,
|
||||||
|
) {
|
||||||
|
channelScreenModel.sendPost(onSendNewMessage)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
SelectFromGallery(
|
||||||
|
isUploading = channelScreenModel.isUploadingImage,
|
||||||
|
tint = MaterialTheme.colorScheme.placeholderText,
|
||||||
|
modifier = EditFieldLeadingIconModifier,
|
||||||
|
onImageChosen = channelScreenModel::pickedMedia,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors =
|
||||||
|
TextFieldDefaults.colors(
|
||||||
|
focusedIndicatorColor = Color.Transparent,
|
||||||
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
visualTransformation = UrlUserTagTransformation(MaterialTheme.colorScheme.primary),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -38,7 +38,7 @@ fun ThinSendButton(
|
|||||||
) {
|
) {
|
||||||
IconButton(
|
IconButton(
|
||||||
enabled = isActive,
|
enabled = isActive,
|
||||||
modifier = modifier,
|
// modifier = modifier,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
|
|||||||
+1
-1
@@ -147,7 +147,7 @@ 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.chats.public.ChannelHeader
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ChannelHeader
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ThinSendButton
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ThinSendButton
|
||||||
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
|
||||||
|
|||||||
@@ -114,7 +114,9 @@ val HalfStartPadding = Modifier.padding(start = 5.dp)
|
|||||||
val StdStartPadding = Modifier.padding(start = 10.dp)
|
val StdStartPadding = Modifier.padding(start = 10.dp)
|
||||||
val StdTopPadding = Modifier.padding(top = 10.dp)
|
val StdTopPadding = Modifier.padding(top = 10.dp)
|
||||||
val HalfTopPadding = Modifier.padding(top = 5.dp)
|
val HalfTopPadding = Modifier.padding(top = 5.dp)
|
||||||
|
|
||||||
val HalfHalfVertPadding = Modifier.padding(vertical = 3.dp)
|
val HalfHalfVertPadding = Modifier.padding(vertical = 3.dp)
|
||||||
|
val HalfHalfHorzModifier = Modifier.padding(horizontal = 3.dp)
|
||||||
|
|
||||||
val HalfPadding = Modifier.padding(5.dp)
|
val HalfPadding = Modifier.padding(5.dp)
|
||||||
val StdPadding = Modifier.padding(10.dp)
|
val StdPadding = Modifier.padding(10.dp)
|
||||||
@@ -207,7 +209,7 @@ val CashuCardBorders = Modifier.fillMaxWidth().padding(10.dp).clip(shape = Quote
|
|||||||
|
|
||||||
val EditFieldModifier =
|
val EditFieldModifier =
|
||||||
Modifier.padding(start = 10.dp, end = 10.dp, bottom = 10.dp, top = 5.dp).fillMaxWidth()
|
Modifier.padding(start = 10.dp, end = 10.dp, bottom = 10.dp, top = 5.dp).fillMaxWidth()
|
||||||
val EditFieldTrailingIconModifier = Modifier.height(26.dp).padding(start = 5.dp, end = 0.dp)
|
val EditFieldTrailingIconModifier = Modifier.padding(start = 5.dp, end = 0.dp)
|
||||||
val EditFieldLeadingIconModifier = Modifier.height(32.dp).padding(start = 2.dp)
|
val EditFieldLeadingIconModifier = Modifier.height(32.dp).padding(start = 2.dp)
|
||||||
|
|
||||||
val ZeroPadding = PaddingValues(0.dp)
|
val ZeroPadding = PaddingValues(0.dp)
|
||||||
@@ -299,3 +301,8 @@ val ripple24dp = ripple(bounded = false, radius = Size24dp)
|
|||||||
val defaultTweenDuration = 100
|
val defaultTweenDuration = 100
|
||||||
val defaultTweenFloatSpec = tween<Float>(durationMillis = defaultTweenDuration)
|
val defaultTweenFloatSpec = tween<Float>(durationMillis = defaultTweenDuration)
|
||||||
val defaultTweenIntOffsetSpec = tween<IntOffset>(durationMillis = defaultTweenDuration)
|
val defaultTweenIntOffsetSpec = tween<IntOffset>(durationMillis = defaultTweenDuration)
|
||||||
|
|
||||||
|
val StreamingHeaderModifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(min = 50.dp, max = 300.dp)
|
||||||
|
|||||||
@@ -109,4 +109,21 @@ abstract class NostrSigner(
|
|||||||
) as T,
|
) as T,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <T : Event> assembleRumor(ev: EventTemplate<T>) = assembleRumor<T>(ev.createdAt, ev.kind, ev.tags, ev.content)
|
||||||
|
|
||||||
|
fun <T : Event> assembleRumor(
|
||||||
|
createdAt: Long,
|
||||||
|
kind: Int,
|
||||||
|
tags: Array<Array<String>>,
|
||||||
|
content: String,
|
||||||
|
) = EventFactory.create(
|
||||||
|
id = EventHasher.hashId(pubKey, createdAt, kind, tags, content),
|
||||||
|
pubKey = pubKey,
|
||||||
|
createdAt = createdAt,
|
||||||
|
kind = kind,
|
||||||
|
tags = tags,
|
||||||
|
content = content,
|
||||||
|
sig = "",
|
||||||
|
) as T
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-1
@@ -23,14 +23,17 @@ package com.vitorpamplona.quartz.nip28PublicChat.message
|
|||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.tagArray
|
||||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
|
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
|
||||||
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
|
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
|
||||||
|
import com.vitorpamplona.quartz.nip10Notes.tags.markedETag
|
||||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
||||||
import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel
|
import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel
|
||||||
import com.vitorpamplona.quartz.nip28PublicChat.base.channel
|
import com.vitorpamplona.quartz.nip28PublicChat.base.channel
|
||||||
import com.vitorpamplona.quartz.nip28PublicChat.base.reply
|
import com.vitorpamplona.quartz.nip28PublicChat.base.reply
|
||||||
|
import com.vitorpamplona.quartz.nip37Drafts.ExposeInDraft
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
@@ -42,7 +45,8 @@ class ChannelMessageEvent(
|
|||||||
content: String,
|
content: String,
|
||||||
sig: HexKey,
|
sig: HexKey,
|
||||||
) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||||
IsInPublicChatChannel {
|
IsInPublicChatChannel,
|
||||||
|
ExposeInDraft {
|
||||||
override fun channel() = markedRoot() ?: unmarkedRoot()
|
override fun channel() = markedRoot() ?: unmarkedRoot()
|
||||||
|
|
||||||
override fun channelId() = channel()?.eventId
|
override fun channelId() = channel()?.eventId
|
||||||
@@ -51,6 +55,12 @@ class ChannelMessageEvent(
|
|||||||
|
|
||||||
override fun unmarkedReplyTos() = super.unmarkedReplyTos().filter { it != channelId() }
|
override fun unmarkedReplyTos() = super.unmarkedReplyTos().filter { it != channelId() }
|
||||||
|
|
||||||
|
override fun exposeInDraft() =
|
||||||
|
tagArray<ChannelMessageEvent> {
|
||||||
|
channel()?.let { markedETag(it) }
|
||||||
|
reply()?.let { markedETag(it) }
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val KIND = 42
|
const val KIND = 42
|
||||||
const val ALT = "Public chat message"
|
const val ALT = "Public chat message"
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* 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.quartz.nip37Drafts
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.tags.kinds.kind
|
||||||
|
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||||
|
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent.Companion.ALT_DESCRIPTION
|
||||||
|
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent.Companion.KIND
|
||||||
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
|
||||||
|
class DraftBuilder {
|
||||||
|
companion object {
|
||||||
|
fun <T : Event> encryptAndSign(
|
||||||
|
dTag: String,
|
||||||
|
draft: T,
|
||||||
|
signer: NostrSigner,
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
onReady: (DraftEvent) -> Unit,
|
||||||
|
) {
|
||||||
|
signer.nip44Encrypt(draft.toJson(), signer.pubKey) { encryptedContent ->
|
||||||
|
val template =
|
||||||
|
eventTemplate<DraftEvent>(KIND, encryptedContent, createdAt) {
|
||||||
|
alt(ALT_DESCRIPTION)
|
||||||
|
dTag(dTag)
|
||||||
|
kind(draft.kind)
|
||||||
|
|
||||||
|
if (draft is ExposeInDraft) {
|
||||||
|
addAll(draft.exposeInDraft())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signer.sign(template) {
|
||||||
|
it.addToCache(signer.pubKey, draft)
|
||||||
|
onReady(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,8 @@ import com.vitorpamplona.quartz.nip01Core.core.Event
|
|||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.tags.kinds.kind
|
||||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||||
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
||||||
@@ -121,6 +123,7 @@ class DraftEvent(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val KIND = 31234
|
const val KIND = 31234
|
||||||
|
const val ALT_DESCRIPTION = "Draft Event"
|
||||||
|
|
||||||
fun createAddressTag(
|
fun createAddressTag(
|
||||||
pubKey: HexKey,
|
pubKey: HexKey,
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* 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.quartz.nip37Drafts
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||||
|
|
||||||
|
interface ExposeInDraft {
|
||||||
|
fun exposeInDraft(): TagArray
|
||||||
|
}
|
||||||
+12
-1
@@ -23,16 +23,20 @@ package com.vitorpamplona.quartz.nip53LiveActivities.chat
|
|||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.tagArray
|
||||||
import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
|
import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
|
||||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
|
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
|
||||||
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
|
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
|
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.tags.addressables.aTag
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
|
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||||
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
|
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
|
||||||
|
import com.vitorpamplona.quartz.nip10Notes.tags.markedETag
|
||||||
import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag
|
import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag
|
||||||
|
import com.vitorpamplona.quartz.nip37Drafts.ExposeInDraft
|
||||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
|
||||||
@@ -47,7 +51,8 @@ class LiveActivitiesChatMessageEvent(
|
|||||||
) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||||
EventHintProvider,
|
EventHintProvider,
|
||||||
PubKeyHintProvider,
|
PubKeyHintProvider,
|
||||||
AddressHintProvider {
|
AddressHintProvider,
|
||||||
|
ExposeInDraft {
|
||||||
override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint)
|
override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint)
|
||||||
|
|
||||||
override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + tags.mapNotNull(QTag::parseEventAsHint)
|
override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + tags.mapNotNull(QTag::parseEventAsHint)
|
||||||
@@ -64,6 +69,12 @@ class LiveActivitiesChatMessageEvent(
|
|||||||
|
|
||||||
override fun unmarkedReplyTos() = super.markedReplyTos().minus(activityHex() ?: "")
|
override fun unmarkedReplyTos() = super.markedReplyTos().minus(activityHex() ?: "")
|
||||||
|
|
||||||
|
override fun exposeInDraft() =
|
||||||
|
tagArray<LiveActivitiesChatMessageEvent> {
|
||||||
|
activity()?.let { aTag(it) }
|
||||||
|
reply()?.let { markedETag(it) }
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val KIND = 1311
|
const val KIND = 1311
|
||||||
const val ALT = "Live activity chat message"
|
const val ALT = "Live activity chat message"
|
||||||
|
|||||||
Reference in New Issue
Block a user