Zaps, Likes and Replies in Public Chats and Private Messages

This commit is contained in:
Vitor Pamplona 2023-02-24 13:12:31 -05:00
parent 236177c6ce
commit bc50f08ca2
8 changed files with 564 additions and 292 deletions

View File

@ -289,14 +289,50 @@ class Account(
LocalCache.consume(signedEvent, null) LocalCache.consume(signedEvent, null)
} }
fun createPrivateMessageWithReply(
recipientPubKey: ByteArray,
msg: String,
replyTos: List<String>? = null, mentions: List<String>? = null,
privateKey: ByteArray,
createdAt: Long = Date().time / 1000,
publishedRecipientPubKey: ByteArray? = null,
advertiseNip18: Boolean = true
): PrivateDmEvent {
val content = Utils.encrypt(
if (advertiseNip18) {
PrivateDmEvent.nip18Advertisement
} else { "" } + msg,
privateKey,
recipientPubKey)
val pubKey = Utils.pubkeyCreate(privateKey)
val tags = mutableListOf<List<String>>()
publishedRecipientPubKey?.let {
tags.add(listOf("p", publishedRecipientPubKey.toHex()))
}
replyTos?.forEach {
tags.add(listOf("e", it))
}
mentions?.forEach {
tags.add(listOf("p", it))
}
val id = Event.generateId(pubKey, createdAt, PrivateDmEvent.kind, tags, content)
val sig = Utils.sign(id, privateKey)
return PrivateDmEvent(id, pubKey, createdAt, tags, content, sig)
}
fun sendPrivateMeesage(message: String, toUser: String, replyingTo: Note? = null) { fun sendPrivateMeesage(message: String, toUser: String, replyingTo: Note? = null) {
if (!isWriteable()) return if (!isWriteable()) return
val user = LocalCache.users[toUser] ?: return val user = LocalCache.users[toUser] ?: return
val signedEvent = PrivateDmEvent.create( val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null }
val mentionsHex = emptyList<String>()
val signedEvent = createPrivateMessageWithReply(
recipientPubKey = user.pubkey(), recipientPubKey = user.pubkey(),
publishedRecipientPubKey = user.pubkey(), publishedRecipientPubKey = user.pubkey(),
msg = message, msg = message,
replyTos = repliesToHex,
mentions = mentionsHex,
privateKey = loggedIn.privKey!!, privateKey = loggedIn.privKey!!,
advertiseNip18 = false advertiseNip18 = false
) )

View File

@ -1,34 +1,48 @@
package com.vitorpamplona.amethyst.ui.note package com.vitorpamplona.amethyst.ui.note
import android.content.Context
import android.widget.Toast
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Divider
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.IconButton import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bolt
import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.icons.outlined.Bolt
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -38,16 +52,23 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.navigation.NavController import androidx.navigation.NavController
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.RoboHashCache import com.vitorpamplona.amethyst.RoboHashCache
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
@ -56,13 +77,15 @@ import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.ResizeImage import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import kotlinx.coroutines.launch
val ChatBubbleShapeMe = RoundedCornerShape(15.dp, 15.dp, 3.dp, 15.dp) val ChatBubbleShapeMe = RoundedCornerShape(15.dp, 15.dp, 3.dp, 15.dp)
val ChatBubbleShapeThem = RoundedCornerShape(3.dp, 15.dp, 15.dp, 15.dp) val ChatBubbleShapeThem = RoundedCornerShape(3.dp, 15.dp, 15.dp, 15.dp)
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun ChatroomMessageCompose(baseNote: Note, routeForLastRead: String?, innerQuote: Boolean = false, accountViewModel: AccountViewModel, navController: NavController) { fun ChatroomMessageCompose(baseNote: Note, routeForLastRead: String?, innerQuote: Boolean = false, accountViewModel: AccountViewModel, navController: NavController, onWantsToReply: (Note) -> Unit) {
val noteState by baseNote.live().metadata.observeAsState() val noteState by baseNote.live().metadata.observeAsState()
val note = noteState?.note val note = noteState?.note
@ -121,17 +144,22 @@ fun ChatroomMessageCompose(baseNote: Note, routeForLastRead: String?, innerQuote
Column() { Column() {
Row( Row(
modifier = Modifier.fillMaxWidth(1f).padding( modifier = Modifier
start = 12.dp, .fillMaxWidth(1f)
end = 12.dp, .padding(
top = 5.dp, start = 12.dp,
bottom = 5.dp end = 12.dp,
), top = 5.dp,
bottom = 5.dp
),
horizontalArrangement = alignment horizontalArrangement = alignment
) { ) {
var availableBubbleSize by remember { mutableStateOf(IntSize.Zero) }
Row( Row(
horizontalArrangement = alignment, horizontalArrangement = alignment,
modifier = Modifier.fillMaxWidth(if (innerQuote) 1f else 0.85f) modifier = Modifier.fillMaxWidth(if (innerQuote) 1f else 0.85f).onSizeChanged {
availableBubbleSize = it
},
) { ) {
Surface( Surface(
@ -143,8 +171,12 @@ fun ChatroomMessageCompose(baseNote: Note, routeForLastRead: String?, innerQuote
onLongClick = { popupExpanded = true } onLongClick = { popupExpanded = true }
) )
) { ) {
var bubbleSize by remember { mutableStateOf(IntSize.Zero) }
Column( Column(
modifier = Modifier.padding(start = 10.dp, end = 10.dp, bottom = 5.dp), modifier = Modifier.padding(start = 10.dp, end = 5.dp, bottom = 5.dp).onSizeChanged {
bubbleSize = it
},
) { ) {
val authorState by note.author!!.live().metadata.observeAsState() val authorState by note.author!!.live().metadata.observeAsState()
@ -195,7 +227,8 @@ fun ChatroomMessageCompose(baseNote: Note, routeForLastRead: String?, innerQuote
null, null,
innerQuote = true, innerQuote = true,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
navController = navController navController = navController,
onWantsToReply = onWantsToReply
) )
} }
} }
@ -244,20 +277,37 @@ fun ChatroomMessageCompose(baseNote: Note, routeForLastRead: String?, innerQuote
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.End, horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.padding(top = 2.dp) modifier = Modifier.padding(top = 2.dp).then(
) { with(LocalDensity.current) {
Text( Modifier.widthIn(bubbleSize.width.toDp(), availableBubbleSize.width.toDp())
timeAgoLong(note.event?.createdAt), }
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
fontSize = 12.sp
) )
) {
Row() {
Text(
timeAgoShort(note.event?.createdAt),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
fontSize = 12.sp
)
RelayBadges(note) RelayBadges(note)
Spacer(modifier = Modifier.width(10.dp))
}
Row() {
LikeReaction(baseNote, accountViewModel)
Spacer(modifier = Modifier.width(10.dp))
ZapReaction(baseNote, accountViewModel)
Spacer(modifier = Modifier.width(10.dp))
ReplyReaction(baseNote, accountViewModel, showCounter = false) {
onWantsToReply(baseNote)
}
}
} }
} }
} }
} }
NoteDropDownMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel) NoteDropDownMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
@ -266,8 +316,6 @@ fun ChatroomMessageCompose(baseNote: Note, routeForLastRead: String?, innerQuote
} }
} }
@Composable @Composable
private fun RelayBadges(baseNote: Note) { private fun RelayBadges(baseNote: Note) {
val noteRelaysState by baseNote.live().relays.observeAsState() val noteRelaysState by baseNote.live().relays.observeAsState()
@ -283,7 +331,10 @@ private fun RelayBadges(baseNote: Note) {
FlowRow(Modifier.padding(start = 10.dp)) { FlowRow(Modifier.padding(start = 10.dp)) {
relaysToDisplay.forEach { relaysToDisplay.forEach {
val url = it.removePrefix("wss://") val url = it.removePrefix("wss://")
Box(Modifier.size(15.dp).padding(1.dp)) { Box(
Modifier
.size(15.dp)
.padding(1.dp)) {
AsyncImage( AsyncImage(
model = "https://${url}/favicon.ico", model = "https://${url}/favicon.ico",
placeholder = BitmapPainter(RoboHashCache.get(ctx, url)), placeholder = BitmapPainter(RoboHashCache.get(ctx, url)),
@ -295,7 +346,7 @@ private fun RelayBadges(baseNote: Note) {
.fillMaxSize(1f) .fillMaxSize(1f)
.clip(shape = CircleShape) .clip(shape = CircleShape)
.background(MaterialTheme.colors.background) .background(MaterialTheme.colors.background)
.clickable(onClick = { uri.openUri("https://" + url) } ) .clickable(onClick = { uri.openUri("https://" + url) })
) )
} }
} }

View File

@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.note package com.vitorpamplona.amethyst.ui.note
import android.content.Context
import android.widget.Toast import android.widget.Toast
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
@ -49,6 +50,7 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.UriHandler
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
@ -78,6 +80,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import java.math.BigDecimal import java.math.BigDecimal
import java.math.RoundingMode import java.math.RoundingMode
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -87,23 +90,6 @@ fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel) {
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return val account = accountState?.account ?: return
val reactionsState by baseNote.live().reactions.observeAsState()
val reactedNote = reactionsState?.note
val boostsState by baseNote.live().boosts.observeAsState()
val boostedNote = boostsState?.note
val zapsState by baseNote.live().zaps.observeAsState()
val zappedNote = zapsState?.note
val repliesState by baseNote.live().replies.observeAsState()
val replies = repliesState?.note?.replies ?: emptySet()
val grayTint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
val uri = LocalUriHandler.current
val context = LocalContext.current
val scope = rememberCoroutineScope()
var wantsToReplyTo by remember { var wantsToReplyTo by remember {
mutableStateOf<Note?>(null) mutableStateOf<Note?>(null)
} }
@ -118,256 +104,337 @@ fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel) {
if (wantsToQuote != null) if (wantsToQuote != null)
NewPostView({ wantsToQuote = null }, null, wantsToQuote, account) NewPostView({ wantsToQuote = null }, null, wantsToQuote, account)
var wantsToZap by remember { mutableStateOf(false) }
var wantsToChangeZapAmount by remember { mutableStateOf(false) }
var wantsToBoost by remember { mutableStateOf(false) }
Row( Row(
modifier = Modifier modifier = Modifier.padding(top = 8.dp).fillMaxWidth(),
.padding(top = 8.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
IconButton( ReplyReaction(baseNote, accountViewModel, Modifier.weight(1f)) {
modifier = Modifier.then(Modifier.size(20.dp)), wantsToReplyTo = baseNote
onClick = {
if (account.isWriteable())
wantsToReplyTo = baseNote
else
scope.launch {
Toast.makeText(
context,
"Login with a Private key to be able to reply",
Toast.LENGTH_SHORT
).show()
}
}
) {
Icon(
painter = painterResource(R.drawable.ic_comment),
null,
modifier = Modifier.size(15.dp),
tint = grayTint,
)
} }
Text( BoostReaction(baseNote, accountViewModel, Modifier.weight(1f)) {
" ${showCount(replies.size)}", wantsToQuote = baseNote
fontSize = 14.sp,
color = grayTint,
modifier = Modifier.weight(1f)
)
IconButton(
modifier = Modifier.then(Modifier.size(20.dp)),
onClick = {
if (account.isWriteable())
wantsToBoost = true
else
scope.launch {
Toast.makeText(
context,
"Login with a Private key to be able to boost posts",
Toast.LENGTH_SHORT
).show()
}
}
) {
if (wantsToBoost) {
BoostTypeChoicePopup(
baseNote,
accountViewModel,
onDismiss = {
wantsToBoost = false
},
onQuote = {
wantsToBoost = false
wantsToQuote = baseNote
}
)
}
if (boostedNote?.isBoostedBy(account.userProfile()) == true) {
Icon(
painter = painterResource(R.drawable.ic_retweeted),
null,
modifier = Modifier.size(20.dp),
tint = Color.Unspecified
)
} else {
Icon(
painter = painterResource(R.drawable.ic_retweet),
null,
modifier = Modifier.size(20.dp),
tint = grayTint
)
}
} }
Text( LikeReaction(baseNote, accountViewModel, Modifier.weight(1f))
" ${showCount(boostedNote?.boosts?.size)}",
fontSize = 14.sp,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
modifier = Modifier.weight(1f)
)
IconButton( ZapReaction(baseNote, accountViewModel, Modifier.weight(1f))
modifier = Modifier.then(Modifier.size(20.dp)),
onClick = {
if (account.isWriteable())
accountViewModel.reactTo(baseNote)
else
scope.launch {
Toast.makeText(
context,
"Login with a Private key to like Posts",
Toast.LENGTH_SHORT
).show()
}
}
) {
if (reactedNote?.isReactedBy(account.userProfile()) == true) {
Icon(
painter = painterResource(R.drawable.ic_liked),
null,
modifier = Modifier.size(16.dp),
tint = Color.Unspecified
)
} else {
Icon(
painter = painterResource(R.drawable.ic_like),
null,
modifier = Modifier.size(16.dp),
tint = grayTint
)
}
}
Text( ViewCountReaction(baseNote, Modifier.weight(1f))
" ${showCount(reactedNote?.reactions?.size)}",
fontSize = 14.sp,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
modifier = Modifier.weight(1f)
)
Row(
modifier = Modifier
.then(Modifier.size(20.dp))
.combinedClickable(
role = Role.Button,
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false, radius = 24.dp),
onClick = {
if (account.zapAmountChoices.isEmpty()) {
scope.launch {
Toast
.makeText(
context,
"No Zap Amount Setup. Long Press to change",
Toast.LENGTH_SHORT
)
.show()
}
} else if (!account.isWriteable()) {
scope.launch {
Toast
.makeText(
context,
"Login with a Private key to be able to send Zaps",
Toast.LENGTH_SHORT
)
.show()
}
} else if (account.zapAmountChoices.size == 1) {
accountViewModel.zap(baseNote, account.zapAmountChoices.first() * 1000, "", context) {
scope.launch {
Toast
.makeText(context, it, Toast.LENGTH_SHORT)
.show()
}
}
} else if (account.zapAmountChoices.size > 1) {
wantsToZap = true
}
},
onLongClick = {
wantsToChangeZapAmount = true
}
)
) {
if (wantsToZap) {
ZapAmountChoicePopup(
baseNote,
accountViewModel,
onDismiss = {
wantsToZap = false
},
onChangeAmount = {
wantsToZap = false
wantsToChangeZapAmount = true
}
)
}
if (wantsToChangeZapAmount) {
UpdateZapAmountDialog({ wantsToChangeZapAmount = false }, account = account)
}
if (zappedNote?.isZappedBy(account.userProfile()) == true) {
Icon(
imageVector = Icons.Default.Bolt,
contentDescription = "Zaps",
modifier = Modifier.size(20.dp),
tint = BitcoinOrange
)
} else {
Icon(
imageVector = Icons.Outlined.Bolt,
contentDescription = "Zaps",
modifier = Modifier.size(20.dp),
tint = grayTint
)
}
}
Text(
showAmount(zappedNote?.zappedAmount()),
fontSize = 14.sp,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
modifier = Modifier.weight(1f)
)
IconButton(
modifier = Modifier.then(Modifier.size(20.dp)),
onClick = { uri.openUri("https://counter.amethyst.social/${baseNote.idHex}/") }
) {
Icon(
imageVector = Icons.Outlined.BarChart,
null,
modifier = Modifier.size(19.dp),
tint = grayTint
)
}
Row(modifier = Modifier.weight(1f)) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data("https://counter.amethyst.social/${baseNote.idHex}.svg?label=+&color=00000000")
.crossfade(true)
.diskCachePolicy(CachePolicy.DISABLED)
.memoryCachePolicy(CachePolicy.ENABLED)
.build(),
contentDescription = "View count",
modifier = Modifier.height(24.dp),
colorFilter = ColorFilter.tint(grayTint)
)
}
} }
} }
@Composable
fun ReplyReaction(
baseNote: Note,
accountViewModel: AccountViewModel,
textModifier: Modifier = Modifier,
showCounter: Boolean = true,
onPress: () -> Unit,
) {
val repliesState by baseNote.live().replies.observeAsState()
val replies = repliesState?.note?.replies ?: emptySet()
val grayTint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
val context = LocalContext.current
val scope = rememberCoroutineScope()
IconButton(
modifier = Modifier.then(Modifier.size(20.dp)),
onClick = {
if (accountViewModel.isWriteable())
onPress()
else
scope.launch {
Toast.makeText(
context,
"Login with a Private key to be able to reply",
Toast.LENGTH_SHORT
).show()
}
}
) {
Icon(
painter = painterResource(R.drawable.ic_comment),
null,
modifier = Modifier.size(15.dp),
tint = grayTint,
)
}
if (showCounter)
Text(
" ${showCount(replies.size)}",
fontSize = 14.sp,
color = grayTint,
modifier = textModifier
)
}
@Composable
private fun BoostReaction(
baseNote: Note,
accountViewModel: AccountViewModel,
textModifier: Modifier = Modifier,
onQuotePress: () -> Unit,
) {
val boostsState by baseNote.live().boosts.observeAsState()
val boostedNote = boostsState?.note
val grayTint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
val context = LocalContext.current
val scope = rememberCoroutineScope()
var wantsToBoost by remember { mutableStateOf(false) }
IconButton(
modifier = Modifier.then(Modifier.size(20.dp)),
onClick = {
if (accountViewModel.isWriteable())
wantsToBoost = true
else
scope.launch {
Toast.makeText(
context,
"Login with a Private key to be able to boost posts",
Toast.LENGTH_SHORT
).show()
}
}
) {
if (wantsToBoost) {
BoostTypeChoicePopup(
baseNote,
accountViewModel,
onDismiss = {
wantsToBoost = false
},
onQuote = {
wantsToBoost = false
onQuotePress()
}
)
}
if (boostedNote?.isBoostedBy(accountViewModel.userProfile()) == true) {
Icon(
painter = painterResource(R.drawable.ic_retweeted),
null,
modifier = Modifier.size(20.dp),
tint = Color.Unspecified
)
} else {
Icon(
painter = painterResource(R.drawable.ic_retweet),
null,
modifier = Modifier.size(20.dp),
tint = grayTint
)
}
}
Text(
" ${showCount(boostedNote?.boosts?.size)}",
fontSize = 14.sp,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
modifier = textModifier
)
}
@Composable
fun LikeReaction(
baseNote: Note,
accountViewModel: AccountViewModel,
textModifier: Modifier = Modifier
) {
val reactionsState by baseNote.live().reactions.observeAsState()
val reactedNote = reactionsState?.note
val grayTint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
val context = LocalContext.current
val scope = rememberCoroutineScope()
IconButton(
modifier = Modifier.then(Modifier.size(20.dp)),
onClick = {
if (accountViewModel.isWriteable())
accountViewModel.reactTo(baseNote)
else
scope.launch {
Toast.makeText(
context,
"Login with a Private key to like Posts",
Toast.LENGTH_SHORT
).show()
}
}
) {
if (reactedNote?.isReactedBy(accountViewModel.userProfile()) == true) {
Icon(
painter = painterResource(R.drawable.ic_liked),
null,
modifier = Modifier.size(16.dp),
tint = Color.Unspecified
)
} else {
Icon(
painter = painterResource(R.drawable.ic_like),
null,
modifier = Modifier.size(16.dp),
tint = grayTint
)
}
}
Text(
" ${showCount(reactedNote?.reactions?.size)}",
fontSize = 14.sp,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
modifier = textModifier
)
}
@Composable
@OptIn(ExperimentalFoundationApi::class)
fun ZapReaction(
baseNote: Note,
accountViewModel: AccountViewModel,
textModifier: Modifier = Modifier,
) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
val zapsState by baseNote.live().zaps.observeAsState()
val zappedNote = zapsState?.note
var wantsToZap by remember { mutableStateOf(false) }
var wantsToChangeZapAmount by remember { mutableStateOf(false) }
val grayTint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
val context = LocalContext.current
val scope = rememberCoroutineScope()
Row(
modifier = Modifier
.then(Modifier.size(20.dp))
.combinedClickable(
role = Role.Button,
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false, radius = 24.dp),
onClick = {
if (account.zapAmountChoices.isEmpty()) {
scope.launch {
Toast
.makeText(
context,
"No Zap Amount Setup. Long Press to change",
Toast.LENGTH_SHORT
)
.show()
}
} else if (!accountViewModel.isWriteable()) {
scope.launch {
Toast
.makeText(
context,
"Login with a Private key to be able to send Zaps",
Toast.LENGTH_SHORT
)
.show()
}
} else if (account.zapAmountChoices.size == 1) {
accountViewModel.zap(baseNote, account.zapAmountChoices.first() * 1000, "", context) {
scope.launch {
Toast
.makeText(context, it, Toast.LENGTH_SHORT)
.show()
}
}
} else if (account.zapAmountChoices.size > 1) {
wantsToZap = true
}
},
onLongClick = {
wantsToChangeZapAmount = true
}
)
) {
if (wantsToZap) {
ZapAmountChoicePopup(
baseNote,
accountViewModel,
onDismiss = {
wantsToZap = false
},
onChangeAmount = {
wantsToZap = false
wantsToChangeZapAmount = true
}
)
}
if (wantsToChangeZapAmount) {
UpdateZapAmountDialog({ wantsToChangeZapAmount = false }, account = account)
}
if (zappedNote?.isZappedBy(account.userProfile()) == true) {
Icon(
imageVector = Icons.Default.Bolt,
contentDescription = "Zaps",
modifier = Modifier.size(20.dp),
tint = BitcoinOrange
)
} else {
Icon(
imageVector = Icons.Outlined.Bolt,
contentDescription = "Zaps",
modifier = Modifier.size(20.dp),
tint = grayTint
)
}
}
Text(
showAmount(zappedNote?.zappedAmount()),
fontSize = 14.sp,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
modifier = textModifier
)
}
@Composable
private fun ViewCountReaction(baseNote: Note, textModifier: Modifier = Modifier) {
val uri = LocalUriHandler.current
val grayTint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
IconButton(
modifier = Modifier.then(Modifier.size(20.dp)),
onClick = { uri.openUri("https://counter.amethyst.social/${baseNote.idHex}/") }
) {
Icon(
imageVector = Icons.Outlined.BarChart,
null,
modifier = Modifier.size(19.dp),
tint = grayTint
)
}
Row(modifier = textModifier) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data("https://counter.amethyst.social/${baseNote.idHex}.svg?label=+&color=00000000")
.crossfade(true)
.diskCachePolicy(CachePolicy.DISABLED)
.memoryCachePolicy(CachePolicy.ENABLED)
.build(),
contentDescription = "View count",
modifier = Modifier.height(24.dp),
colorFilter = ColorFilter.tint(grayTint)
)
}
}
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
@ -415,7 +482,7 @@ private fun BoostTypeChoicePopup(baseNote: Note, accountViewModel: AccountViewMo
@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class) @OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
@Composable @Composable
private fun ZapAmountChoicePopup(baseNote: Note, accountViewModel: AccountViewModel, onDismiss: () -> Unit, onChangeAmount: () -> Unit) { fun ZapAmountChoicePopup(baseNote: Note, accountViewModel: AccountViewModel, onDismiss: () -> Unit, onChangeAmount: () -> Unit) {
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()

View File

@ -22,6 +22,22 @@ fun timeAgo(mills: Long?): String {
.replace(" days ago", "d") .replace(" days ago", "d")
} }
fun timeAgoShort(mills: Long?): String {
if (mills == null) return " "
var humanReadable = DateUtils.getRelativeTimeSpanString(
mills * 1000,
System.currentTimeMillis(),
DateUtils.MINUTE_IN_MILLIS,
DateUtils.FORMAT_ABBREV_ALL
).toString()
if (humanReadable.startsWith("In") || humanReadable.startsWith("0")) {
humanReadable = "now";
}
return humanReadable
}
fun timeAgoLong(mills: Long?): String { fun timeAgoLong(mills: Long?): String {
if (mills == null) return " " if (mills == null) return " "

View File

@ -18,11 +18,12 @@ import androidx.compose.runtime.collectAsState
import androidx.navigation.NavController import androidx.navigation.NavController
import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable @Composable
fun ChatroomFeedView(viewModel: FeedViewModel, accountViewModel: AccountViewModel, navController: NavController, routeForLastRead: String?) { fun ChatroomFeedView(viewModel: FeedViewModel, accountViewModel: AccountViewModel, navController: NavController, routeForLastRead: String?, onWantsToReply: (Note) -> Unit ) {
val feedState by viewModel.feedContent.collectAsState() val feedState by viewModel.feedContent.collectAsState()
var isRefreshing by remember { mutableStateOf(false) } var isRefreshing by remember { mutableStateOf(false) }
@ -65,7 +66,7 @@ fun ChatroomFeedView(viewModel: FeedViewModel, accountViewModel: AccountViewMode
) { ) {
var previousDate: String = "" var previousDate: String = ""
itemsIndexed(state.feed.value, key = { index, item -> item.idHex }) { index, item -> itemsIndexed(state.feed.value, key = { index, item -> item.idHex }) { index, item ->
ChatroomMessageCompose(item, routeForLastRead, accountViewModel = accountViewModel, navController = navController) ChatroomMessageCompose(item, routeForLastRead, accountViewModel = accountViewModel, navController = navController, onWantsToReply = onWantsToReply)
} }
} }
} }

View File

@ -22,6 +22,14 @@ class AccountViewModel(private val account: Account): ViewModel() {
val accountLiveData: LiveData<AccountState> = account.live.map { it } val accountLiveData: LiveData<AccountState> = account.live.map { it }
val accountLanguagesLiveData: LiveData<AccountState> = account.liveLanguages.map { it } val accountLanguagesLiveData: LiveData<AccountState> = account.liveLanguages.map { it }
fun isWriteable(): Boolean {
return account.isWriteable()
}
fun userProfile(): User {
return account.userProfile()
}
fun reactTo(note: Note) { fun reactTo(note: Note) {
account.reactTo(note) account.reactTo(note)
} }

View File

@ -4,10 +4,13 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@ -18,12 +21,14 @@ import androidx.compose.material.Divider
import androidx.compose.material.DropdownMenu import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.LocalTextStyle import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.TextField import androidx.compose.material.TextField
import androidx.compose.material.TextFieldDefaults import androidx.compose.material.TextFieldDefaults
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Cancel
import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.EditNote import androidx.compose.material.icons.filled.EditNote
import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.filled.Share
@ -74,6 +79,7 @@ import com.vitorpamplona.amethyst.ui.actions.NewPostView
import com.vitorpamplona.amethyst.ui.actions.PostButton import com.vitorpamplona.amethyst.ui.actions.PostButton
import com.vitorpamplona.amethyst.ui.dal.ChannelFeedFilter import com.vitorpamplona.amethyst.ui.dal.ChannelFeedFilter
import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.navigation.Route
import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import nostr.postr.toNpub import nostr.postr.toNpub
@ -84,6 +90,7 @@ fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, accoun
if (account != null && channelId != null) { if (account != null && channelId != null) {
val newPost = remember { mutableStateOf(TextFieldValue("")) } val newPost = remember { mutableStateOf(TextFieldValue("")) }
val replyTo = remember { mutableStateOf<Note?>(null) }
ChannelFeedFilter.loadMessagesBetween(account, channelId) ChannelFeedFilter.loadMessagesBetween(account, channelId)
NostrChannelDataSource.loadMessagesBetween(channelId) NostrChannelDataSource.loadMessagesBetween(channelId)
@ -130,13 +137,47 @@ fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, accoun
.padding(vertical = 0.dp) .padding(vertical = 0.dp)
.weight(1f, true) .weight(1f, true)
) { ) {
ChatroomFeedView(feedViewModel, accountViewModel, navController, "Channel/${channelId}") ChatroomFeedView(feedViewModel, accountViewModel, navController, "Channel/${channelId}") {
replyTo.value = it
}
}
Spacer(modifier = Modifier.height(10.dp))
val replyingNote = replyTo.value
if (replyingNote != null) {
Row(Modifier.padding(horizontal = 10.dp), verticalAlignment = Alignment.CenterVertically) {
Column(Modifier.weight(1f)) {
ChatroomMessageCompose(
baseNote = replyingNote,
null,
innerQuote = true,
accountViewModel = accountViewModel,
navController = navController,
onWantsToReply = {
replyTo.value = it
}
)
}
Column(Modifier.padding(end = 10.dp)) {
IconButton(
modifier = Modifier.size(30.dp),
onClick = { replyTo.value = null }
) {
Icon(
imageVector = Icons.Default.Cancel,
null,
modifier = Modifier.padding(end = 5.dp).size(30.dp),
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
}
}
}
} }
//LAST ROW //LAST ROW
Row(modifier = Modifier Row(modifier = Modifier.padding(start = 10.dp, end = 10.dp, bottom = 10.dp).fillMaxWidth(),
.padding(10.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@ -158,8 +199,9 @@ fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, accoun
trailingIcon = { trailingIcon = {
PostButton( PostButton(
onPost = { onPost = {
account.sendChannelMeesage(newPost.value.text, channel.idHex, null, null) account.sendChannelMeesage(newPost.value.text, channel.idHex, replyTo.value, null)
newPost.value = TextFieldValue("") newPost.value = TextFieldValue("")
replyTo.value = null
feedViewModel.refresh() // Don't wait a full second before updating feedViewModel.refresh() // Don't wait a full second before updating
}, },
newPost.value.text.isNotBlank(), newPost.value.text.isNotBlank(),
@ -220,7 +262,9 @@ fun ChannelHeader(baseChannel: Channel, account: Account, accountStateViewModel:
} }
} }
Row(modifier = Modifier.height(35.dp).padding(bottom = 3.dp)) { Row(modifier = Modifier
.height(35.dp)
.padding(bottom = 3.dp)) {
NoteCopyButton(channel) NoteCopyButton(channel)
if (channel.creator == account.userProfile()) { if (channel.creator == account.userProfile()) {
@ -253,7 +297,9 @@ private fun NoteCopyButton(
var popupExpanded by remember { mutableStateOf(false) } var popupExpanded by remember { mutableStateOf(false) }
Button( Button(
modifier = Modifier.padding(horizontal = 3.dp).width(50.dp), modifier = Modifier
.padding(horizontal = 3.dp)
.width(50.dp),
onClick = { popupExpanded = true }, onClick = { popupExpanded = true },
shape = RoundedCornerShape(20.dp), shape = RoundedCornerShape(20.dp),
colors = ButtonDefaults colors = ButtonDefaults
@ -288,7 +334,9 @@ private fun EditButton(account: Account, channel: Channel) {
NewChannelView({ wantsToPost = false }, account = account, channel) NewChannelView({ wantsToPost = false }, account = account, channel)
Button( Button(
modifier = Modifier.padding(horizontal = 3.dp).width(50.dp), modifier = Modifier
.padding(horizontal = 3.dp)
.width(50.dp),
onClick = { wantsToPost = true }, onClick = { wantsToPost = true },
shape = RoundedCornerShape(20.dp), shape = RoundedCornerShape(20.dp),
colors = ButtonDefaults colors = ButtonDefaults

View File

@ -4,20 +4,26 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Divider import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.LocalTextStyle import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.TextField import androidx.compose.material.TextField
import androidx.compose.material.TextFieldDefaults import androidx.compose.material.TextFieldDefaults
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Cancel
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@ -41,6 +47,7 @@ import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import com.vitorpamplona.amethyst.RoboHashCache import com.vitorpamplona.amethyst.RoboHashCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.NostrChannelDataSource import com.vitorpamplona.amethyst.service.NostrChannelDataSource
import com.vitorpamplona.amethyst.service.NostrChatroomDataSource import com.vitorpamplona.amethyst.service.NostrChatroomDataSource
@ -49,6 +56,7 @@ import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.ResizeImage import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.actions.PostButton import com.vitorpamplona.amethyst.ui.actions.PostButton
import com.vitorpamplona.amethyst.ui.dal.ChatroomFeedFilter import com.vitorpamplona.amethyst.ui.dal.ChatroomFeedFilter
import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@ -59,6 +67,7 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr
if (account != null && userId != null) { if (account != null && userId != null) {
val newPost = remember { mutableStateOf(TextFieldValue("")) } val newPost = remember { mutableStateOf(TextFieldValue("")) }
val replyTo = remember { mutableStateOf<Note?>(null) }
ChatroomFeedFilter.loadMessagesBetween(account, userId) ChatroomFeedFilter.loadMessagesBetween(account, userId)
NostrChatroomDataSource.loadMessagesBetween(account, userId) NostrChatroomDataSource.loadMessagesBetween(account, userId)
@ -104,12 +113,47 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr
.padding(vertical = 0.dp) .padding(vertical = 0.dp)
.weight(1f, true) .weight(1f, true)
) { ) {
ChatroomFeedView(feedViewModel, accountViewModel, navController, "Room/${userId}") ChatroomFeedView(feedViewModel, accountViewModel, navController, "Room/${userId}") {
replyTo.value = it
}
}
Spacer(modifier = Modifier.height(10.dp))
val replyingNote = replyTo.value
if (replyingNote != null) {
Row(Modifier.padding(horizontal = 10.dp), verticalAlignment = Alignment.CenterVertically) {
Column(Modifier.weight(1f)) {
ChatroomMessageCompose(
baseNote = replyingNote,
null,
innerQuote = true,
accountViewModel = accountViewModel,
navController = navController,
onWantsToReply = {
replyTo.value = it
}
)
}
Column(Modifier.padding(end = 10.dp)) {
IconButton(
modifier = Modifier.size(30.dp),
onClick = { replyTo.value = null }
) {
Icon(
imageVector = Icons.Default.Cancel,
null,
modifier = Modifier.padding(end = 5.dp).size(30.dp),
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
}
}
}
} }
//LAST ROW //LAST ROW
Row(modifier = Modifier Row(modifier = Modifier.padding(start = 10.dp, end = 10.dp, bottom = 10.dp, top = 5.dp)
.padding(10.dp)
.fillMaxWidth(), .fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
@ -132,8 +176,9 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr
trailingIcon = { trailingIcon = {
PostButton( PostButton(
onPost = { onPost = {
account.sendPrivateMeesage(newPost.value.text, userId) account.sendPrivateMeesage(newPost.value.text, userId, replyTo.value)
newPost.value = TextFieldValue("") newPost.value = TextFieldValue("")
replyTo.value = null
feedViewModel.refresh() // Don't wait a full second before updating feedViewModel.refresh() // Don't wait a full second before updating
}, },
newPost.value.text.isNotBlank(), newPost.value.text.isNotBlank(),