Compare commits

...
12 Commits
38 changed files with 899 additions and 660 deletions
+3 -3
View File
@@ -13,8 +13,8 @@ android {
applicationId "com.vitorpamplona.amethyst"
minSdk 26
targetSdk 34
versionCode 342
versionName "0.83.1"
versionCode 343
versionName "0.83.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -89,7 +89,7 @@ android {
buildTypes {
release {
//signingConfig signingConfigs.debug
signingConfig signingConfigs.debug
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), 'proguard-rules.pro'
minifyEnabled true
resValue "string", "app_name", "@string/app_name_release"
@@ -1,11 +0,0 @@
package com.vitorpamplona.amethyst
object OptOutFromFilters {
var warnAboutPostsWithReports: Boolean = true
var filterSpamFromStrangers: Boolean = true
fun start(warnAboutReports: Boolean, filterSpam: Boolean) {
warnAboutPostsWithReports = warnAboutReports
filterSpamFromStrangers = filterSpam
}
}
@@ -64,7 +64,7 @@ class ServiceManager {
// Resets Proxy Use
HttpClient.start(account?.proxy)
OptOutFromFilters.start(account?.warnAboutPostsWithReports ?: true, account?.filterSpamFromStrangers ?: true)
LocalCache.antiSpam.active = account?.filterSpamFromStrangers ?: true
Coil.setImageLoader {
Amethyst.instance.imageLoaderBuilder().components {
if (Build.VERSION.SDK_INT >= 28) {
@@ -11,7 +11,6 @@ import androidx.lifecycle.asLiveData
import androidx.lifecycle.liveData
import androidx.lifecycle.switchMap
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.OptOutFromFilters
import com.vitorpamplona.amethyst.service.FileHeader
import com.vitorpamplona.amethyst.service.Nip96MediaServers
import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource
@@ -103,7 +102,7 @@ val DefaultChannels = setOf(
"42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5" // -> Amethyst's Group
)
val DefaultReactions = listOf("\uD83D\uDE80", "\uD83E\uDEC2", "\uD83D\uDC40", "\uD83D\uDE02")
val DefaultReactions = listOf("\uD83D\uDE80", "\uD83E\uDEC2", "\uD83D\uDC40", "\uD83D\uDE02", "\uD83C\uDF89", "\uD83E\uDD14", "\uD83D\uDE31")
val DefaultZapAmounts = listOf(500L, 1000L, 5000L)
@@ -426,7 +425,7 @@ class Account(
fun updateOptOutOptions(warnReports: Boolean, filterSpam: Boolean) {
warnAboutPostsWithReports = warnReports
filterSpamFromStrangers = filterSpam
OptOutFromFilters.start(warnAboutPostsWithReports, filterSpamFromStrangers)
LocalCache.antiSpam.active = filterSpamFromStrangers
if (!filterSpamFromStrangers) {
transientHiddenUsers = persistentSetOf()
}
@@ -4,7 +4,6 @@ import android.util.Log
import android.util.LruCache
import androidx.compose.runtime.Stable
import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.OptOutFromFilters
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
@@ -19,10 +18,12 @@ class AntiSpamFilter {
val recentMessages = LruCache<Int, String>(1000)
val spamMessages = LruCache<Int, Spammer>(1000)
var active: Boolean = true
fun isSpam(event: Event, relay: Relay?): Boolean {
checkNotInMainThread()
if (!OptOutFromFilters.filterSpamFromStrangers) return false
if (!active) return false
val idHex = event.id
@@ -60,7 +60,6 @@ import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.Size18Modifier
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.Size20dp
import com.vitorpamplona.amethyst.ui.theme.SmallishBorder
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
@@ -158,7 +157,7 @@ fun CashuPreview(
Icon(
painter = painterResource(R.drawable.cashu),
null,
modifier = Modifier.size(20.dp),
modifier = Size20Modifier,
tint = Color.Unspecified
)
@@ -206,7 +205,7 @@ fun CashuPreview(
if (isRedeeming) {
LoadingAnimation()
} else {
ZapIcon(Size20dp, tint = Color.White)
ZapIcon(Size20Modifier, tint = Color.White)
}
Spacer(DoubleHorzSpacer)
@@ -320,7 +319,7 @@ fun CashuPreviewNew(
if (isRedeeming) {
LoadingAnimation()
} else {
ZapIcon(Size20dp, tint = MaterialTheme.colorScheme.onBackground)
ZapIcon(Size20Modifier, tint = MaterialTheme.colorScheme.onBackground)
}
Spacer(StdHorzSpacer)
@@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Divider
@@ -36,6 +35,7 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog
import com.vitorpamplona.amethyst.ui.note.payViaIntent
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
import kotlinx.coroutines.Dispatchers
@@ -119,7 +119,7 @@ fun InvoicePreview(lnInvoice: String, amount: String?) {
Icon(
painter = painterResource(R.drawable.lightning),
null,
modifier = Modifier.size(20.dp),
modifier = Size20Modifier,
tint = Color.Unspecified
)
@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
@@ -37,6 +36,7 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import com.vitorpamplona.quartz.events.LnZapEvent
@@ -94,7 +94,7 @@ fun InvoiceRequest(
Icon(
painter = painterResource(R.drawable.lightning),
null,
modifier = Modifier.size(20.dp),
modifier = Size20Modifier,
tint = Color.Unspecified
)
@@ -0,0 +1,93 @@
package com.vitorpamplona.amethyst.ui.elements
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.ThemeComparison
@Composable
@Preview
fun AddButtonPreview() {
ThemeComparison(
onDark = {
Row() {
Column {
AddButton(isActive = true) {}
AddButton(isActive = false) {}
}
Column {
RemoveButton(isActive = true) {}
RemoveButton(isActive = false) {}
}
}
},
onLight = {
Row() {
Column {
AddButton(isActive = true) {}
AddButton(isActive = false) {}
}
Column {
RemoveButton(isActive = true) {}
RemoveButton(isActive = false) {}
}
}
}
)
}
@Composable
fun AddButton(
text: Int = R.string.add,
isActive: Boolean = true,
modifier: Modifier = Modifier.padding(start = 3.dp),
onClick: () -> Unit
) {
Button(
modifier = modifier,
onClick = {
if (isActive) {
onClick()
}
},
shape = ButtonBorder,
enabled = isActive,
contentPadding = PaddingValues(vertical = 0.dp, horizontal = 16.dp)
) {
Text(text = stringResource(text), color = Color.White, textAlign = TextAlign.Center)
}
}
@Composable
fun RemoveButton(
isActive: Boolean = true,
onClick: () -> Unit
) {
Button(
modifier = Modifier.padding(start = 3.dp),
onClick = {
if (isActive) {
onClick()
}
},
shape = ButtonBorder,
enabled = isActive,
contentPadding = PaddingValues(vertical = 0.dp, horizontal = 16.dp)
) {
Text(text = stringResource(R.string.remove), color = Color.White)
}
}
@@ -0,0 +1,60 @@
package com.vitorpamplona.amethyst.ui.elements
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.text.AnnotatedString
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
@Composable
fun DisplayFollowingCommunityInPost(
baseNote: Note,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
Column(HalfStartPadding) {
Row(verticalAlignment = Alignment.CenterVertically) {
DisplayCommunity(baseNote, nav)
}
}
}
@Composable
private fun DisplayCommunity(note: Note, nav: (String) -> Unit) {
val communityTag = remember(note) {
note.event?.getTagOfAddressableKind(CommunityDefinitionEvent.kind)
} ?: return
val displayTag = remember(note) { AnnotatedString(getCommunityShortName(communityTag)) }
val route = remember(note) { "Community/${communityTag.toTag()}" }
ClickableText(
text = displayTag,
onClick = { nav(route) },
style = LocalTextStyle.current.copy(
color = MaterialTheme.colorScheme.primary.copy(
alpha = 0.52f
)
),
maxLines = 1
)
}
private fun getCommunityShortName(communityTag: ATag): String {
val name = if (communityTag.dTag.length > 10) {
communityTag.dTag.take(10) + "..."
} else {
communityTag.dTag.take(10)
}
return "/n/$name"
}
@@ -0,0 +1,71 @@
package com.vitorpamplona.amethyst.ui.elements
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.text.AnnotatedString
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
fun DisplayFollowingHashtagsInPost(
baseNote: Note,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val noteEvent = remember { baseNote.event } ?: return
val userFollowState by accountViewModel.userFollows.observeAsState()
var firstTag by remember { mutableStateOf<String?>(null) }
LaunchedEffect(key1 = userFollowState) {
launch(Dispatchers.Default) {
val followingTags = userFollowState?.user?.cachedFollowingTagSet() ?: emptySet()
val newFirstTag = noteEvent.firstIsTaggedHashes(followingTags)
if (firstTag != newFirstTag) {
launch(Dispatchers.Main) {
firstTag = newFirstTag
}
}
}
}
firstTag?.let {
Column(verticalArrangement = Arrangement.Center) {
Row(verticalAlignment = Alignment.CenterVertically) {
DisplayTagList(it, nav)
}
}
}
}
@Composable
private fun DisplayTagList(firstTag: String, nav: (String) -> Unit) {
val displayTag = remember(firstTag) { AnnotatedString(" #$firstTag") }
val route = remember(firstTag) { "Hashtag/$firstTag" }
ClickableText(
text = displayTag,
onClick = { nav(route) },
style = LocalTextStyle.current.copy(
color = MaterialTheme.colorScheme.primary.copy(
alpha = 0.52f
)
),
maxLines = 1
)
}
@@ -0,0 +1,41 @@
package com.vitorpamplona.amethyst.ui.elements
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import com.vitorpamplona.amethyst.ui.theme.Font14SP
import com.vitorpamplona.amethyst.ui.theme.ThemeComparison
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
@Composable
@Preview
fun DisplayPoWPreview() {
ThemeComparison(
onDark = {
DisplayPoW(pow = 24)
},
onLight = {
DisplayPoW(pow = 24)
}
)
}
@Composable
fun DisplayPoW(
pow: Int
) {
val powStr = remember(pow) {
"PoW-$pow"
}
Text(
powStr,
color = MaterialTheme.colorScheme.lessImportantLink,
fontSize = Font14SP,
fontWeight = FontWeight.Bold,
maxLines = 1
)
}
@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.note
package com.vitorpamplona.amethyst.ui.elements
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
@@ -9,19 +10,26 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
@@ -35,10 +43,102 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.actions.PostButton
import com.vitorpamplona.amethyst.ui.note.ZapIcon
import com.vitorpamplona.amethyst.ui.note.ZappedIcon
import com.vitorpamplona.amethyst.ui.note.showAmount
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.math.BigDecimal
@Stable
data class Reward(val amount: BigDecimal)
@Composable
fun DisplayReward(
baseReward: Reward,
baseNote: Note,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
var popupExpanded by remember { mutableStateOf(false) }
Column() {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.clickable { popupExpanded = true }
) {
ClickableText(
text = AnnotatedString("#bounty"),
onClick = { nav("Hashtag/bounty") },
style = LocalTextStyle.current.copy(
color = MaterialTheme.colorScheme.primary.copy(
alpha = 0.52f
)
)
)
RenderPledgeAmount(baseNote, baseReward, accountViewModel)
}
if (popupExpanded) {
AddBountyAmountDialog(baseNote, accountViewModel) {
popupExpanded = false
}
}
}
}
@Composable
private fun RenderPledgeAmount(
baseNote: Note,
baseReward: Reward,
accountViewModel: AccountViewModel
) {
val repliesState by baseNote.live().replies.observeAsState()
var reward by remember {
mutableStateOf<String>(
showAmount(baseReward.amount)
)
}
var hasPledge by remember {
mutableStateOf<Boolean>(
false
)
}
LaunchedEffect(key1 = repliesState) {
launch(Dispatchers.IO) {
repliesState?.note?.pledgedAmountByOthers()?.let {
val newRewardAmount = showAmount(baseReward.amount.add(it))
if (newRewardAmount != reward) {
reward = newRewardAmount
}
}
val newHasPledge = repliesState?.note?.hasPledgeBy(accountViewModel.userProfile()) == true
if (hasPledge != newHasPledge) {
launch(Dispatchers.Main) {
hasPledge = newHasPledge
}
}
}
}
if (hasPledge) {
ZappedIcon(modifier = Size20Modifier)
} else {
ZapIcon(modifier = Size20Modifier, MaterialTheme.colorScheme.placeholderText)
}
Text(
text = reward,
color = MaterialTheme.colorScheme.placeholderText,
maxLines = 1
)
}
class AddBountyAmountViewModel : ViewModel() {
private var account: Account? = null
@@ -0,0 +1,41 @@
package com.vitorpamplona.amethyst.ui.elements
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.text.AnnotatedString
import com.vitorpamplona.amethyst.ui.theme.HalfTopPadding
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
import kotlinx.collections.immutable.ImmutableList
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun DisplayUncitedHashtags(
hashtags: ImmutableList<String>,
eventContent: String,
nav: (String) -> Unit
) {
val unusedHashtags = remember(eventContent) {
hashtags.filter { !eventContent.contains(it, true) }
}
if (unusedHashtags.isNotEmpty()) {
FlowRow(
modifier = HalfTopPadding
) {
unusedHashtags.forEach { hashtag ->
ClickableText(
text = remember { AnnotatedString("#$hashtag ") },
onClick = { nav("Hashtag/$hashtag") },
style = LocalTextStyle.current.copy(
color = MaterialTheme.colorScheme.lessImportantLink
)
)
}
}
}
}
@@ -0,0 +1,84 @@
package com.vitorpamplona.amethyst.ui.elements
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowForwardIos
import androidx.compose.material.icons.outlined.Bolt
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.note.UserPicture
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.Size25dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.quartz.events.EventInterface
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun DisplayZapSplits(noteEvent: EventInterface, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val list = remember(noteEvent) { noteEvent.zapSplitSetup() }
if (list.isEmpty()) return
Row(verticalAlignment = Alignment.CenterVertically) {
Box(
Modifier
.height(20.dp)
.width(25.dp)
) {
Icon(
imageVector = Icons.Outlined.Bolt,
contentDescription = stringResource(id = R.string.zaps),
modifier = Modifier
.size(20.dp)
.align(Alignment.CenterStart),
tint = BitcoinOrange
)
Icon(
imageVector = Icons.Outlined.ArrowForwardIos,
contentDescription = stringResource(id = R.string.zaps),
modifier = Modifier
.size(13.dp)
.align(Alignment.CenterEnd),
tint = BitcoinOrange
)
}
Spacer(modifier = StdHorzSpacer)
FlowRow {
list.forEach {
if (it.isLnAddress) {
ClickableText(
text = AnnotatedString(it.lnAddressOrPubKeyHex),
onClick = { },
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary)
)
} else {
UserPicture(
userHex = it.lnAddressOrPubKeyHex,
size = Size25dp,
accountViewModel = accountViewModel,
nav = nav
)
}
}
}
}
}
@@ -0,0 +1,58 @@
package com.vitorpamplona.amethyst.ui.layouts
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.ui.note.RepostedIcon
import com.vitorpamplona.amethyst.ui.theme.Size18Modifier
import com.vitorpamplona.amethyst.ui.theme.Size35Modifier
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
import com.vitorpamplona.amethyst.ui.theme.placeholderText
@Composable
@Preview
private fun GenericRepostSectionPreview() {
GenericRepostLayout(
baseAuthorPicture = {
Text("ab")
},
repostAuthorPicture = {
Text("cd")
}
)
}
@Composable
fun GenericRepostLayout(
baseAuthorPicture: @Composable () -> Unit,
repostAuthorPicture: @Composable () -> Unit
) {
Box(modifier = Size55Modifier) {
Box(remember { Size35Modifier.align(Alignment.TopStart) }) {
baseAuthorPicture()
}
Box(
remember {
Size18Modifier
.align(Alignment.BottomStart)
.padding(1.dp)
}
) {
RepostedIcon(modifier = Size18Modifier, MaterialTheme.colorScheme.placeholderText)
}
Box(
remember { Size35Modifier.align(Alignment.BottomEnd) },
contentAlignment = Alignment.BottomEnd
) {
repostAuthorPicture()
}
}
}
@@ -112,6 +112,7 @@ import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer
import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.Size22Modifier
import com.vitorpamplona.amethyst.ui.theme.Size34dp
import com.vitorpamplona.amethyst.ui.theme.Size40dp
@@ -725,12 +726,12 @@ fun SimpleTextSpinner(
contentAlignment = Alignment.Center
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Spacer(modifier = Modifier.size(20.dp))
Spacer(modifier = Size20Modifier)
Text(currentText)
Icon(
imageVector = Icons.Default.ExpandMore,
null,
modifier = Modifier.size(20.dp),
modifier = Size20Modifier,
tint = MaterialTheme.colorScheme.placeholderText
)
}
@@ -0,0 +1,67 @@
package com.vitorpamplona.amethyst.ui.navigation
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
import com.vitorpamplona.quartz.events.ChatroomKey
import com.vitorpamplona.quartz.events.ChatroomKeyable
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
import kotlinx.collections.immutable.persistentSetOf
import java.net.URLEncoder
fun routeFor(note: Note, loggedIn: User): String? {
val noteEvent = note.event
if (noteEvent is ChannelMessageEvent || noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) {
note.channelHex()?.let {
return "Channel/$it"
}
} else if (noteEvent is LiveActivitiesEvent || noteEvent is LiveActivitiesChatMessageEvent) {
note.channelHex()?.let {
return "Channel/${URLEncoder.encode(it, "utf-8")}"
}
} else if (noteEvent is ChatroomKeyable) {
val room = noteEvent.chatroomKey(loggedIn.pubkeyHex)
loggedIn.createChatroom(room)
return "Room/${room.hashCode()}"
} else if (noteEvent is CommunityDefinitionEvent) {
return "Community/${URLEncoder.encode(note.idHex, "utf-8")}"
} else {
return "Note/${URLEncoder.encode(note.idHex, "utf-8")}"
}
return null
}
fun routeToMessage(user: HexKey, draftMessage: String?, accountViewModel: AccountViewModel): String {
val withKey = ChatroomKey(persistentSetOf(user))
accountViewModel.account.userProfile().createChatroom(withKey)
return if (draftMessage != null) {
"Room/${withKey.hashCode()}?message=$draftMessage"
} else {
"Room/${withKey.hashCode()}"
}
}
fun routeToMessage(user: User, draftMessage: String?, accountViewModel: AccountViewModel): String {
return routeToMessage(user.pubkeyHex, draftMessage, accountViewModel)
}
fun routeFor(note: Channel): String {
return "Channel/${note.idHex}"
}
fun routeFor(user: User): String {
return "User/${user.pubkeyHex}"
}
fun authorRouteFor(note: Note): String {
return "User/${note.author?.pubkeyHex}"
}
@@ -33,6 +33,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.navigation.routeFor
import com.vitorpamplona.amethyst.ui.screen.BadgeCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
@@ -59,7 +59,7 @@ import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.Font12SP
import com.vitorpamplona.amethyst.ui.theme.ReactionRowHeightChat
import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size15dp
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
import com.vitorpamplona.amethyst.ui.theme.Size20dp
import com.vitorpamplona.amethyst.ui.theme.Size25dp
import com.vitorpamplona.amethyst.ui.theme.Size5dp
@@ -450,7 +450,7 @@ private fun MessageBubbleLines(
grayTint = MaterialTheme.colorScheme.placeholderText,
accountViewModel = accountViewModel,
showCounter = false,
iconSize = Size15dp
iconSizeModifier = Size15Modifier
) {
onWantsToReply(baseNote)
}
@@ -123,11 +123,11 @@ fun LikedIcon(modifier: Modifier) {
}
@Composable
fun LikeIcon(iconSize: Dp, grayTint: Color) {
fun LikeIcon(iconSizeModifier: Modifier, grayTint: Color) {
Icon(
painter = painterResource(R.drawable.ic_like),
null,
modifier = remember(iconSize) { Modifier.size(iconSize) },
modifier = iconSizeModifier,
tint = grayTint
)
}
@@ -162,11 +162,6 @@ fun ZappedIcon(modifier: Modifier) {
ZapIcon(modifier = modifier, BitcoinOrange)
}
@Composable
fun ZapIcon(iconSize: Dp, tint: Color = Color.Unspecified) {
ZapIcon(modifier = remember(iconSize) { Modifier.size(iconSize) }, tint)
}
@Composable
fun ZapIcon(modifier: Modifier, tint: Color = Color.Unspecified) {
Icon(
@@ -228,11 +223,11 @@ fun ExpandMoreIcon(modifier: Modifier) {
}
@Composable
fun CommentIcon(iconSize: Dp, tint: Color) {
fun CommentIcon(iconSizeModifier: Modifier, tint: Color) {
Icon(
painter = painterResource(R.drawable.ic_comment),
contentDescription = null,
modifier = remember { Modifier.size(iconSize) },
modifier = iconSizeModifier,
tint = tint
)
}
@@ -22,6 +22,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.ui.navigation.routeFor
import com.vitorpamplona.amethyst.ui.screen.MessageSetCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
@@ -49,6 +49,8 @@ import com.vitorpamplona.amethyst.ui.components.ImageUrlType
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.navigation.authorRouteFor
import com.vitorpamplona.amethyst.ui.navigation.routeFor
import com.vitorpamplona.amethyst.ui.screen.CombinedZap
import com.vitorpamplona.amethyst.ui.screen.MultiSetCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -34,12 +34,12 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.navigation.routeFor
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.ui.note.LoadStatuses
import com.vitorpamplona.amethyst.ui.note.NIP05CheckingIcon
import com.vitorpamplona.amethyst.ui.note.NIP05FailedVerification
import com.vitorpamplona.amethyst.ui.note.NIP05VerifiedIcon
import com.vitorpamplona.amethyst.ui.note.routeFor
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.Font14SP
import com.vitorpamplona.amethyst.ui.theme.NIP05IconSize
@@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -28,13 +27,7 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.CutCornerShape
import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowForwardIos
import androidx.compose.material.icons.outlined.Bolt
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Divider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
@@ -44,7 +37,6 @@ import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
@@ -53,7 +45,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.BottomEnd
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -71,7 +62,6 @@ import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.graphics.drawable.toBitmap
@@ -114,6 +104,17 @@ import com.vitorpamplona.amethyst.ui.components.figureOutMimeType
import com.vitorpamplona.amethyst.ui.components.imageExtensions
import com.vitorpamplona.amethyst.ui.components.measureSpaceWidth
import com.vitorpamplona.amethyst.ui.components.removeQueryParamsForExtensionComparison
import com.vitorpamplona.amethyst.ui.elements.AddButton
import com.vitorpamplona.amethyst.ui.elements.DisplayFollowingCommunityInPost
import com.vitorpamplona.amethyst.ui.elements.DisplayFollowingHashtagsInPost
import com.vitorpamplona.amethyst.ui.elements.DisplayPoW
import com.vitorpamplona.amethyst.ui.elements.DisplayReward
import com.vitorpamplona.amethyst.ui.elements.DisplayUncitedHashtags
import com.vitorpamplona.amethyst.ui.elements.DisplayZapSplits
import com.vitorpamplona.amethyst.ui.elements.RemoveButton
import com.vitorpamplona.amethyst.ui.elements.Reward
import com.vitorpamplona.amethyst.ui.layouts.GenericRepostLayout
import com.vitorpamplona.amethyst.ui.navigation.routeFor
import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
@@ -124,8 +125,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.LeaveCommunityButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LiveFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.NormalTimeAgo
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
@@ -133,14 +132,11 @@ import com.vitorpamplona.amethyst.ui.theme.Font14SP
import com.vitorpamplona.amethyst.ui.theme.HalfDoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.HalfPadding
import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
import com.vitorpamplona.amethyst.ui.theme.HalfTopPadding
import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
import com.vitorpamplona.amethyst.ui.theme.Size16Modifier
import com.vitorpamplona.amethyst.ui.theme.Size18Modifier
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.Size24Modifier
import com.vitorpamplona.amethyst.ui.theme.Size25dp
import com.vitorpamplona.amethyst.ui.theme.Size30Modifier
@@ -157,17 +153,18 @@ import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.UserNameMaxRowHeight
import com.vitorpamplona.amethyst.ui.theme.UserNameRowHeight
import com.vitorpamplona.amethyst.ui.theme.WidthAuthorPictureModifier
import com.vitorpamplona.amethyst.ui.theme.boostedNoteModifier
import com.vitorpamplona.amethyst.ui.theme.channelNotePictureModifier
import com.vitorpamplona.amethyst.ui.theme.grayText
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
import com.vitorpamplona.amethyst.ui.theme.normalNoteModifier
import com.vitorpamplona.amethyst.ui.theme.normalWithTopMarginNoteModifier
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.replyBackground
import com.vitorpamplona.amethyst.ui.theme.replyModifier
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.toNpub
import com.vitorpamplona.quartz.events.AppDefinitionEvent
import com.vitorpamplona.quartz.events.AudioHeaderEvent
@@ -178,8 +175,6 @@ import com.vitorpamplona.quartz.events.BaseTextNoteEvent
import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
import com.vitorpamplona.quartz.events.ChatroomKey
import com.vitorpamplona.quartz.events.ChatroomKeyable
import com.vitorpamplona.quartz.events.ClassifiedsEvent
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
@@ -187,7 +182,6 @@ import com.vitorpamplona.quartz.events.EmojiPackEvent
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
import com.vitorpamplona.quartz.events.EmojiUrl
import com.vitorpamplona.quartz.events.EmptyTagList
import com.vitorpamplona.quartz.events.EventInterface
import com.vitorpamplona.quartz.events.FileHeaderEvent
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
import com.vitorpamplona.quartz.events.GenericRepostEvent
@@ -212,14 +206,11 @@ import com.vitorpamplona.quartz.events.UserMetadata
import com.vitorpamplona.quartz.events.toImmutableListOfLists
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
import java.math.BigDecimal
import java.net.URL
import java.net.URLEncoder
import java.util.Locale
@OptIn(ExperimentalFoundationApi::class)
@@ -289,7 +280,7 @@ fun CheckHiddenNoteCompose(
) {
if (showHidden) {
// Ignores reports as well
val state by remember {
val state by remember(note) {
mutableStateOf(
AccountViewModel.NoteComposeReportState()
)
@@ -350,7 +341,7 @@ fun LoadedNoteCompose(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
var state by remember {
var state by remember(note) {
mutableStateOf(
AccountViewModel.NoteComposeReportState()
)
@@ -395,7 +386,7 @@ fun RenderReportState(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
var showReportedNote by remember { mutableStateOf(false) }
var showReportedNote by remember(note) { mutableStateOf(false) }
Crossfade(targetState = !state.isAcceptable && !showReportedNote, label = "RenderReportState") { showHiddenNote ->
if (showHiddenNote) {
@@ -953,15 +944,12 @@ fun InnerNoteWithReactions(
val notBoostedNorQuote = !isBoostedNote && !isQuotedNote
Row(
modifier = remember {
Modifier
.fillMaxWidth()
.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
// Don't add margin to the bottom because of the Divider down below
)
modifier = if (!isBoostedNote && addMarginTop) {
normalWithTopMarginNoteModifier
} else if (!isBoostedNote) {
normalNoteModifier
} else {
boostedNoteModifier
}
) {
if (notBoostedNorQuote) {
@@ -1070,60 +1058,6 @@ private fun NoteBody(
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun DisplayZapSplits(noteEvent: EventInterface, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val list = remember(noteEvent) { noteEvent.zapSplitSetup() }
if (list.isEmpty()) return
Row(verticalAlignment = CenterVertically) {
Box(
Modifier
.height(20.dp)
.width(25.dp)
) {
Icon(
imageVector = Icons.Outlined.Bolt,
contentDescription = stringResource(id = R.string.zaps),
modifier = Modifier
.size(20.dp)
.align(Alignment.CenterStart),
tint = BitcoinOrange
)
Icon(
imageVector = Icons.Outlined.ArrowForwardIos,
contentDescription = stringResource(id = R.string.zaps),
modifier = Modifier
.size(13.dp)
.align(Alignment.CenterEnd),
tint = BitcoinOrange
)
}
Spacer(modifier = StdHorzSpacer)
FlowRow {
list.forEach {
if (it.isLnAddress) {
ClickableText(
text = AnnotatedString(it.lnAddressOrPubKeyHex),
onClick = { },
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary)
)
} else {
UserPicture(
userHex = it.lnAddressOrPubKeyHex,
size = Size25dp,
accountViewModel = accountViewModel,
nav = nav
)
}
}
}
}
}
@Composable
private fun RenderNoteRow(
baseNote: Note,
@@ -1265,56 +1199,6 @@ private fun RenderNoteRow(
}
}
fun routeFor(note: Note, loggedIn: User): String? {
val noteEvent = note.event
if (noteEvent is ChannelMessageEvent || noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) {
note.channelHex()?.let {
return "Channel/$it"
}
} else if (noteEvent is LiveActivitiesEvent || noteEvent is LiveActivitiesChatMessageEvent) {
note.channelHex()?.let {
return "Channel/${URLEncoder.encode(it, "utf-8")}"
}
} else if (noteEvent is ChatroomKeyable) {
val room = noteEvent.chatroomKey(loggedIn.pubkeyHex)
loggedIn.createChatroom(room)
return "Room/${room.hashCode()}"
} else if (noteEvent is CommunityDefinitionEvent) {
return "Community/${URLEncoder.encode(note.idHex, "utf-8")}"
} else {
return "Note/${URLEncoder.encode(note.idHex, "utf-8")}"
}
return null
}
fun routeToMessage(user: HexKey, draftMessage: String?, accountViewModel: AccountViewModel): String {
val withKey = ChatroomKey(persistentSetOf(user))
accountViewModel.account.userProfile().createChatroom(withKey)
return if (draftMessage != null) {
"Room/${withKey.hashCode()}?message=$draftMessage"
} else {
"Room/${withKey.hashCode()}"
}
}
fun routeToMessage(user: User, draftMessage: String?, accountViewModel: AccountViewModel): String {
return routeToMessage(user.pubkeyHex, draftMessage, accountViewModel)
}
fun routeFor(note: Channel): String {
return "Channel/${note.idHex}"
}
fun routeFor(user: User): String {
return "User/${user.pubkeyHex}"
}
fun authorRouteFor(note: Note): String {
return "User/${note.author?.pubkeyHex}"
}
@Composable
fun LoadDecryptedContent(
note: Note,
@@ -2289,45 +2173,6 @@ private fun EmojiListOptions(
}
}
@Composable
fun RemoveButton(onClick: () -> Unit) {
Button(
modifier = Modifier.padding(start = 3.dp),
onClick = onClick,
shape = ButtonBorder,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary
),
contentPadding = PaddingValues(vertical = 0.dp, horizontal = 16.dp)
) {
Text(text = stringResource(R.string.remove), color = Color.White)
}
}
@Composable
fun AddButton(
text: Int = R.string.add,
isActive: Boolean = true,
modifier: Modifier = Modifier.padding(start = 3.dp),
onClick: () -> Unit
) {
Button(
modifier = modifier,
onClick = {
if (isActive) {
onClick()
}
},
shape = ButtonBorder,
colors = ButtonDefaults.buttonColors(
containerColor = if (isActive) MaterialTheme.colorScheme.primary else Color.Gray
),
contentPadding = PaddingValues(vertical = 0.dp, horizontal = 16.dp)
) {
Text(text = stringResource(text), color = Color.White, textAlign = TextAlign.Center)
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun RenderPinListEvent(
@@ -2800,19 +2645,8 @@ private fun BadgeBox(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val isRepost by remember(baseNote) {
derivedStateOf {
baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent
}
}
if (isRepost) {
val baseReply by remember(baseNote) {
derivedStateOf {
baseNote.replyTo?.lastOrNull()
}
}
baseReply?.let {
if (baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent) {
baseNote.replyTo?.lastOrNull()?.let {
RelayBadges(it, accountViewModel, nav)
}
} else {
@@ -2826,31 +2660,24 @@ private fun RenderAuthorImages(
nav: (String) -> Unit,
accountViewModel: AccountViewModel
) {
val baseRepost by remember {
derivedStateOf {
baseNote.replyTo?.lastOrNull()
}
}
val isRepost = baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent
if (isRepost && baseRepost != null) {
RepostNoteAuthorPicture(baseNote, baseRepost!!, accountViewModel, nav)
if (isRepost) {
val baseRepost = baseNote.replyTo?.lastOrNull()
if (baseRepost != null) {
RepostNoteAuthorPicture(baseNote, baseRepost, accountViewModel, nav)
} else {
NoteAuthorPicture(baseNote, nav, accountViewModel, Size55dp)
}
} else {
NoteAuthorPicture(baseNote, nav, accountViewModel, Size55dp)
}
val isChannel = baseNote.event is ChannelMessageEvent && baseNote.channelHex() != null
val automaticallyShowProfilePicture = remember {
accountViewModel.settings.showProfilePictures.value
}
if (isChannel) {
val baseChannelHex = remember { baseNote.channelHex() }
if (baseNote.event is ChannelMessageEvent) {
val baseChannelHex = remember(baseNote) { baseNote.channelHex() }
if (baseChannelHex != null) {
LoadChannel(baseChannelHex, accountViewModel) { channel ->
ChannelNotePicture(channel, loadProfilePicture = automaticallyShowProfilePicture)
ChannelNotePicture(channel, loadProfilePicture = accountViewModel.settings.showProfilePictures.value)
}
}
}
@@ -2901,7 +2728,7 @@ private fun RepostNoteAuthorPicture(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
GenericRepostSection(
GenericRepostLayout(
baseAuthorPicture = {
NoteAuthorPicture(
baseNote = baseNote,
@@ -2921,45 +2748,6 @@ private fun RepostNoteAuthorPicture(
)
}
@Composable
@Preview
private fun GenericRepostSectionPreview() {
GenericRepostSection(
baseAuthorPicture = {
Text("ab")
},
repostAuthorPicture = {
Text("cd")
}
)
}
@Composable
private fun GenericRepostSection(
baseAuthorPicture: @Composable () -> Unit,
repostAuthorPicture: @Composable () -> Unit
) {
Box(modifier = Size55Modifier) {
Box(remember { Size35Modifier.align(Alignment.TopStart) }) {
baseAuthorPicture()
}
Box(
remember {
Size18Modifier
.align(Alignment.BottomStart)
.padding(1.dp)
}
) {
RepostedIcon(modifier = Size18Modifier, MaterialTheme.colorScheme.placeholderText)
}
Box(remember { Size35Modifier.align(Alignment.BottomEnd) }, contentAlignment = BottomEnd) {
repostAuthorPicture()
}
}
}
@Composable
fun DisplayHighlight(
highlight: String,
@@ -3095,232 +2883,6 @@ private fun LoadAndDisplayUser(
}
}
@Composable
fun DisplayFollowingCommunityInPost(
baseNote: Note,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
Column(HalfStartPadding) {
Row(verticalAlignment = CenterVertically) {
DisplayCommunity(baseNote, nav)
}
}
}
@Composable
fun DisplayFollowingHashtagsInPost(
baseNote: Note,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val noteEvent = remember { baseNote.event } ?: return
val userFollowState by accountViewModel.userFollows.observeAsState()
var firstTag by remember { mutableStateOf<String?>(null) }
LaunchedEffect(key1 = userFollowState) {
launch(Dispatchers.Default) {
val followingTags = userFollowState?.user?.cachedFollowingTagSet() ?: emptySet()
val newFirstTag = noteEvent.firstIsTaggedHashes(followingTags)
if (firstTag != newFirstTag) {
launch(Dispatchers.Main) {
firstTag = newFirstTag
}
}
}
}
firstTag?.let {
Column(verticalArrangement = Arrangement.Center) {
Row(verticalAlignment = CenterVertically) {
DisplayTagList(it, nav)
}
}
}
}
@Composable
private fun DisplayTagList(firstTag: String, nav: (String) -> Unit) {
val displayTag = remember(firstTag) { AnnotatedString(" #$firstTag") }
val route = remember(firstTag) { "Hashtag/$firstTag" }
ClickableText(
text = displayTag,
onClick = { nav(route) },
style = LocalTextStyle.current.copy(
color = MaterialTheme.colorScheme.primary.copy(
alpha = 0.52f
)
),
maxLines = 1
)
}
@Composable
private fun DisplayCommunity(note: Note, nav: (String) -> Unit) {
val communityTag = remember(note) {
note.event?.getTagOfAddressableKind(CommunityDefinitionEvent.kind)
} ?: return
val displayTag = remember(note) { AnnotatedString(getCommunityShortName(communityTag)) }
val route = remember(note) { "Community/${communityTag.toTag()}" }
ClickableText(
text = displayTag,
onClick = { nav(route) },
style = LocalTextStyle.current.copy(
color = MaterialTheme.colorScheme.primary.copy(
alpha = 0.52f
)
),
maxLines = 1
)
}
private fun getCommunityShortName(communityTag: ATag): String {
val name = if (communityTag.dTag.length > 10) {
communityTag.dTag.take(10) + "..."
} else {
communityTag.dTag.take(10)
}
return "/n/$name"
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun DisplayUncitedHashtags(
hashtags: ImmutableList<String>,
eventContent: String,
nav: (String) -> Unit
) {
val unusedHashtags = remember(eventContent) {
hashtags.filter { !eventContent.contains(it, true) }
}
if (unusedHashtags.isNotEmpty()) {
FlowRow(
modifier = HalfTopPadding
) {
unusedHashtags.forEach { hashtag ->
ClickableText(
text = remember { AnnotatedString("#$hashtag ") },
onClick = { nav("Hashtag/$hashtag") },
style = LocalTextStyle.current.copy(
color = MaterialTheme.colorScheme.lessImportantLink
)
)
}
}
}
}
@Composable
fun DisplayPoW(
pow: Int
) {
val powStr = remember(pow) {
"PoW-$pow"
}
Text(
powStr,
color = MaterialTheme.colorScheme.lessImportantLink,
fontSize = Font14SP,
fontWeight = FontWeight.Bold,
maxLines = 1
)
}
@Stable
data class Reward(val amount: BigDecimal)
@Composable
fun DisplayReward(
baseReward: Reward,
baseNote: Note,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
var popupExpanded by remember { mutableStateOf(false) }
Column() {
Row(
verticalAlignment = CenterVertically,
modifier = Modifier.clickable { popupExpanded = true }
) {
ClickableText(
text = AnnotatedString("#bounty"),
onClick = { nav("Hashtag/bounty") },
style = LocalTextStyle.current.copy(
color = MaterialTheme.colorScheme.primary.copy(
alpha = 0.52f
)
)
)
RenderPledgeAmount(baseNote, baseReward, accountViewModel)
}
if (popupExpanded) {
AddBountyAmountDialog(baseNote, accountViewModel) {
popupExpanded = false
}
}
}
}
@Composable
private fun RenderPledgeAmount(
baseNote: Note,
baseReward: Reward,
accountViewModel: AccountViewModel
) {
val repliesState by baseNote.live().replies.observeAsState()
var reward by remember {
mutableStateOf<String>(
showAmount(baseReward.amount)
)
}
var hasPledge by remember {
mutableStateOf<Boolean>(
false
)
}
LaunchedEffect(key1 = repliesState) {
launch(Dispatchers.IO) {
repliesState?.note?.pledgedAmountByOthers()?.let {
val newRewardAmount = showAmount(baseReward.amount.add(it))
if (newRewardAmount != reward) {
reward = newRewardAmount
}
}
val newHasPledge = repliesState?.note?.hasPledgeBy(accountViewModel.userProfile()) == true
if (hasPledge != newHasPledge) {
launch(Dispatchers.Main) {
hasPledge = newHasPledge
}
}
}
}
if (hasPledge) {
ZappedIcon(modifier = Size20Modifier)
} else {
ZapIcon(modifier = Size20Modifier, MaterialTheme.colorScheme.placeholderText)
}
Text(
text = reward,
color = MaterialTheme.colorScheme.placeholderText,
maxLines = 1
)
}
@Composable
fun BadgeDisplay(baseNote: Note) {
val background = MaterialTheme.colorScheme.background
@@ -54,6 +54,7 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.StringToastMsg
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
@@ -70,7 +71,6 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.*
import kotlin.math.roundToInt
@Composable
@@ -13,7 +13,6 @@ import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.animation.with
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -83,20 +82,23 @@ import com.vitorpamplona.amethyst.ui.actions.NewPostView
import com.vitorpamplona.amethyst.ui.components.ImageUrlType
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
import com.vitorpamplona.amethyst.ui.components.TextType
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.DarkerGreen
import com.vitorpamplona.amethyst.ui.theme.Font14SP
import com.vitorpamplona.amethyst.ui.theme.HalfDoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
import com.vitorpamplona.amethyst.ui.theme.Height24dpModifier
import com.vitorpamplona.amethyst.ui.theme.ModifierWidth3dp
import com.vitorpamplona.amethyst.ui.theme.NoSoTinyBorders
import com.vitorpamplona.amethyst.ui.theme.ReactionRowExpandButton
import com.vitorpamplona.amethyst.ui.theme.ReactionRowHeight
import com.vitorpamplona.amethyst.ui.theme.ReactionRowZapraiserSize
import com.vitorpamplona.amethyst.ui.theme.Size0dp
import com.vitorpamplona.amethyst.ui.theme.Size17dp
import com.vitorpamplona.amethyst.ui.theme.Size19dp
import com.vitorpamplona.amethyst.ui.theme.Size16Modifier
import com.vitorpamplona.amethyst.ui.theme.Size17Modifier
import com.vitorpamplona.amethyst.ui.theme.Size19Modifier
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.Size20dp
import com.vitorpamplona.amethyst.ui.theme.Size22Modifier
@@ -126,7 +128,7 @@ fun ReactionsRow(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val wantsToSeeReactions = remember {
val wantsToSeeReactions = remember(baseNote) {
mutableStateOf(false)
}
@@ -532,13 +534,11 @@ fun ReplyReaction(
grayTint: Color,
accountViewModel: AccountViewModel,
showCounter: Boolean = true,
iconSize: Dp = Size17dp,
iconSizeModifier: Modifier = Size17Modifier,
onPress: () -> Unit
) {
IconButton(
modifier = remember {
Modifier.size(iconSize)
},
modifier = iconSizeModifier,
onClick = {
if (accountViewModel.isWriteable()) {
onPress()
@@ -550,7 +550,7 @@ fun ReplyReaction(
}
}
) {
CommentIcon(iconSize, grayTint)
CommentIcon(iconSizeModifier, grayTint)
}
if (showCounter) {
@@ -596,7 +596,7 @@ val slideAnimation: ContentTransform =
@Composable
private fun TextCount(count: Int, textColor: Color) {
Text(
text = remember(count) { showCount(count) },
text = showCount(count),
fontSize = Font14SP,
color = textColor,
modifier = HalfStartPadding,
@@ -625,17 +625,14 @@ fun BoostReaction(
baseNote: Note,
grayTint: Color,
accountViewModel: AccountViewModel,
iconSize: Dp = 20.dp,
iconSizeModifier: Modifier = Size20Modifier,
iconSize: Dp = Size20dp,
onQuotePress: () -> Unit
) {
var wantsToBoost by remember { mutableStateOf(false) }
val iconButtonModifier = remember {
Modifier.size(iconSize)
}
IconButton(
modifier = iconButtonModifier,
modifier = iconSizeModifier,
onClick = {
accountViewModel.tryBoost(baseNote) {
wantsToBoost = true
@@ -643,7 +640,7 @@ fun BoostReaction(
}
) {
ObserveBoostIcon(baseNote, accountViewModel) { hasBoosted ->
RepostedIcon(iconButtonModifier, if (hasBoosted) Color.Unspecified else grayTint)
RepostedIcon(iconSizeModifier, if (hasBoosted) Color.Unspecified else grayTint)
}
if (wantsToBoost) {
@@ -695,36 +692,27 @@ fun LikeReaction(
grayTint: Color,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
iconSize: Dp = 20.dp,
heartSize: Dp = 16.dp,
iconSize: Dp = Size20dp,
heartSizeModifier: Modifier = Size16Modifier,
iconFontSize: TextUnit = Font14SP
) {
val iconButtonModifier = remember {
Modifier.size(iconSize)
}
var wantsToChangeReactionSymbol by remember { mutableStateOf(false) }
var wantsToReact by remember { mutableStateOf(false) }
Box(
contentAlignment = Center,
modifier = iconButtonModifier.combinedClickable(
modifier = Modifier.size(iconSize).combinedClickable(
role = Role.Button,
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false, radius = Size24dp),
onClick = {
likeClick(
baseNote,
accountViewModel,
onMultipleChoices = {
wantsToReact = true
},
onWantsToSignReaction = {
if (accountViewModel.account.reactionChoices.size == 1) {
accountViewModel.reactToOrDelete(baseNote)
} else if (accountViewModel.account.reactionChoices.size > 1) {
wantsToReact = true
}
accountViewModel.reactToOrDelete(baseNote)
}
)
},
@@ -736,40 +724,40 @@ fun LikeReaction(
ObserveLikeIcon(baseNote, accountViewModel) { reactionType ->
Crossfade(targetState = reactionType.value, label = "LikeIcon") {
if (it != null) {
RenderReactionType(it, heartSize, iconFontSize)
RenderReactionType(it, heartSizeModifier, iconFontSize)
} else {
LikeIcon(heartSize, grayTint)
LikeIcon(heartSizeModifier, grayTint)
}
}
}
if (wantsToChangeReactionSymbol) {
UpdateReactionTypeDialog(
{ wantsToChangeReactionSymbol = false },
accountViewModel = accountViewModel,
nav
)
}
if (wantsToReact) {
ReactionChoicePopup(
baseNote,
iconSize,
accountViewModel,
onDismiss = {
wantsToReact = false
},
onChangeAmount = {
wantsToReact = false
wantsToChangeReactionSymbol = true
}
)
}
}
ObserveLikeText(baseNote) { reactionCount ->
SlidingAnimationCount(reactionCount, grayTint)
}
if (wantsToChangeReactionSymbol) {
UpdateReactionTypeDialog(
{ wantsToChangeReactionSymbol = false },
accountViewModel = accountViewModel,
nav
)
}
if (wantsToReact) {
ReactionChoicePopup(
baseNote,
iconSize,
accountViewModel,
onDismiss = {
wantsToReact = false
},
onChangeAmount = {
wantsToReact = false
wantsToChangeReactionSymbol = true
}
)
}
}
@Composable
@@ -798,14 +786,10 @@ fun ObserveLikeIcon(
@Composable
private fun RenderReactionType(
reactionType: String,
iconSize: Dp = Size20dp,
iconSizeModifier: Modifier = Size20Modifier,
iconFontSize: TextUnit
) {
val isCustom = remember(reactionType) {
reactionType.startsWith(":")
}
if (isCustom) {
if (reactionType.isNotEmpty() && reactionType[0] == ':') {
val renderable = remember(reactionType) {
listOf(
ImageUrlType(reactionType.removePrefix(":").substringAfter(":"))
@@ -820,7 +804,7 @@ private fun RenderReactionType(
)
} else {
when (reactionType) {
"+" -> LikedIcon(iconSize)
"+" -> LikedIcon(iconSizeModifier)
"-" -> Text(text = "\uD83D\uDC4E", fontSize = iconFontSize)
else -> Text(text = reactionType, fontSize = iconFontSize)
}
@@ -835,7 +819,6 @@ fun ObserveLikeText(baseNote: Note, inner: @Composable (Int) -> Unit) {
}
private fun likeClick(
baseNote: Note,
accountViewModel: AccountViewModel,
onMultipleChoices: () -> Unit,
onWantsToSignReaction: () -> Unit
@@ -851,7 +834,7 @@ private fun likeClick(
R.string.login_with_a_private_key_to_like_posts
)
} else if (accountViewModel.account.reactionChoices.size == 1) {
accountViewModel.reactToOrDelete(baseNote)
onWantsToSignReaction()
} else if (accountViewModel.account.reactionChoices.size > 1) {
onMultipleChoices()
}
@@ -863,7 +846,8 @@ fun ZapReaction(
baseNote: Note,
grayTint: Color,
accountViewModel: AccountViewModel,
iconSize: Dp = 20.dp,
iconSize: Dp = Size20dp,
iconSizeModifier: Modifier = Size20Modifier,
animationSize: Dp = 14.dp,
nav: (String) -> Unit
) {
@@ -884,48 +868,47 @@ fun ZapReaction(
Row(
verticalAlignment = CenterVertically,
modifier = Modifier
.size(iconSize)
.combinedClickable(
role = Role.Button,
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false, radius = Size24dp),
onClick = {
zapClick(
baseNote,
accountViewModel,
context,
onZappingProgress = { progress: Float ->
scope.launch {
zappingProgress = progress
}
},
onMultipleChoices = {
wantsToZap = true
},
onError = { _, message ->
scope.launch {
zappingProgress = 0f
showErrorMessageDialog = message
}
},
onPayViaIntent = {
wantsToPay = it
modifier = iconSizeModifier.combinedClickable(
role = Role.Button,
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false, radius = Size24dp),
onClick = {
zapClick(
baseNote,
accountViewModel,
context,
onZappingProgress = { progress: Float ->
scope.launch {
zappingProgress = progress
}
)
},
onLongClick = {
wantsToChangeZapAmount = true
},
onDoubleClick = {
wantsToSetCustomZap = true
}
)
},
onMultipleChoices = {
wantsToZap = true
},
onError = { _, message ->
scope.launch {
zappingProgress = 0f
showErrorMessageDialog = message
}
},
onPayViaIntent = {
wantsToPay = it
}
)
},
onLongClick = {
wantsToChangeZapAmount = true
},
onDoubleClick = {
wantsToSetCustomZap = true
}
)
) {
if (wantsToZap) {
ZapAmountChoicePopup(
baseNote,
accountViewModel,
baseNote = baseNote,
iconSize = iconSize,
accountViewModel = accountViewModel,
onDismiss = {
wantsToZap = false
zappingProgress = 0f
@@ -1032,9 +1015,9 @@ fun ZapReaction(
) { wasZappedByLoggedInUser ->
Crossfade(targetState = wasZappedByLoggedInUser.value, label = "ZapIcon") {
if (it) {
ZappedIcon(iconSize)
ZappedIcon(iconSizeModifier)
} else {
ZapIcon(iconSize, grayTint)
ZapIcon(iconSizeModifier, grayTint)
}
}
}
@@ -1133,8 +1116,8 @@ private fun ObserveZapAmountText(
fun ViewCountReaction(
note: Note,
grayTint: Color,
barChartModifier: Modifier = Modifier.size(Size19dp),
numberSizeModifier: Modifier = Modifier.height(Size24dp),
barChartModifier: Modifier = Size19Modifier,
numberSizeModifier: Modifier = Height24dpModifier,
viewCountColorFilter: ColorFilter
) {
ViewCountIcon(barChartModifier, grayTint)
@@ -1352,6 +1335,7 @@ private fun ActionableReactionButton(
fun ZapAmountChoicePopup(
baseNote: Note,
accountViewModel: AccountViewModel,
iconSize: Dp,
onDismiss: () -> Unit,
onChangeAmount: () -> Unit,
onError: (title: String, text: String) -> Unit,
@@ -1359,17 +1343,19 @@ fun ZapAmountChoicePopup(
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit
) {
val context = LocalContext.current
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
val zapMessage = ""
val iconSizePx = with(LocalDensity.current) {
-iconSize.toPx().toInt()
}
Popup(
alignment = Alignment.BottomCenter,
offset = IntOffset(0, -50),
offset = IntOffset(0, iconSizePx),
onDismissRequest = { onDismiss() }
) {
FlowRow(horizontalArrangement = Arrangement.Center) {
account.zapAmountChoices.forEach { amountInSats ->
accountViewModel.account.zapAmountChoices.forEach { amountInSats ->
Button(
modifier = Modifier.padding(horizontal = 3.dp),
onClick = {
@@ -1382,7 +1368,7 @@ fun ZapAmountChoicePopup(
onError,
onProgress,
onPayViaIntent,
account.defaultZapType
accountViewModel.account.defaultZapType
)
onDismiss()
},
@@ -1407,7 +1393,7 @@ fun ZapAmountChoicePopup(
onError,
onProgress,
onPayViaIntent,
account.defaultZapType
accountViewModel.account.defaultZapType
)
onDismiss()
},
@@ -63,6 +63,7 @@ import com.vitorpamplona.amethyst.ui.actions.SaveButton
import com.vitorpamplona.amethyst.ui.components.ImageUrlType
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
import com.vitorpamplona.amethyst.ui.components.TextType
import com.vitorpamplona.amethyst.ui.navigation.routeFor
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.placeholderText
@@ -139,19 +139,13 @@ fun UserPicture(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val route by remember(user) {
derivedStateOf {
"User/${user.pubkeyHex}"
}
}
ClickableUserPicture(
baseUser = user,
size = size,
accountViewModel = accountViewModel,
modifier = pictureModifier,
onClick = {
nav(route)
nav("User/${user.pubkeyHex}")
}
)
}
@@ -365,7 +359,10 @@ fun WatchUserFollows(userHex: String, accountViewModel: AccountViewModel, onFoll
accountViewModel.userFollows.map {
it.user.isFollowingCached(userHex) || (userHex == accountViewModel.account.userProfile().pubkeyHex)
}.distinctUntilChanged()
}.observeAsState(false)
}.observeAsState(
accountViewModel.account.userProfile().isFollowingCached(userHex) ||
(userHex == accountViewModel.account.userProfile().pubkeyHex)
)
onFollowChanges(showFollowingMark)
}
@@ -3,7 +3,6 @@ package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
@@ -65,20 +64,22 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.components.InlineCarrousel
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
import com.vitorpamplona.amethyst.ui.elements.DisplayFollowingCommunityInPost
import com.vitorpamplona.amethyst.ui.elements.DisplayFollowingHashtagsInPost
import com.vitorpamplona.amethyst.ui.elements.DisplayPoW
import com.vitorpamplona.amethyst.ui.elements.DisplayReward
import com.vitorpamplona.amethyst.ui.elements.DisplayZapSplits
import com.vitorpamplona.amethyst.ui.elements.Reward
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
import com.vitorpamplona.amethyst.ui.note.AudioHeader
import com.vitorpamplona.amethyst.ui.note.AudioTrackHeader
import com.vitorpamplona.amethyst.ui.note.BadgeDisplay
import com.vitorpamplona.amethyst.ui.note.BlankNote
import com.vitorpamplona.amethyst.ui.note.CreateImageHeader
import com.vitorpamplona.amethyst.ui.note.DisplayFollowingCommunityInPost
import com.vitorpamplona.amethyst.ui.note.DisplayFollowingHashtagsInPost
import com.vitorpamplona.amethyst.ui.note.DisplayHighlight
import com.vitorpamplona.amethyst.ui.note.DisplayLocation
import com.vitorpamplona.amethyst.ui.note.DisplayPeopleList
import com.vitorpamplona.amethyst.ui.note.DisplayPoW
import com.vitorpamplona.amethyst.ui.note.DisplayRelaySet
import com.vitorpamplona.amethyst.ui.note.DisplayReward
import com.vitorpamplona.amethyst.ui.note.DisplayZapSplits
import com.vitorpamplona.amethyst.ui.note.FileHeaderDisplay
import com.vitorpamplona.amethyst.ui.note.FileStorageHeaderDisplay
import com.vitorpamplona.amethyst.ui.note.HiddenNote
@@ -95,8 +96,6 @@ import com.vitorpamplona.amethyst.ui.note.RenderPoll
import com.vitorpamplona.amethyst.ui.note.RenderPostApproval
import com.vitorpamplona.amethyst.ui.note.RenderRepost
import com.vitorpamplona.amethyst.ui.note.RenderTextEvent
import com.vitorpamplona.amethyst.ui.note.Reward
import com.vitorpamplona.amethyst.ui.note.routeToMessage
import com.vitorpamplona.amethyst.ui.note.showAmount
import com.vitorpamplona.amethyst.ui.note.timeAgo
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -98,9 +98,10 @@ import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
import com.vitorpamplona.amethyst.ui.components.ZoomableUrlVideo
import com.vitorpamplona.amethyst.ui.elements.DisplayUncitedHashtags
import com.vitorpamplona.amethyst.ui.navigation.routeFor
import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
import com.vitorpamplona.amethyst.ui.note.DisplayUncitedHashtags
import com.vitorpamplona.amethyst.ui.note.LikeReaction
import com.vitorpamplona.amethyst.ui.note.LoadChannel
import com.vitorpamplona.amethyst.ui.note.MoreOptionsButton
@@ -109,7 +110,6 @@ import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
import com.vitorpamplona.amethyst.ui.note.UserPicture
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.note.ZapReaction
import com.vitorpamplona.amethyst.ui.note.routeFor
import com.vitorpamplona.amethyst.ui.note.timeAgo
import com.vitorpamplona.amethyst.ui.note.timeAgoShort
import com.vitorpamplona.amethyst.ui.screen.NostrChannelFeedViewModel
@@ -47,7 +47,7 @@ import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.note.AddButton
import com.vitorpamplona.amethyst.ui.elements.AddButton
import com.vitorpamplona.amethyst.ui.screen.NostrHiddenAccountsFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrHiddenWordsFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrSpammerAccountsFeedViewModel
@@ -109,12 +109,12 @@ import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog
import com.vitorpamplona.amethyst.ui.components.figureOutMimeType
import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog
import com.vitorpamplona.amethyst.ui.note.LightningAddressIcon
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.ui.note.payViaIntent
import com.vitorpamplona.amethyst.ui.note.routeToMessage
import com.vitorpamplona.amethyst.ui.qrcode.ShowQRDialog
import com.vitorpamplona.amethyst.ui.screen.FeedState
import com.vitorpamplona.amethyst.ui.screen.LnZapFeedView
@@ -46,6 +46,7 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.NostrVideoDataSource
import com.vitorpamplona.amethyst.ui.actions.NewPostView
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
import com.vitorpamplona.amethyst.ui.navigation.routeFor
import com.vitorpamplona.amethyst.ui.note.BoostReaction
import com.vitorpamplona.amethyst.ui.note.FileHeaderDisplay
import com.vitorpamplona.amethyst.ui.note.FileStorageHeaderDisplay
@@ -59,7 +60,6 @@ import com.vitorpamplona.amethyst.ui.note.ReplyReaction
import com.vitorpamplona.amethyst.ui.note.ViewCountReaction
import com.vitorpamplona.amethyst.ui.note.WatchForReports
import com.vitorpamplona.amethyst.ui.note.ZapReaction
import com.vitorpamplona.amethyst.ui.note.routeFor
import com.vitorpamplona.amethyst.ui.screen.FeedEmpty
import com.vitorpamplona.amethyst.ui.screen.FeedError
import com.vitorpamplona.amethyst.ui.screen.FeedState
@@ -69,7 +69,11 @@ import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.RefresheableView
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
import com.vitorpamplona.amethyst.ui.screen.rememberForeverPagerState
import com.vitorpamplona.amethyst.ui.theme.Size35Modifier
import com.vitorpamplona.amethyst.ui.theme.Size35dp
import com.vitorpamplona.amethyst.ui.theme.Size39Modifier
import com.vitorpamplona.amethyst.ui.theme.Size40Modifier
import com.vitorpamplona.amethyst.ui.theme.Size40dp
import com.vitorpamplona.amethyst.ui.theme.onBackgroundColorFilter
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.events.FileHeaderEvent
@@ -270,7 +274,7 @@ fun RenderReportState(
Crossfade(targetState = (!state.isAcceptable || state.isHiddenAuthor) && !showReportedNote) { showHiddenNote ->
if (showHiddenNote) {
Column(remember { Modifier.fillMaxSize() }, verticalArrangement = Arrangement.Center) {
Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center) {
HiddenNote(
state.relevantReports,
state.isHiddenAuthor,
@@ -432,17 +436,49 @@ fun ReactionsColumn(baseNote: Note, accountViewModel: AccountViewModel, nav: (St
Spacer(modifier = Modifier.height(8.dp))
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(bottom = 75.dp, end = 20.dp)) {
ReplyReaction(baseNote, grayTint = MaterialTheme.colorScheme.onBackground, accountViewModel, iconSize = 40.dp) {
ReplyReaction(
baseNote = baseNote,
grayTint = MaterialTheme.colorScheme.onBackground,
accountViewModel = accountViewModel,
iconSizeModifier = Size40Modifier
) {
routeFor(
baseNote,
accountViewModel.userProfile()
)?.let { nav(it) }
}
BoostReaction(baseNote, grayTint = MaterialTheme.colorScheme.onBackground, accountViewModel, iconSize = 40.dp) {
BoostReaction(
baseNote = baseNote,
grayTint = MaterialTheme.colorScheme.onBackground,
accountViewModel = accountViewModel,
iconSizeModifier = Size40Modifier,
iconSize = Size40dp
) {
wantsToQuote = baseNote
}
LikeReaction(baseNote, grayTint = MaterialTheme.colorScheme.onBackground, accountViewModel, nav, iconSize = 40.dp, heartSize = Size35dp, 28.sp)
ZapReaction(baseNote, grayTint = MaterialTheme.colorScheme.onBackground, accountViewModel, iconSize = 40.dp, animationSize = Size35dp, nav = nav)
ViewCountReaction(baseNote, grayTint = MaterialTheme.colorScheme.onBackground, barChartModifier = Modifier.size(39.dp), viewCountColorFilter = MaterialTheme.colorScheme.onBackgroundColorFilter)
LikeReaction(
baseNote = baseNote,
grayTint = MaterialTheme.colorScheme.onBackground,
accountViewModel = accountViewModel,
nav = nav,
iconSize = Size40dp,
heartSizeModifier = Size35Modifier,
28.sp
)
ZapReaction(
baseNote = baseNote,
grayTint = MaterialTheme.colorScheme.onBackground,
accountViewModel = accountViewModel,
iconSize = Size40dp,
iconSizeModifier = Size40Modifier,
animationSize = Size35dp,
nav = nav
)
ViewCountReaction(
note = baseNote,
grayTint = MaterialTheme.colorScheme.onBackground,
barChartModifier = Size39Modifier,
viewCountColorFilter = MaterialTheme.colorScheme.onBackgroundColorFilter
)
}
}
@@ -89,13 +89,17 @@ val Size6Modifier = Modifier.size(6.dp)
val Size10Modifier = Modifier.size(10.dp)
val Size15Modifier = Modifier.size(15.dp)
val Size16Modifier = Modifier.size(16.dp)
val Size17Modifier = Modifier.size(17.dp)
val Size18Modifier = Modifier.size(18.dp)
val Size19Modifier = Modifier.size(19.dp)
val Size20Modifier = Modifier.size(20.dp)
val Size22Modifier = Modifier.size(22.dp)
val Size24Modifier = Modifier.size(24.dp)
val Size26Modifier = Modifier.size(26.dp)
val Size30Modifier = Modifier.size(30.dp)
val Size35Modifier = Modifier.size(35.dp)
val Size39Modifier = Modifier.size(39.dp)
val Size40Modifier = Modifier.size(40.dp)
val Size50Modifier = Modifier.size(50.dp)
val Size55Modifier = Modifier.size(55.dp)
@@ -114,6 +118,7 @@ val ReactionRowHeightChat = Modifier.height(25.dp)
val UserNameRowHeight = Modifier.fillMaxWidth()
val UserNameMaxRowHeight = Modifier.fillMaxWidth()
val Height24dpModifier = Modifier.height(24.dp)
val Height4dpModifier = Modifier.height(4.dp)
val AccountPictureModifier = Modifier.size(55.dp).clip(shape = CircleShape)
@@ -161,3 +166,24 @@ val IconRowModifier = Modifier
.padding(vertical = 15.dp, horizontal = 25.dp)
val emptyLineItemModifier = Modifier.height(Size75dp).fillMaxWidth()
val normalNoteModifier = Modifier.fillMaxWidth()
.padding(
start = 12.dp,
end = 12.dp,
top = 0.dp
)
val normalWithTopMarginNoteModifier = Modifier.fillMaxWidth()
.padding(
start = 12.dp,
end = 12.dp,
top = 10.dp
)
val boostedNoteModifier = Modifier.fillMaxWidth()
.padding(
start = 0.dp,
end = 0.dp,
top = 0.dp
)
@@ -4,12 +4,14 @@ import android.app.Activity
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
@@ -27,6 +29,7 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.view.WindowCompat
import androidx.lifecycle.viewmodel.compose.viewModel
import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.resolveDefaults
import com.patrykandpatrick.vico.compose.style.ChartStyle
@@ -318,7 +321,7 @@ val ColorScheme.relayIconModifier: Modifier
get() = if (isLight) LightRelayIconModifier else DarkRelayIconModifier
val ColorScheme.largeRelayIconModifier: Modifier
get() = if (isLight) LightRelayIconModifier else DarkRelayIconModifier
get() = if (isLight) LightLargeRelayIconModifier else DarkLargeRelayIconModifier
val ColorScheme.chartStyle: ChartStyle
get() {
@@ -367,3 +370,27 @@ fun AmethystTheme(sharedPrefsViewModel: SharedPreferencesViewModel, content: @Co
}
}
}
@Composable
fun ThemeComparison(
onDark: @Composable () -> Unit,
onLight: @Composable () -> Unit
) {
Column() {
val darkTheme: SharedPreferencesViewModel = viewModel()
darkTheme.updateTheme(ThemeType.DARK)
AmethystTheme(darkTheme) {
Surface(color = MaterialTheme.colorScheme.background) {
onDark()
}
}
val lightTheme: SharedPreferencesViewModel = viewModel()
lightTheme.updateTheme(ThemeType.LIGHT)
AmethystTheme(lightTheme) {
Surface(color = MaterialTheme.colorScheme.background) {
onLight()
}
}
}
}
@@ -24,6 +24,7 @@ class PushNotificationReceiverService : FirebaseMessagingService() {
// this is called when a message is received
override fun onMessageReceived(remoteMessage: RemoteMessage) {
Log.d("Time", "Notification received $remoteMessage")
scope.launch(Dispatchers.IO) {
val (value, elapsed) = measureTimedValue {
parseMessage(remoteMessage.data)?.let {
@@ -23,6 +23,7 @@ class Nip44v2(val secp256k1: Secp256k1, val random: SecureRandom) {
private val hkdf = Hkdf()
private val h02 = Hex.decode("02")
private val saltPrefix = "nip44-v2".toByteArray(Charsets.UTF_8)
private val hashLength = 32
private val minPlaintextSize: Int = 0x0001 // 1b msg => padded to 32b
@@ -179,7 +180,7 @@ class Nip44v2(val secp256k1: Secp256k1, val random: SecureRandom) {
*/
fun computeConversationKey(privateKey: ByteArray, pubKey: ByteArray): ByteArray {
val sharedX = secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33)
return hkdf.extract(sharedX, "nip44-v2".toByteArray(Charsets.UTF_8))
return hkdf.extract(sharedX, saltPrefix)
}
class EncryptedInfo(