kopia lustrzana https://github.com/vitorpamplona/amethyst
Zaps, Likes and Replies in Public Chats and Private Messages
rodzic
236177c6ce
commit
bc50f08ca2
|
@ -289,14 +289,50 @@ class Account(
|
|||
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) {
|
||||
if (!isWriteable()) 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(),
|
||||
publishedRecipientPubKey = user.pubkey(),
|
||||
msg = message,
|
||||
replyTos = repliesToHex,
|
||||
mentions = mentionsHex,
|
||||
privateKey = loggedIn.privKey!!,
|
||||
advertiseNip18 = false
|
||||
)
|
||||
|
|
|
@ -1,34 +1,48 @@
|
|||
package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
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.RoundedCornerShape
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
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.outlined.Bolt
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
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.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
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.Shape
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
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.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import coil.compose.AsyncImage
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
import com.vitorpamplona.amethyst.NotificationCache
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.RoboHashCache
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||
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.TranslateableRichTextViewer
|
||||
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 ChatBubbleShapeThem = RoundedCornerShape(3.dp, 15.dp, 15.dp, 15.dp)
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@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 note = noteState?.note
|
||||
|
||||
|
@ -121,7 +144,9 @@ fun ChatroomMessageCompose(baseNote: Note, routeForLastRead: String?, innerQuote
|
|||
|
||||
Column() {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(1f).padding(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(1f)
|
||||
.padding(
|
||||
start = 12.dp,
|
||||
end = 12.dp,
|
||||
top = 5.dp,
|
||||
|
@ -129,9 +154,12 @@ fun ChatroomMessageCompose(baseNote: Note, routeForLastRead: String?, innerQuote
|
|||
),
|
||||
horizontalArrangement = alignment
|
||||
) {
|
||||
var availableBubbleSize by remember { mutableStateOf(IntSize.Zero) }
|
||||
Row(
|
||||
horizontalArrangement = alignment,
|
||||
modifier = Modifier.fillMaxWidth(if (innerQuote) 1f else 0.85f)
|
||||
modifier = Modifier.fillMaxWidth(if (innerQuote) 1f else 0.85f).onSizeChanged {
|
||||
availableBubbleSize = it
|
||||
},
|
||||
) {
|
||||
|
||||
Surface(
|
||||
|
@ -143,8 +171,12 @@ fun ChatroomMessageCompose(baseNote: Note, routeForLastRead: String?, innerQuote
|
|||
onLongClick = { popupExpanded = true }
|
||||
)
|
||||
) {
|
||||
var bubbleSize by remember { mutableStateOf(IntSize.Zero) }
|
||||
|
||||
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()
|
||||
|
@ -195,7 +227,8 @@ fun ChatroomMessageCompose(baseNote: Note, routeForLastRead: String?, innerQuote
|
|||
null,
|
||||
innerQuote = true,
|
||||
accountViewModel = accountViewModel,
|
||||
navController = navController
|
||||
navController = navController,
|
||||
onWantsToReply = onWantsToReply
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -244,20 +277,37 @@ fun ChatroomMessageCompose(baseNote: Note, routeForLastRead: String?, innerQuote
|
|||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.End,
|
||||
modifier = Modifier.padding(top = 2.dp)
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier.padding(top = 2.dp).then(
|
||||
with(LocalDensity.current) {
|
||||
Modifier.widthIn(bubbleSize.width.toDp(), availableBubbleSize.width.toDp())
|
||||
}
|
||||
)
|
||||
) {
|
||||
Row() {
|
||||
Text(
|
||||
timeAgoLong(note.event?.createdAt),
|
||||
timeAgoShort(note.event?.createdAt),
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||
fontSize = 12.sp
|
||||
)
|
||||
|
||||
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)
|
||||
|
@ -266,8 +316,6 @@ fun ChatroomMessageCompose(baseNote: Note, routeForLastRead: String?, innerQuote
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Composable
|
||||
private fun RelayBadges(baseNote: Note) {
|
||||
val noteRelaysState by baseNote.live().relays.observeAsState()
|
||||
|
@ -283,7 +331,10 @@ private fun RelayBadges(baseNote: Note) {
|
|||
FlowRow(Modifier.padding(start = 10.dp)) {
|
||||
relaysToDisplay.forEach {
|
||||
val url = it.removePrefix("wss://")
|
||||
Box(Modifier.size(15.dp).padding(1.dp)) {
|
||||
Box(
|
||||
Modifier
|
||||
.size(15.dp)
|
||||
.padding(1.dp)) {
|
||||
AsyncImage(
|
||||
model = "https://${url}/favicon.ico",
|
||||
placeholder = BitmapPainter(RoboHashCache.get(ctx, url)),
|
||||
|
@ -295,7 +346,7 @@ private fun RelayBadges(baseNote: Note) {
|
|||
.fillMaxSize(1f)
|
||||
.clip(shape = CircleShape)
|
||||
.background(MaterialTheme.colors.background)
|
||||
.clickable(onClick = { uri.openUri("https://" + url) } )
|
||||
.clickable(onClick = { uri.openUri("https://" + url) })
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.animateContentSize
|
||||
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.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.platform.UriHandler
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
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 java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
@ -87,23 +90,6 @@ fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel) {
|
|||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
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 {
|
||||
mutableStateOf<Note?>(null)
|
||||
}
|
||||
|
@ -118,23 +104,48 @@ fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel) {
|
|||
if (wantsToQuote != null)
|
||||
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(
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp)
|
||||
.fillMaxWidth(),
|
||||
modifier = Modifier.padding(top = 8.dp).fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
ReplyReaction(baseNote, accountViewModel, Modifier.weight(1f)) {
|
||||
wantsToReplyTo = baseNote
|
||||
}
|
||||
|
||||
BoostReaction(baseNote, accountViewModel, Modifier.weight(1f)) {
|
||||
wantsToQuote = baseNote
|
||||
}
|
||||
|
||||
LikeReaction(baseNote, accountViewModel, Modifier.weight(1f))
|
||||
|
||||
ZapReaction(baseNote, accountViewModel, Modifier.weight(1f))
|
||||
|
||||
ViewCountReaction(baseNote, Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@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 (account.isWriteable())
|
||||
wantsToReplyTo = baseNote
|
||||
if (accountViewModel.isWriteable())
|
||||
onPress()
|
||||
else
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
|
@ -153,17 +164,35 @@ fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel) {
|
|||
)
|
||||
}
|
||||
|
||||
if (showCounter)
|
||||
Text(
|
||||
" ${showCount(replies.size)}",
|
||||
fontSize = 14.sp,
|
||||
color = grayTint,
|
||||
modifier = Modifier.weight(1f)
|
||||
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 (account.isWriteable())
|
||||
if (accountViewModel.isWriteable())
|
||||
wantsToBoost = true
|
||||
else
|
||||
scope.launch {
|
||||
|
@ -184,12 +213,12 @@ fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel) {
|
|||
},
|
||||
onQuote = {
|
||||
wantsToBoost = false
|
||||
wantsToQuote = baseNote
|
||||
onQuotePress()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (boostedNote?.isBoostedBy(account.userProfile()) == true) {
|
||||
if (boostedNote?.isBoostedBy(accountViewModel.userProfile()) == true) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_retweeted),
|
||||
null,
|
||||
|
@ -210,13 +239,27 @@ fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel) {
|
|||
" ${showCount(boostedNote?.boosts?.size)}",
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||
modifier = Modifier.weight(1f)
|
||||
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 (account.isWriteable())
|
||||
if (accountViewModel.isWriteable())
|
||||
accountViewModel.reactTo(baseNote)
|
||||
else
|
||||
scope.launch {
|
||||
|
@ -228,7 +271,7 @@ fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel) {
|
|||
}
|
||||
}
|
||||
) {
|
||||
if (reactedNote?.isReactedBy(account.userProfile()) == true) {
|
||||
if (reactedNote?.isReactedBy(accountViewModel.userProfile()) == true) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_liked),
|
||||
null,
|
||||
|
@ -249,10 +292,31 @@ fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel) {
|
|||
" ${showCount(reactedNote?.reactions?.size)}",
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||
modifier = Modifier.weight(1f)
|
||||
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))
|
||||
|
@ -271,7 +335,7 @@ fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel) {
|
|||
)
|
||||
.show()
|
||||
}
|
||||
} else if (!account.isWriteable()) {
|
||||
} else if (!accountViewModel.isWriteable()) {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
|
@ -336,8 +400,14 @@ fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel) {
|
|||
showAmount(zappedNote?.zappedAmount()),
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||
modifier = Modifier.weight(1f)
|
||||
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)),
|
||||
|
@ -351,7 +421,7 @@ fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel) {
|
|||
)
|
||||
}
|
||||
|
||||
Row(modifier = Modifier.weight(1f)) {
|
||||
Row(modifier = textModifier) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data("https://counter.amethyst.social/${baseNote.idHex}.svg?label=+&color=00000000")
|
||||
|
@ -364,11 +434,8 @@ fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel) {
|
|||
colorFilter = ColorFilter.tint(grayTint)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun BoostTypeChoicePopup(baseNote: Note, accountViewModel: AccountViewModel, onDismiss: () -> Unit, onQuote: () -> Unit) {
|
||||
|
@ -415,7 +482,7 @@ private fun BoostTypeChoicePopup(baseNote: Note, accountViewModel: AccountViewMo
|
|||
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
|
||||
@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 scope = rememberCoroutineScope()
|
||||
|
||||
|
|
|
@ -22,6 +22,22 @@ fun timeAgo(mills: Long?): String {
|
|||
.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 {
|
||||
if (mills == null) return " "
|
||||
|
||||
|
|
|
@ -18,11 +18,12 @@ import androidx.compose.runtime.collectAsState
|
|||
import androidx.navigation.NavController
|
||||
import com.google.accompanist.swiperefresh.SwipeRefresh
|
||||
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@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()
|
||||
|
||||
var isRefreshing by remember { mutableStateOf(false) }
|
||||
|
@ -65,7 +66,7 @@ fun ChatroomFeedView(viewModel: FeedViewModel, accountViewModel: AccountViewMode
|
|||
) {
|
||||
var previousDate: String = ""
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,14 @@ class AccountViewModel(private val account: Account): ViewModel() {
|
|||
val accountLiveData: LiveData<AccountState> = account.live.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) {
|
||||
account.reactTo(note)
|
||||
}
|
||||
|
|
|
@ -4,10 +4,13 @@ import androidx.compose.foundation.layout.Arrangement
|
|||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
|
@ -18,12 +21,14 @@ import androidx.compose.material.Divider
|
|||
import androidx.compose.material.DropdownMenu
|
||||
import androidx.compose.material.DropdownMenuItem
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextField
|
||||
import androidx.compose.material.TextFieldDefaults
|
||||
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.EditNote
|
||||
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.dal.ChannelFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||
import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import nostr.postr.toNpub
|
||||
|
||||
|
@ -84,6 +90,7 @@ fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, accoun
|
|||
|
||||
if (account != null && channelId != null) {
|
||||
val newPost = remember { mutableStateOf(TextFieldValue("")) }
|
||||
val replyTo = remember { mutableStateOf<Note?>(null) }
|
||||
|
||||
ChannelFeedFilter.loadMessagesBetween(account, channelId)
|
||||
NostrChannelDataSource.loadMessagesBetween(channelId)
|
||||
|
@ -130,13 +137,47 @@ fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, accoun
|
|||
.padding(vertical = 0.dp)
|
||||
.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
|
||||
Row(modifier = Modifier
|
||||
.padding(10.dp)
|
||||
.fillMaxWidth(),
|
||||
Row(modifier = Modifier.padding(start = 10.dp, end = 10.dp, bottom = 10.dp).fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
@ -158,8 +199,9 @@ fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, accoun
|
|||
trailingIcon = {
|
||||
PostButton(
|
||||
onPost = {
|
||||
account.sendChannelMeesage(newPost.value.text, channel.idHex, null, null)
|
||||
account.sendChannelMeesage(newPost.value.text, channel.idHex, replyTo.value, null)
|
||||
newPost.value = TextFieldValue("")
|
||||
replyTo.value = null
|
||||
feedViewModel.refresh() // Don't wait a full second before updating
|
||||
},
|
||||
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)
|
||||
|
||||
if (channel.creator == account.userProfile()) {
|
||||
|
@ -253,7 +297,9 @@ private fun NoteCopyButton(
|
|||
var popupExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp).width(50.dp),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 3.dp)
|
||||
.width(50.dp),
|
||||
onClick = { popupExpanded = true },
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
colors = ButtonDefaults
|
||||
|
@ -288,7 +334,9 @@ private fun EditButton(account: Account, channel: Channel) {
|
|||
NewChannelView({ wantsToPost = false }, account = account, channel)
|
||||
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp).width(50.dp),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 3.dp)
|
||||
.width(50.dp),
|
||||
onClick = { wantsToPost = true },
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
colors = ButtonDefaults
|
||||
|
|
|
@ -4,20 +4,26 @@ import androidx.compose.foundation.clickable
|
|||
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.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextField
|
||||
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.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
|
@ -41,6 +47,7 @@ import androidx.lifecycle.LifecycleEventObserver
|
|||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
import com.vitorpamplona.amethyst.RoboHashCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
|
||||
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.actions.PostButton
|
||||
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.screen.loggedIn.AccountViewModel
|
||||
|
||||
|
@ -59,6 +67,7 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr
|
|||
|
||||
if (account != null && userId != null) {
|
||||
val newPost = remember { mutableStateOf(TextFieldValue("")) }
|
||||
val replyTo = remember { mutableStateOf<Note?>(null) }
|
||||
|
||||
ChatroomFeedFilter.loadMessagesBetween(account, userId)
|
||||
NostrChatroomDataSource.loadMessagesBetween(account, userId)
|
||||
|
@ -104,12 +113,47 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr
|
|||
.padding(vertical = 0.dp)
|
||||
.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
|
||||
Row(modifier = Modifier
|
||||
.padding(10.dp)
|
||||
Row(modifier = Modifier.padding(start = 10.dp, end = 10.dp, bottom = 10.dp, top = 5.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
|
@ -132,8 +176,9 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr
|
|||
trailingIcon = {
|
||||
PostButton(
|
||||
onPost = {
|
||||
account.sendPrivateMeesage(newPost.value.text, userId)
|
||||
account.sendPrivateMeesage(newPost.value.text, userId, replyTo.value)
|
||||
newPost.value = TextFieldValue("")
|
||||
replyTo.value = null
|
||||
feedViewModel.refresh() // Don't wait a full second before updating
|
||||
},
|
||||
newPost.value.text.isNotBlank(),
|
||||
|
|
Ładowanie…
Reference in New Issue