Compare commits

..
4 Commits
10 changed files with 459 additions and 350 deletions
+2 -2
View File
@@ -13,8 +13,8 @@ android {
applicationId "com.vitorpamplona.amethyst"
minSdk 26
targetSdk 33
versionCode 182
versionName "0.52.2"
versionCode 183
versionName "0.52.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -63,7 +63,6 @@ import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
@@ -78,7 +77,10 @@ import kotlinx.coroutines.launch
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = null, account: Account, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = null, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = remember(accountState) { accountState?.account } ?: return
val postViewModel: NewPostViewModel = viewModel()
val context = LocalContext.current
@@ -17,18 +17,17 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.actions.NewPostView
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
fun NewNoteButton(account: Account, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
fun NewNoteButton(accountViewModel: AccountViewModel, nav: (String) -> Unit) {
var wantsToPost by remember {
mutableStateOf(false)
}
if (wantsToPost) {
NewPostView({ wantsToPost = false }, account = account, accountViewModel = accountViewModel, nav = nav)
NewPostView({ wantsToPost = false }, accountViewModel = accountViewModel, nav = nav)
}
OutlinedButton(
@@ -83,7 +83,6 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.NoteState
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.AudioTrackEvent
import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent
@@ -151,9 +150,9 @@ fun NoteCompose(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
println("AARender NoteCompose")
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = remember(accountState) { accountState?.account } ?: return
val loggedIn = remember(accountState) { accountState?.account?.userProfile() } ?: return
val noteState by baseNote.live().metadata.observeAsState()
val note = remember(noteState) { noteState?.note } ?: return
@@ -161,13 +160,11 @@ fun NoteCompose(
val noteReportsState by baseNote.live().reports.observeAsState()
val noteForReports = remember(noteReportsState) { noteReportsState?.note } ?: return
val noteEvent = remember(noteState) { note.event }
val baseChannel = remember(noteState) { note.channel() }
val isSensitive = remember(noteState) { note.event?.isSensitive() ?: false }
var popupExpanded by remember { mutableStateOf(false) }
if (note.event == null) {
var popupExpanded by remember { mutableStateOf(false) }
if (noteEvent == null) {
BlankNote(
remember {
modifier.combinedClickable(
@@ -178,13 +175,11 @@ fun NoteCompose(
isBoostedNote
)
note.let {
NoteQuickActionMenu(it, popupExpanded, { popupExpanded = false }, accountViewModel)
}
NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
} else if (account.isHidden(noteForReports.author!!) || (isSensitive && account.showSensitiveContent == false)) {
// Does nothing
} else {
var showHiddenNote by remember { mutableStateOf(false) }
var showReportedNote by remember { mutableStateOf(false) }
var isAcceptableAndCanPreview by remember { mutableStateOf(Pair(true, true)) }
LaunchedEffect(key1 = noteReportsState, key2 = accountState) {
@@ -203,192 +198,236 @@ fun NoteCompose(
}
}
if (!isAcceptableAndCanPreview.first && !showHiddenNote) {
if (!isAcceptableAndCanPreview.first && !showReportedNote) {
HiddenNote(
account.getRelevantReports(noteForReports),
account.userProfile(),
modifier,
isBoostedNote,
nav,
onClick = { showHiddenNote = true }
onClick = { showReportedNote = true }
)
} else if ((noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) && baseChannel != null) {
ChannelHeader(baseChannel = baseChannel, account = account, nav = nav)
} else if (noteEvent is BadgeDefinitionEvent) {
BadgeDisplay(baseNote = note)
} else if (noteEvent is FileHeaderEvent) {
FileHeaderDisplay(note)
} else if (noteEvent is FileStorageHeaderEvent) {
FileStorageHeaderDisplay(note)
} else {
var isNew by remember { mutableStateOf<Boolean>(false) }
NormalNote(
baseNote,
routeForLastRead,
modifier,
isBoostedNote,
isQuotedNote,
unPackReply,
makeItShort,
addMarginTop,
isAcceptableAndCanPreview.second,
parentBackgroundColor,
accountViewModel,
nav
)
}
}
}
val scope = rememberCoroutineScope()
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun NormalNote(
baseNote: Note,
routeForLastRead: String? = null,
modifier: Modifier = Modifier,
isBoostedNote: Boolean = false,
isQuotedNote: Boolean = false,
unPackReply: Boolean = true,
makeItShort: Boolean = false,
addMarginTop: Boolean = true,
canPreview: Boolean = true,
parentBackgroundColor: Color? = null,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
println("AARender NormalNote")
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = remember(accountState) { accountState?.account } ?: return
val loggedIn = remember(accountState) { accountState?.account?.userProfile() } ?: return
LaunchedEffect(key1 = routeForLastRead) {
scope.launch(Dispatchers.IO) {
routeForLastRead?.let {
val lastTime = NotificationCache.load(it)
val noteEvent = remember { baseNote.event }
val channelHex = remember { baseNote.channelHex() }
val createdAt = note.createdAt()
if (createdAt != null) {
NotificationCache.markAsRead(it, createdAt)
var popupExpanded by remember { mutableStateOf(false) }
val newIsNew = createdAt > lastTime
if (newIsNew != isNew) {
isNew = newIsNew
}
if ((noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) && channelHex != null) {
ChannelHeader(channelHex = channelHex, account = account, nav = nav)
} else if (noteEvent is BadgeDefinitionEvent) {
BadgeDisplay(baseNote = baseNote)
} else if (noteEvent is FileHeaderEvent) {
FileHeaderDisplay(baseNote)
} else if (noteEvent is FileStorageHeaderEvent) {
FileStorageHeaderDisplay(baseNote)
} else {
var isNew by remember { mutableStateOf<Boolean>(false) }
val scope = rememberCoroutineScope()
LaunchedEffect(key1 = routeForLastRead) {
scope.launch(Dispatchers.IO) {
routeForLastRead?.let {
val lastTime = NotificationCache.load(it)
val createdAt = baseNote.createdAt()
if (createdAt != null) {
NotificationCache.markAsRead(it, createdAt)
val newIsNew = createdAt > lastTime
if (newIsNew != isNew) {
isNew = newIsNew
}
}
}
}
}
val primaryColor = MaterialTheme.colors.primary.copy(0.12f)
val defaultBackgroundColor = MaterialTheme.colors.background
val primaryColor = MaterialTheme.colors.primary.copy(0.12f)
val defaultBackgroundColor = MaterialTheme.colors.background
val backgroundColor = remember(isNew, parentBackgroundColor) {
if (isNew) {
if (parentBackgroundColor != null) {
primaryColor.compositeOver(parentBackgroundColor)
} else {
primaryColor.compositeOver(defaultBackgroundColor)
}
val backgroundColor = remember(isNew, parentBackgroundColor) {
if (isNew) {
if (parentBackgroundColor != null) {
primaryColor.compositeOver(parentBackgroundColor)
} else {
parentBackgroundColor ?: defaultBackgroundColor
primaryColor.compositeOver(defaultBackgroundColor)
}
} else {
parentBackgroundColor ?: defaultBackgroundColor
}
}
val columnModifier = remember(backgroundColor) {
modifier
.combinedClickable(
onClick = {
scope.launch {
routeFor(note, loggedIn)?.let {
nav(it)
}
val columnModifier = remember(backgroundColor) {
modifier
.combinedClickable(
onClick = {
scope.launch {
routeFor(baseNote, loggedIn)?.let {
nav(it)
}
},
onLongClick = { popupExpanded = true }
)
.background(backgroundColor)
}
}
},
onLongClick = { popupExpanded = true }
)
.background(backgroundColor)
}
Column(modifier = columnModifier) {
Row(
Column(modifier = columnModifier) {
Row(
modifier = remember {
Modifier
.padding(
start = if (!isBoostedNote) 12.dp else 0.dp,
end = if (!isBoostedNote) 12.dp else 0.dp,
top = if (addMarginTop && !isBoostedNote) 10.dp else 0.dp
)
}
) {
if (!isBoostedNote && !isQuotedNote) {
DrawAuthorImages(baseNote, loggedIn, nav)
}
Column(
modifier = remember {
Modifier
.padding(
start = if (!isBoostedNote) 12.dp else 0.dp,
end = if (!isBoostedNote) 12.dp else 0.dp,
top = if (addMarginTop && !isBoostedNote) 10.dp else 0.dp
)
.padding(start = if (!isBoostedNote && !isQuotedNote) 10.dp else 0.dp)
}
) {
if (!isBoostedNote && !isQuotedNote) {
DrawAuthorImages(baseNote, loggedIn, nav)
FirstUserInfoRow(
baseNote = baseNote,
showAuthorPicture = isQuotedNote,
account = account,
accountViewModel = accountViewModel,
nav = nav
)
if (noteEvent !is RepostEvent && !makeItShort && !isQuotedNote) {
SecondUserInfoRow(
baseNote,
account,
nav
)
}
Column(
modifier = remember {
Modifier
.padding(start = if (!isBoostedNote && !isQuotedNote) 10.dp else 0.dp)
}
) {
FirstUserInfoRow(
baseNote = baseNote,
showAuthorPicture = isQuotedNote,
account = account,
accountViewModel = accountViewModel,
nav = nav
Spacer(modifier = Modifier.height(2.dp))
if (!makeItShort) {
ReplyRow(
baseNote,
unPackReply,
backgroundColor,
account,
accountViewModel,
nav
)
}
if (noteEvent !is RepostEvent && !makeItShort && !isQuotedNote) {
SecondUserInfoRow(
note,
account,
nav
)
when (noteEvent) {
is ReactionEvent -> {
RenderReaction(baseNote, backgroundColor, accountViewModel, nav)
}
Spacer(modifier = Modifier.height(2.dp))
is RepostEvent -> {
RenderRepost(baseNote, backgroundColor, accountViewModel, nav)
}
if (!makeItShort) {
ReplyRow(
note,
unPackReply,
is ReportEvent -> {
RenderReport(baseNote)
}
is LongTextNoteEvent -> {
RenderLongFormContent(baseNote, loggedIn, accountViewModel, nav)
}
is BadgeAwardEvent -> {
RenderBadgeAward(baseNote, backgroundColor, accountViewModel, nav)
}
is PeopleListEvent -> {
RenderPeopleList(baseNote, backgroundColor, accountViewModel, nav)
}
is AudioTrackEvent -> {
RenderAudioTrack(baseNote, loggedIn, accountViewModel, nav)
}
is PinListEvent -> {
RenderPinListEvent(baseNote, backgroundColor, accountViewModel, nav)
}
is PrivateDmEvent -> {
RenderPrivateMessage(baseNote, makeItShort, canPreview, backgroundColor, accountViewModel, nav)
}
is HighlightEvent -> {
RenderHighlight(baseNote, makeItShort, canPreview, backgroundColor, accountViewModel, nav)
}
is PollNoteEvent -> {
RenderPoll(
baseNote,
makeItShort,
canPreview,
backgroundColor,
account,
accountViewModel,
nav
)
}
when (noteEvent) {
is ReactionEvent -> {
RenderReaction(note, backgroundColor, accountViewModel, nav)
}
is RepostEvent -> {
RenderRepost(note, backgroundColor, accountViewModel, nav)
}
is ReportEvent -> {
RenderReport(note)
}
is LongTextNoteEvent -> {
RenderLongFormContent(note, loggedIn, accountViewModel, nav)
}
is BadgeAwardEvent -> {
RenderBadgeAward(note, backgroundColor, accountViewModel, nav)
}
is PeopleListEvent -> {
RenderPeopleList(noteState, backgroundColor, accountViewModel, nav)
}
is AudioTrackEvent -> {
RenderAudioTrack(note, loggedIn, accountViewModel, nav)
}
is PinListEvent -> {
RenderPinListEvent(noteState, backgroundColor, accountViewModel, nav)
}
is PrivateDmEvent -> {
RenderPrivateMessage(note, makeItShort, isAcceptableAndCanPreview.second, backgroundColor, accountViewModel, nav)
}
is HighlightEvent -> {
RenderHighlight(note, makeItShort, isAcceptableAndCanPreview.second, backgroundColor, accountViewModel, nav)
}
is PollNoteEvent -> {
RenderPoll(
note,
makeItShort,
isAcceptableAndCanPreview.second,
backgroundColor,
accountViewModel,
nav
)
}
else -> {
RenderTextEvent(
note,
makeItShort,
isAcceptableAndCanPreview.second,
backgroundColor,
accountViewModel,
nav
)
}
else -> {
RenderTextEvent(
baseNote,
makeItShort,
canPreview,
backgroundColor,
accountViewModel,
nav
)
}
NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
}
NoteQuickActionMenu(baseNote, popupExpanded, { popupExpanded = false }, accountViewModel)
}
}
}
@@ -581,14 +620,15 @@ private fun RenderPrivateMessage(
val noteEvent = note.event as? PrivateDmEvent ?: return
val withMe = remember { noteEvent.with(accountViewModel.userProfile().pubkeyHex) }
val tags = remember(note.event?.id()) { note.event?.tags() }
val hashtags = remember(note.event?.id()) { note.event?.hashtags() ?: emptyList() }
val modifier = remember(note.event?.id()) { Modifier.fillMaxWidth() }
val isAuthorTheLoggedUser = remember(note.event?.id()) { accountViewModel.isLoggedUser(note.author) }
if (withMe) {
val eventContent = remember { accountViewModel.decrypt(note) }
val hashtags = remember(note.event?.id()) { note.event?.hashtags() ?: emptyList() }
val modifier = remember(note.event?.id()) { Modifier.fillMaxWidth() }
val isAuthorTheLoggedUser = remember(note.event?.id()) { accountViewModel.isLoggedUser(note.author) }
val tags = remember(note.event?.id()) { note.event?.tags() }
if (eventContent != null) {
if (makeItShort && isAuthorTheLoggedUser) {
Text(
@@ -628,7 +668,7 @@ private fun RenderPrivateMessage(
),
canPreview = !makeItShort,
Modifier.fillMaxWidth(),
noteEvent.tags(),
emptyList(),
backgroundColor,
accountViewModel,
nav
@@ -647,16 +687,14 @@ private fun RenderPrivateMessage(
@Composable
fun RenderPeopleList(
noteState: NoteState?,
baseNote: Note,
backgroundColor: Color,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
DisplayPeopleList(noteState, backgroundColor, accountViewModel, nav)
DisplayPeopleList(baseNote, backgroundColor, accountViewModel, nav)
noteState?.note?.let {
ReactionsRow(it, accountViewModel, nav)
}
ReactionsRow(baseNote, accountViewModel, nav)
Divider(
modifier = Modifier.padding(top = 10.dp),
@@ -666,13 +704,12 @@ fun RenderPeopleList(
@Composable
fun DisplayPeopleList(
noteState: NoteState?,
baseNote: Note,
backgroundColor: Color,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val note = remember(noteState) { noteState?.note } ?: return
val noteEvent = note.event as? PeopleListEvent ?: return
val noteEvent = baseNote.event as? PeopleListEvent ?: return
var members by remember { mutableStateOf<List<User>>(listOf()) }
@@ -698,7 +735,7 @@ fun DisplayPeopleList(
textAlign = TextAlign.Center
)
LaunchedEffect(key1 = noteState) {
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
members = noteEvent.bookmarkedPeople().mapNotNull { hex ->
LocalCache.checkGetOrCreateUser(hex)
@@ -875,16 +912,16 @@ private fun RenderRepost(
@Composable
private fun RenderPinListEvent(
noteState: NoteState?,
baseNote: Note,
backgroundColor: Color,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val noteEvent = noteState?.note?.event as? PinListEvent ?: return
val noteEvent = baseNote.event as? PinListEvent ?: return
PinListHeader(noteState, backgroundColor, accountViewModel, nav)
PinListHeader(baseNote, backgroundColor, accountViewModel, nav)
ReactionsRow(noteState?.note, accountViewModel, nav)
ReactionsRow(baseNote, accountViewModel, nav)
Divider(
modifier = Modifier.padding(top = 10.dp),
@@ -894,15 +931,14 @@ private fun RenderPinListEvent(
@Composable
fun PinListHeader(
noteState: NoteState?,
baseNote: Note,
backgroundColor: Color,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val note = remember(noteState) { noteState?.note } ?: return
val noteEvent = note.event as? PinListEvent ?: return
val noteEvent = baseNote.event as? PinListEvent ?: return
var pins by remember { mutableStateOf<List<String>>(noteEvent.pins()) }
var pins by remember { mutableStateOf(noteEvent.pins()) }
var expanded by remember {
mutableStateOf(false)
@@ -1,6 +1,7 @@
package com.vitorpamplona.amethyst.ui.note
import android.widget.Toast
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -20,6 +21,7 @@ import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.ProgressIndicatorDefaults
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bolt
@@ -56,6 +58,7 @@ import coil.request.CachePolicy
import coil.request.ImageRequest
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.actions.NewPostView
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
@@ -68,9 +71,6 @@ import kotlin.math.roundToInt
@Composable
fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = remember(accountState) { accountState?.account } ?: return
val grayTint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
var wantsToReplyTo by remember {
@@ -82,17 +82,17 @@ fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel, nav: (Strin
}
if (wantsToReplyTo != null) {
NewPostView({ wantsToReplyTo = null }, wantsToReplyTo, null, account, accountViewModel, nav)
NewPostView({ wantsToReplyTo = null }, wantsToReplyTo, null, accountViewModel, nav)
}
if (wantsToQuote != null) {
NewPostView({ wantsToQuote = null }, null, wantsToQuote, account, accountViewModel, nav)
NewPostView({ wantsToQuote = null }, null, wantsToQuote, accountViewModel, nav)
}
Spacer(modifier = Modifier.height(8.dp))
Row(verticalAlignment = CenterVertically) {
Row(verticalAlignment = CenterVertically, modifier = Modifier.weight(1f)) {
Row(verticalAlignment = CenterVertically, modifier = remember { Modifier.weight(1f) }) {
ReplyReaction(baseNote, grayTint, accountViewModel) {
wantsToReplyTo = baseNote
}
@@ -123,17 +123,6 @@ fun ReplyReaction(
iconSize: Dp = 20.dp,
onPress: () -> Unit
) {
val repliesState by baseNote.live().replies.observeAsState()
val replies = remember(repliesState) { repliesState?.note?.replies } ?: emptySet()
val isWriteable = remember { accountViewModel.isWriteable() }
val replyCount by remember(repliesState) {
derivedStateOf {
showCount(replies.size)
}
}
val context = LocalContext.current
val scope = rememberCoroutineScope()
@@ -148,7 +137,7 @@ fun ReplyReaction(
IconButton(
modifier = iconButtonModifier,
onClick = {
if (isWriteable) {
if (accountViewModel.isWriteable()) {
onPress()
} else {
scope.launch {
@@ -170,14 +159,26 @@ fun ReplyReaction(
}
if (showCounter) {
Text(
" $replyCount",
fontSize = 14.sp,
color = grayTint
)
ReplyCounter(baseNote, grayTint)
}
}
@Composable
fun ReplyCounter(baseNote: Note, textColor: Color) {
val repliesState by baseNote.live().replies.observeAsState()
val replyCount by remember(repliesState) {
derivedStateOf {
" " + showCount(repliesState?.note?.replies?.size)
}
}
Text(
text = replyCount,
fontSize = 14.sp,
color = textColor
)
}
@Composable
fun BoostReaction(
baseNote: Note,
@@ -186,29 +187,6 @@ fun BoostReaction(
iconSize: Dp = 20.dp,
onQuotePress: () -> Unit
) {
val boostsState by baseNote.live().boosts.observeAsState()
val boostedNote = remember(boostsState) { boostsState?.note } ?: return
val hasBoosted by remember(boostsState) {
derivedStateOf {
accountViewModel.hasBoosted(baseNote)
}
}
val wasBoostedByLoggedIn by remember(boostsState) {
derivedStateOf {
boostedNote.isBoostedBy(accountViewModel.userProfile())
}
}
val isWriteable = remember { accountViewModel.isWriteable() }
val boostCount by remember(boostsState) {
derivedStateOf {
showCount(boostedNote.boosts.size)
}
}
val context = LocalContext.current
val scope = rememberCoroutineScope()
@@ -218,15 +196,11 @@ fun BoostReaction(
Modifier.size(iconSize)
}
val iconModifier = remember {
Modifier.size(iconSize)
}
IconButton(
modifier = iconButtonModifier,
onClick = {
if (isWriteable) {
if (hasBoosted) {
if (accountViewModel.isWriteable()) {
if (accountViewModel.hasBoosted(baseNote)) {
accountViewModel.deleteBoostsTo(baseNote)
} else {
wantsToBoost = true
@@ -256,16 +230,45 @@ fun BoostReaction(
)
}
Icon(
painter = painterResource(R.drawable.ic_retweeted),
null,
modifier = iconModifier,
tint = if (wasBoostedByLoggedIn) Color.Unspecified else grayTint
)
BoostIcon(baseNote, iconSize, grayTint, accountViewModel.userProfile())
}
BoostText(baseNote, grayTint)
}
@Composable
fun BoostIcon(baseNote: Note, iconSize: Dp = 20.dp, grayTint: Color, loggedIn: User) {
val boostsState by baseNote.live().boosts.observeAsState()
val iconTint by remember(boostsState) {
derivedStateOf {
if (boostsState?.note?.isBoostedBy(loggedIn) == true) Color.Unspecified else grayTint
}
}
val iconModifier = remember {
Modifier.size(iconSize)
}
Icon(
painter = painterResource(R.drawable.ic_retweeted),
null,
modifier = iconModifier,
tint = iconTint
)
}
@Composable
fun BoostText(baseNote: Note, grayTint: Color) {
val boostsState by baseNote.live().boosts.observeAsState()
val boostCount by remember(boostsState) {
derivedStateOf {
" " + showCount(boostsState?.note?.boosts?.size)
}
}
Text(
" $boostCount",
boostCount,
fontSize = 14.sp,
color = grayTint
)
@@ -279,29 +282,6 @@ fun LikeReaction(
iconSize: Dp = 20.dp,
heartSize: Dp = 16.dp
) {
val reactionsState by baseNote.live().reactions.observeAsState()
val reactedNote = remember(reactionsState) { reactionsState?.note } ?: return
val hasReacted by remember(reactionsState) {
derivedStateOf {
accountViewModel.hasReactedTo(baseNote)
}
}
val wasReactedByLoggedIn by remember(reactionsState) {
derivedStateOf {
reactedNote.isReactedBy(accountViewModel.userProfile())
}
}
val isWriteable = remember { accountViewModel.isWriteable() }
val reactionCount by remember(reactionsState) {
derivedStateOf {
showCount(reactedNote.reactions.size)
}
}
val context = LocalContext.current
val scope = rememberCoroutineScope()
@@ -309,15 +289,11 @@ fun LikeReaction(
Modifier.size(iconSize)
}
val iconModifier = remember {
Modifier.size(heartSize)
}
IconButton(
modifier = iconButtonModifier,
onClick = {
if (isWriteable) {
if (hasReacted) {
if (accountViewModel.isWriteable()) {
if (accountViewModel.hasReactedTo(baseNote)) {
accountViewModel.deleteReactionTo(baseNote)
} else {
accountViewModel.reactTo(baseNote)
@@ -333,25 +309,55 @@ fun LikeReaction(
}
}
) {
if (wasReactedByLoggedIn) {
Icon(
painter = painterResource(R.drawable.ic_liked),
null,
modifier = iconModifier,
tint = Color.Unspecified
)
} else {
Icon(
painter = painterResource(R.drawable.ic_like),
null,
modifier = iconModifier,
tint = grayTint
)
LikeIcon(baseNote, heartSize, grayTint, accountViewModel.userProfile())
}
LikeText(baseNote, grayTint)
}
@Composable
fun LikeIcon(baseNote: Note, iconSize: Dp = 20.dp, grayTint: Color, loggedIn: User) {
val reactionsState by baseNote.live().reactions.observeAsState()
val wasReactedByLoggedIn by remember(reactionsState) {
derivedStateOf {
reactionsState?.note?.isReactedBy(loggedIn) == true
}
}
val iconModifier = remember {
Modifier.size(iconSize)
}
if (wasReactedByLoggedIn) {
Icon(
painter = painterResource(R.drawable.ic_liked),
null,
modifier = iconModifier,
tint = Color.Unspecified
)
} else {
Icon(
painter = painterResource(R.drawable.ic_like),
null,
modifier = iconModifier,
tint = grayTint
)
}
}
@Composable
fun LikeText(baseNote: Note, grayTint: Color) {
val reactionsState by baseNote.live().reactions.observeAsState()
val reactionsCount by remember(reactionsState) {
derivedStateOf {
" " + showCount(reactionsState?.note?.reactions?.size)
}
}
Text(
" $reactionCount",
reactionsCount,
fontSize = 14.sp,
color = grayTint
)
@@ -363,16 +369,12 @@ fun ZapReaction(
baseNote: Note,
grayTint: Color,
accountViewModel: AccountViewModel,
textModifier: Modifier = Modifier,
iconSize: Dp = 20.dp,
animationSize: Dp = 14.dp
) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = remember(accountState) { accountState?.account } ?: return
val zapsState by baseNote.live().zaps.observeAsState()
val zappedNote = remember(zapsState) { zapsState?.note } ?: return
var wantsToZap by remember { mutableStateOf(false) }
var wantsToChangeZapAmount by remember { mutableStateOf(false) }
var wantsToSetCustomZap by remember { mutableStateOf(false) }
@@ -382,32 +384,6 @@ fun ZapReaction(
var zappingProgress by remember { mutableStateOf(0f) }
var wasZappedByLoggedInUser by remember { mutableStateOf(false) }
var zapAmountTxt by remember { mutableStateOf<String>("") }
LaunchedEffect(key1 = zapsState) {
scope.launch(Dispatchers.IO) {
if (!wasZappedByLoggedInUser) {
val newWasZapped = accountViewModel.calculateIfNoteWasZappedByAccount(zappedNote)
if (wasZappedByLoggedInUser != newWasZapped) {
wasZappedByLoggedInUser = newWasZapped
}
}
val newZapAmount = showAmount(account.calculateZappedAmount(zappedNote))
if (newZapAmount != zapAmountTxt) {
zapAmountTxt = newZapAmount
}
if (wasZappedByLoggedInUser) {
if (abs(zappingProgress - 1) < 0.001) {
zappingProgress = 1f
}
}
}
}
Row(
verticalAlignment = CenterVertically,
modifier = Modifier
@@ -498,6 +474,7 @@ fun ZapReaction(
}
)
}
if (wantsToChangeZapAmount) {
UpdateZapAmountDialog({ wantsToChangeZapAmount = false }, account = account)
}
@@ -506,28 +483,93 @@ fun ZapReaction(
ZapCustomDialog({ wantsToSetCustomZap = false }, account = account, accountViewModel, baseNote)
}
if (wasZappedByLoggedInUser) {
Icon(
imageVector = Icons.Default.Bolt,
contentDescription = stringResource(R.string.zaps),
modifier = Modifier.size(iconSize),
tint = BitcoinOrange
if (zappingProgress > 0.00001 && zappingProgress < 0.99999) {
Spacer(Modifier.width(3.dp))
val animatedProgress = animateFloatAsState(
targetValue = zappingProgress,
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec
).value
CircularProgressIndicator(
progress = animatedProgress,
modifier = Modifier.size(animationSize),
strokeWidth = 2.dp
)
} else {
if (zappingProgress < 0.1 || zappingProgress > 0.99) {
Icon(
imageVector = Icons.Outlined.Bolt,
contentDescription = stringResource(id = R.string.zaps),
modifier = Modifier.size(iconSize),
tint = grayTint
)
} else {
Spacer(Modifier.width(3.dp))
CircularProgressIndicator(
progress = zappingProgress,
modifier = Modifier.size(animationSize),
strokeWidth = 2.dp
)
ZapIcon(
baseNote,
iconSize,
grayTint,
accountViewModel
)
}
}
ZapAmountText(baseNote, grayTint, accountViewModel)
}
@Composable
private fun ZapIcon(
baseNote: Note,
iconSize: Dp,
grayTint: Color,
accountViewModel: AccountViewModel
) {
var wasZappedByLoggedInUser by remember { mutableStateOf(false) }
val zapsState by baseNote.live().zaps.observeAsState()
val scope = rememberCoroutineScope()
LaunchedEffect(key1 = zapsState) {
scope.launch(Dispatchers.IO) {
zapsState?.note?.let {
if (!wasZappedByLoggedInUser) {
val newWasZapped = accountViewModel.calculateIfNoteWasZappedByAccount(it)
if (wasZappedByLoggedInUser != newWasZapped) {
wasZappedByLoggedInUser = newWasZapped
}
}
}
}
}
if (wasZappedByLoggedInUser) {
Icon(
imageVector = Icons.Default.Bolt,
contentDescription = stringResource(R.string.zaps),
modifier = remember { Modifier.size(iconSize) },
tint = BitcoinOrange
)
} else {
Icon(
imageVector = Icons.Outlined.Bolt,
contentDescription = stringResource(id = R.string.zaps),
modifier = remember { Modifier.size(iconSize) },
tint = grayTint
)
}
}
@Composable
private fun ZapAmountText(
baseNote: Note,
grayTint: Color,
accountViewModel: AccountViewModel
) {
val zapsState by baseNote.live().zaps.observeAsState()
val zappedNote = remember(zapsState) { zapsState?.note } ?: return
val scope = rememberCoroutineScope()
var zapAmountTxt by remember { mutableStateOf("") }
LaunchedEffect(key1 = zapsState) {
scope.launch(Dispatchers.IO) {
val newZapAmount = showAmount(accountViewModel.calculateZapAmount(zappedNote))
if (newZapAmount != zapAmountTxt) {
zapAmountTxt = newZapAmount
}
}
}
@@ -535,8 +577,7 @@ fun ZapReaction(
Text(
zapAmountTxt,
fontSize = 14.sp,
color = grayTint,
modifier = textModifier
color = grayTint
)
}
@@ -353,11 +353,11 @@ fun NoteMaster(
) {
Column() {
if (noteEvent is PeopleListEvent) {
DisplayPeopleList(noteState, MaterialTheme.colors.background, accountViewModel, nav)
DisplayPeopleList(baseNote, MaterialTheme.colors.background, accountViewModel, nav)
} else if (noteEvent is AudioTrackEvent) {
AudioTrackHeader(noteEvent, note, account.userProfile(), nav)
} else if (noteEvent is PinListEvent) {
PinListHeader(noteState, MaterialTheme.colors.background, accountViewModel, nav)
PinListHeader(baseNote, MaterialTheme.colors.background, accountViewModel, nav)
} else if (noteEvent is HighlightEvent) {
DisplayHighlight(
noteEvent.quote(),
@@ -23,6 +23,7 @@ import com.vitorpamplona.amethyst.service.model.ReportEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.math.BigDecimal
import java.util.Locale
@Immutable
@@ -66,6 +67,10 @@ class AccountViewModel(private val account: Account) : ViewModel() {
return account.calculateIfNoteWasZappedByAccount(zappedNote)
}
fun calculateZapAmount(zappedNote: Note): BigDecimal {
return account.calculateZappedAmount(zappedNote)
}
fun zap(note: Note, amount: Long, pollOption: Int?, message: String, context: Context, onError: (String) -> Unit, onProgress: (percent: Float) -> Unit, zapType: LnZapEvent.ZapType) {
val lud16 = note.event?.zapAddress() ?: note.author?.info?.lud16?.trim() ?: note.author?.info?.lud06?.trim()
@@ -105,7 +110,7 @@ class AccountViewModel(private val account: Account) : ViewModel() {
?: "Error parsing error message"
)
} else {
onProgress(0.99f)
onProgress(1f)
}
}
)
@@ -40,6 +40,7 @@ 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
@@ -63,6 +64,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
import com.vitorpamplona.amethyst.ui.actions.NewChannelView
@@ -77,6 +79,8 @@ import com.vitorpamplona.amethyst.ui.navigation.Route
import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose
import com.vitorpamplona.amethyst.ui.screen.ChatroomFeedView
import com.vitorpamplona.amethyst.ui.screen.NostrChannelFeedViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
fun ChannelScreen(
@@ -158,7 +162,12 @@ fun ChannelScreen(
Spacer(modifier = Modifier.height(10.dp))
Row(Modifier.padding(horizontal = 10.dp).animateContentSize(), verticalAlignment = Alignment.CenterVertically) {
Row(
Modifier
.padding(horizontal = 10.dp)
.animateContentSize(),
verticalAlignment = Alignment.CenterVertically
) {
val replyingNote = replyTo.value
if (replyingNote != null) {
Column(Modifier.weight(1f)) {
@@ -182,7 +191,9 @@ fun ChannelScreen(
Icon(
imageVector = Icons.Default.Cancel,
null,
modifier = Modifier.padding(end = 5.dp).size(30.dp),
modifier = Modifier
.padding(end = 5.dp)
.size(30.dp),
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
}
@@ -192,7 +203,9 @@ fun ChannelScreen(
// LAST ROW
Row(
modifier = Modifier.padding(start = 10.dp, end = 10.dp, bottom = 10.dp, top = 5.dp).fillMaxWidth(),
modifier = Modifier
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp, top = 5.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
@@ -246,10 +259,26 @@ fun ChannelScreen(
}
}
@Composable
fun ChannelHeader(channelHex: String, account: Account, nav: (String) -> Unit) {
var baseChannel by remember { mutableStateOf<Channel?>(null) }
val scope = rememberCoroutineScope()
LaunchedEffect(key1 = channelHex) {
scope.launch(Dispatchers.IO) {
baseChannel = LocalCache.checkGetOrCreateChannel(channelHex)
}
}
baseChannel?.let {
ChannelHeader(it, account, nav)
}
}
@Composable
fun ChannelHeader(baseChannel: Channel, account: Account, nav: (String) -> Unit) {
val channelState by baseChannel.live.observeAsState()
val channel = channelState?.channel ?: return
val channel = remember(channelState) { channelState?.channel } ?: return
val context = LocalContext.current.applicationContext
@@ -123,7 +123,7 @@ fun FloatingButtons(navController: NavHostController, accountViewModel: AccountV
}
is AccountState.LoggedIn -> {
if (currentRoute(navController)?.substringBefore("?") == Route.Home.base) {
NewNoteButton(state.account, accountViewModel, nav)
NewNoteButton(accountViewModel, nav)
}
if (currentRoute(navController) == Route.Message.base) {
ChannelFabColumn(state.account, nav)
@@ -325,9 +325,6 @@ private fun RelayBadges(baseNote: Note) {
@Composable
fun ReactionsColumn(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
var wantsToReplyTo by remember {
mutableStateOf<Note?>(null)
}
@@ -337,11 +334,11 @@ fun ReactionsColumn(baseNote: Note, accountViewModel: AccountViewModel, nav: (St
}
if (wantsToReplyTo != null) {
NewPostView({ wantsToReplyTo = null }, wantsToReplyTo, null, account, accountViewModel, nav)
NewPostView({ wantsToReplyTo = null }, wantsToReplyTo, null, accountViewModel, nav)
}
if (wantsToQuote != null) {
NewPostView({ wantsToQuote = null }, null, wantsToQuote, account, accountViewModel, nav)
NewPostView({ wantsToQuote = null }, null, wantsToQuote, accountViewModel, nav)
}
Spacer(modifier = Modifier.height(8.dp))