mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-03-17 13:21:50 +01:00
Starts the migration of public chats to the new structure
Starts the migration of drafts to the new structure
This commit is contained in:
parent
58b6be73ee
commit
fe55ee1818
@ -135,6 +135,7 @@ import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEve
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.taggedEmojis
|
||||
import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent
|
||||
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftBuilder
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
|
||||
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
|
||||
@ -2101,6 +2102,27 @@ class Account(
|
||||
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(
|
||||
signedEvent: Event,
|
||||
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(
|
||||
draftTag: String?,
|
||||
template: EventTemplate<T>,
|
||||
@ -2357,6 +2402,16 @@ class Account(
|
||||
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) {
|
||||
val key = DraftEvent.createAddressTag(userProfile().pubkeyHex, draftTag)
|
||||
LocalCache.getAddressableNoteIfExists(key)?.let { note ->
|
||||
@ -2680,6 +2735,18 @@ class Account(
|
||||
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) {
|
||||
val mine = signedEvents.wraps.filter { (it.recipientPubKey() == signer.pubKey) }
|
||||
|
||||
|
@ -2399,9 +2399,12 @@ object LocalCache {
|
||||
}
|
||||
is ChannelMessageEvent -> {
|
||||
draft.channelId()?.let { channelId ->
|
||||
checkGetOrCreateChannel(channelId)?.let { channel ->
|
||||
channel.addNote(note, null)
|
||||
}
|
||||
checkGetOrCreateChannel(channelId)?.addNote(note, null)
|
||||
}
|
||||
}
|
||||
is LiveActivitiesChatMessageEvent -> {
|
||||
draft.activityAddress()?.let { channelId ->
|
||||
checkGetOrCreateChannel(channelId.toValue())?.addNote(note, null)
|
||||
}
|
||||
}
|
||||
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.privateDM.ChatroomByAuthorScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.public.ChannelScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ChannelScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.CommunityScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.DiscoverScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.drafts.DraftListScreen
|
||||
|
@ -78,11 +78,11 @@ import com.vitorpamplona.amethyst.ui.layouts.LeftPictureLayout
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.BannerImage
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.public.ChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.public.EndedFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.public.LiveFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.public.OfflineFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.public.ScheduledFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.EndedFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.OfflineFlag
|
||||
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.home.CheckIfVideoIsOnline
|
||||
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.VideoDisplay
|
||||
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.theme.DoubleVertSpacer
|
||||
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.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
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.ui.note.timeAgo
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgoShort
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
|
||||
@Composable
|
||||
@ -48,3 +54,21 @@ fun TimeAgo(time: Long) {
|
||||
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.navigation.INav
|
||||
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.replyModifier
|
||||
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.padding
|
||||
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.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@ -36,9 +38,11 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
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.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.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.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.HeaderPictureModifier
|
||||
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 kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
@ -359,3 +365,47 @@ fun WatchAddressableNoteFollows(
|
||||
|
||||
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.UsernameDisplay
|
||||
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.public.ScheduledFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveFlag
|
||||
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.CrossfadeCheckIfVideoIsOnline
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
|
@ -33,7 +33,7 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.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.replyModifier
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
||||
|
@ -79,6 +79,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
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.people.PubKeyReferenceTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
|
||||
@ -860,6 +861,14 @@ class AccountViewModel(
|
||||
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) {
|
||||
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(
|
||||
dirtyUrl: String,
|
||||
onInfo: (Nip11RelayInformation) -> Unit,
|
||||
|
@ -48,10 +48,10 @@ import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.buildNewPostRoute
|
||||
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.Font12SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||
@ -66,7 +66,7 @@ fun ChannelFabColumn(
|
||||
var wantsToCreateChannel by remember { mutableStateOf(false) }
|
||||
|
||||
if (wantsToCreateChannel) {
|
||||
NewChannelView({ wantsToCreateChannel = false }, accountViewModel = accountViewModel)
|
||||
ChannelMetadataDialog({ wantsToCreateChannel = false }, accountViewModel = accountViewModel)
|
||||
}
|
||||
|
||||
Column {
|
||||
|
@ -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.chats.list.ChannelFabColumn
|
||||
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
|
||||
|
||||
@Composable
|
||||
@ -113,7 +113,7 @@ fun MessagesTwoPane(
|
||||
}
|
||||
|
||||
if (it.route == "Channel") {
|
||||
Channel(
|
||||
ChannelView(
|
||||
channelId = it.id,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
|
@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
@ -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.send.ChatNewMessageViewModel
|
||||
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.nip17Dm.base.ChatroomKey
|
||||
import kotlinx.coroutines.launch
|
||||
@ -176,7 +176,7 @@ fun ChatroomViewUI(
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
|
@ -69,6 +69,7 @@ import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font12SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing
|
||||
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size18Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size5Modifier
|
||||
@ -379,7 +380,7 @@ private fun RenderDraftEvent(
|
||||
nav: INav,
|
||||
) {
|
||||
ObserveDraftEvent(note, accountViewModel) {
|
||||
Column {
|
||||
Column(verticalArrangement = RowColSpacing5dp) {
|
||||
RenderReplyRow(
|
||||
note = it,
|
||||
innerQuote = innerQuote,
|
||||
|
@ -57,6 +57,7 @@ import com.vitorpamplona.quartz.nip14Subject.subject
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.BaseDMGroupEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group
|
||||
import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.quotes.quotes
|
||||
import com.vitorpamplona.quartz.nip19Bech32.toNpub
|
||||
@ -179,6 +180,12 @@ open class ChatNewMessageViewModel : ViewModel() {
|
||||
|
||||
open fun reply(replyNote: Note) {
|
||||
replyTo.value = replyNote
|
||||
saveDraft()
|
||||
}
|
||||
|
||||
fun clearReply() {
|
||||
replyTo.value = null
|
||||
saveDraft()
|
||||
}
|
||||
|
||||
open fun quote(quote: Note) {
|
||||
@ -224,8 +231,6 @@ open class ChatNewMessageViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
private fun loadFromDraft(draft: Note) {
|
||||
Log.d("draft", draft.event!!.toJson())
|
||||
|
||||
val draftEvent = draft.event ?: return
|
||||
val accountViewModel = accountViewModel ?: return
|
||||
|
||||
@ -264,9 +269,29 @@ open class ChatNewMessageViewModel : ViewModel() {
|
||||
TextFieldValue(
|
||||
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) {
|
||||
val recepientNpub = draftEvent.verifiedRecipientPubKey()?.let { Hex.decode(it).toNpub() }
|
||||
toUsers = TextFieldValue("@$recepientNpub")
|
||||
|
||||
val replyId = draftEvent.replyTo()
|
||||
if (replyId != null) {
|
||||
accountViewModel.checkGetOrCreateNote(replyId) {
|
||||
replyTo.value = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
message =
|
||||
|
@ -42,12 +42,15 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
|
||||
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
|
||||
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.IncognitoIconOff
|
||||
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.utils.DisplayReplyingToNote
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ThinSendButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.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.Size30Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
@ -70,6 +76,23 @@ import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
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
|
||||
fun PrivateMessageEditFieldRow(
|
||||
channelScreenModel: ChatNewMessageViewModel,
|
||||
@ -77,7 +100,11 @@ fun PrivateMessageEditFieldRow(
|
||||
onSendNewMessage: () -> Unit,
|
||||
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) {
|
||||
launch(Dispatchers.IO) {
|
||||
@ -148,8 +175,7 @@ fun PrivateMessageEditFieldRow(
|
||||
},
|
||||
trailingIcon = {
|
||||
ThinSendButton(
|
||||
isActive =
|
||||
channelScreenModel.message.text.isNotBlank() && !channelScreenModel.isUploadingImage,
|
||||
isActive = channelScreenModel.message.text.isNotBlank() && !channelScreenModel.isUploadingImage,
|
||||
modifier = EditFieldTrailingIconModifier,
|
||||
) {
|
||||
channelScreenModel.sendPost(onSendNewMessage)
|
||||
@ -163,10 +189,7 @@ fun PrivateMessageEditFieldRow(
|
||||
SelectFromGallery(
|
||||
isUploading = channelScreenModel.isUploadingImage,
|
||||
tint = MaterialTheme.colorScheme.placeholderText,
|
||||
modifier =
|
||||
Modifier
|
||||
.size(30.dp)
|
||||
.padding(start = 2.dp),
|
||||
modifier = EditFieldLeadingIconModifier,
|
||||
onImageChosen = channelScreenModel::pickedMedia,
|
||||
)
|
||||
|
||||
|
@ -208,8 +208,8 @@ private fun ImageVideoPostChat(
|
||||
title = R.string.add_sensitive_content_label,
|
||||
description = R.string.add_sensitive_content_description,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||
checked = fileUploadState.sensitiveContent,
|
||||
onCheckedChange = { fileUploadState.sensitiveContent = it },
|
||||
checked = fileUploadState.contentWarning,
|
||||
onCheckedChange = fileUploadState::updateContentWarning,
|
||||
)
|
||||
|
||||
SettingsRow(R.string.file_server, R.string.file_server_description) {
|
||||
|
@ -40,7 +40,11 @@ class ChatFileUploadState(
|
||||
|
||||
var selectedServer by mutableStateOf(defaultServer)
|
||||
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
|
||||
var multiOrchestrator by mutableStateOf<MultiOrchestrator?>(null)
|
||||
@ -72,4 +76,13 @@ class ChatFileUploadState(
|
||||
fun canPost(): Boolean = !isUploadingImage && multiOrchestrator != null
|
||||
|
||||
fun hasPickedMedia() = multiOrchestrator != null
|
||||
|
||||
fun updateContentWarning(value: Boolean) {
|
||||
contentWarning = value
|
||||
if (value) {
|
||||
contentWarningReason = ""
|
||||
} else {
|
||||
contentWarningReason = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ class ChatFileUploader(
|
||||
orchestrator.uploadEncrypted(
|
||||
scope,
|
||||
viewState.caption,
|
||||
if (viewState.sensitiveContent) "" else null,
|
||||
viewState.contentWarningReason,
|
||||
MediaCompressor.intToCompressorQuality(viewState.mediaQualitySlider),
|
||||
cipher,
|
||||
viewState.selectedServer,
|
||||
@ -88,9 +88,7 @@ class ChatFileUploader(
|
||||
alt(viewState.caption)
|
||||
}
|
||||
|
||||
if (viewState.sensitiveContent) {
|
||||
contentWarning("")
|
||||
}
|
||||
viewState.contentWarningReason?.let { contentWarning(it) }
|
||||
}
|
||||
|
||||
account.sendNIP17EncryptedFile(template)
|
||||
@ -125,7 +123,7 @@ class ChatFileUploader(
|
||||
orchestrator.upload(
|
||||
scope,
|
||||
viewState.caption,
|
||||
if (viewState.sensitiveContent) "" else null,
|
||||
viewState.contentWarningReason,
|
||||
MediaCompressor.intToCompressorQuality(viewState.mediaQualitySlider),
|
||||
viewState.selectedServer,
|
||||
account,
|
||||
@ -136,7 +134,7 @@ class ChatFileUploader(
|
||||
results.successful.forEach {
|
||||
if (it.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
|
||||
val iMetaAttachments = IMetaAttachments()
|
||||
iMetaAttachments.add(it.result, viewState.caption, if (viewState.sensitiveContent) "" else null)
|
||||
iMetaAttachments.add(it.result, viewState.caption, viewState.contentWarningReason)
|
||||
|
||||
account.sendPrivateMessage(
|
||||
message = it.result.url,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.actions
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.metadata
|
||||
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
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.launch
|
||||
|
||||
class NewChannelViewModel : ViewModel() {
|
||||
class ChannelMetadataViewModel : ViewModel() {
|
||||
private var account: Account? = null
|
||||
private var originalChannel: PublicChatChannel? = null
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
@ -38,7 +38,7 @@ fun ThinSendButton(
|
||||
) {
|
||||
IconButton(
|
||||
enabled = isActive,
|
||||
modifier = modifier,
|
||||
// modifier = modifier,
|
||||
onClick = onClick,
|
||||
) {
|
||||
Icon(
|
||||
|
@ -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.RenderFeedState
|
||||
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.mockAccountViewModel
|
||||
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 StdTopPadding = Modifier.padding(top = 10.dp)
|
||||
val HalfTopPadding = Modifier.padding(top = 5.dp)
|
||||
|
||||
val HalfHalfVertPadding = Modifier.padding(vertical = 3.dp)
|
||||
val HalfHalfHorzModifier = Modifier.padding(horizontal = 3.dp)
|
||||
|
||||
val HalfPadding = Modifier.padding(5.dp)
|
||||
val StdPadding = Modifier.padding(10.dp)
|
||||
@ -207,7 +209,7 @@ val CashuCardBorders = Modifier.fillMaxWidth().padding(10.dp).clip(shape = Quote
|
||||
|
||||
val EditFieldModifier =
|
||||
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 ZeroPadding = PaddingValues(0.dp)
|
||||
@ -299,3 +301,8 @@ val ripple24dp = ripple(bounded = false, radius = Size24dp)
|
||||
val defaultTweenDuration = 100
|
||||
val defaultTweenFloatSpec = tween<Float>(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,
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -23,14 +23,17 @@ package com.vitorpamplona.quartz.nip28PublicChat.message
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
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.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
|
||||
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.base.IsInPublicChatChannel
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.base.channel
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.base.reply
|
||||
import com.vitorpamplona.quartz.nip37Drafts.ExposeInDraft
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
@ -42,7 +45,8 @@ class ChannelMessageEvent(
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
IsInPublicChatChannel {
|
||||
IsInPublicChatChannel,
|
||||
ExposeInDraft {
|
||||
override fun channel() = markedRoot() ?: unmarkedRoot()
|
||||
|
||||
override fun channelId() = channel()?.eventId
|
||||
@ -51,6 +55,12 @@ class ChannelMessageEvent(
|
||||
|
||||
override fun unmarkedReplyTos() = super.unmarkedReplyTos().filter { it != channelId() }
|
||||
|
||||
override fun exposeInDraft() =
|
||||
tagArray<ChannelMessageEvent> {
|
||||
channel()?.let { markedETag(it) }
|
||||
reply()?.let { markedETag(it) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KIND = 42
|
||||
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.signers.NostrSigner
|
||||
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.nip22Comments.CommentEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
||||
@ -121,6 +123,7 @@ class DraftEvent(
|
||||
|
||||
companion object {
|
||||
const val KIND = 31234
|
||||
const val ALT_DESCRIPTION = "Draft Event"
|
||||
|
||||
fun createAddressTag(
|
||||
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
|
||||
}
|
@ -23,16 +23,20 @@ package com.vitorpamplona.quartz.nip53LiveActivities.chat
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
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.EventHintBundle
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
|
||||
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.events.ETag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
|
||||
import com.vitorpamplona.quartz.nip10Notes.tags.markedETag
|
||||
import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag
|
||||
import com.vitorpamplona.quartz.nip37Drafts.ExposeInDraft
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@ -47,7 +51,8 @@ class LiveActivitiesChatMessageEvent(
|
||||
) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
EventHintProvider,
|
||||
PubKeyHintProvider,
|
||||
AddressHintProvider {
|
||||
AddressHintProvider,
|
||||
ExposeInDraft {
|
||||
override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint)
|
||||
|
||||
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 exposeInDraft() =
|
||||
tagArray<LiveActivitiesChatMessageEvent> {
|
||||
activity()?.let { aTag(it) }
|
||||
reply()?.let { markedETag(it) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KIND = 1311
|
||||
const val ALT = "Live activity chat message"
|
||||
|
Loading…
x
Reference in New Issue
Block a user