Merge remote-tracking branch 'origin/HEAD' into less_memory_test_branch

This commit is contained in:
Vitor Pamplona
2023-03-10 11:28:13 -05:00
32 changed files with 655 additions and 120 deletions
@@ -2,18 +2,20 @@ package com.vitorpamplona.amethyst
import android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import androidx.security.crypto.MasterKey
class EncryptedStorage {
object EncryptedStorage {
private const val PREFERENCES_NAME = "secret_keeper"
fun preferences(context: Context): EncryptedSharedPreferences {
val secretKey: String = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
val preferencesName = "secret_keeper"
val masterKey: MasterKey = MasterKey.Builder(context, MasterKey.DEFAULT_MASTER_KEY_ALIAS)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
return EncryptedSharedPreferences.create(
preferencesName,
secretKey,
context,
PREFERENCES_NAME,
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
) as EncryptedSharedPreferences
@@ -29,7 +29,7 @@ class LocalPreferences(context: Context) {
val LAST_READ: (String) -> String = { route -> "last_read_route_$route" }
}
private val encryptedPreferences = EncryptedStorage().preferences(context)
private val encryptedPreferences = EncryptedStorage.preferences(context)
private val gson = GsonBuilder().create()
fun clearEncryptedStorage() {
@@ -300,7 +300,7 @@ object LocalCache {
// Saves relay list only if it's a user that is currently been seen
user.updateContactList(event)
Log.d("CL", "AAA ${user.toBestDisplayName()} ${follows.size}")
// Log.d("CL", "AAA ${user.toBestDisplayName()} ${follows.size}")
}
}
@@ -368,7 +368,7 @@ class NoteLiveData(val note: Note) : LiveData<NoteState>(NoteState(note)) {
}
}
fun refresh() {
private fun refresh() {
postValue(NoteState(note))
}
@@ -33,12 +33,17 @@ class LnZapEvent(
}
override fun amount(): BigDecimal? {
return lnInvoice()?.let { LnInvoiceUtil.getAmountInSats(it) }
return amount
}
// Keeps this as a field because it's a heavier function used everywhere.
val amount by lazy {
lnInvoice()?.let { LnInvoiceUtil.getAmountInSats(it) }
try {
lnInvoice()?.let { LnInvoiceUtil.getAmountInSats(it) }
} catch (e: Exception) {
Log.e("LnZapEvent", "Failed to Parse LnInvoice ${description()}", e)
null
}
}
override fun containedPost(): Event? = try {
@@ -21,6 +21,9 @@ import com.vitorpamplona.amethyst.service.relays.Client
import com.vitorpamplona.amethyst.ui.screen.AccountScreen
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
class MainActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
@@ -66,7 +69,9 @@ class MainActivity : FragmentActivity() {
super.onResume()
// Only starts after login
ServiceManager.start()
GlobalScope.launch(Dispatchers.IO) {
ServiceManager.start()
}
}
override fun onPause() {
@@ -40,7 +40,9 @@ import androidx.navigation.NavHostController
import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
val bottomNavigationItems = listOf(
Route.Home,
@@ -157,11 +159,15 @@ private fun NotifiableIcon(item: Route, currentRoute: String?, accountViewModel:
val context = LocalContext.current.applicationContext
LaunchedEffect(key1 = notif) {
hasNewItems = item.hasNewItems(account, notif.cache, context)
withContext(Dispatchers.IO) {
hasNewItems = item.hasNewItems(account, notif.cache, context)
}
}
LaunchedEffect(key1 = db) {
hasNewItems = item.hasNewItems(account, notif.cache, context)
withContext(Dispatchers.IO) {
hasNewItems = item.hasNewItems(account, notif.cache, context)
}
}
if (hasNewItems) {
@@ -43,6 +43,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import androidx.navigation.NavHostController
import coil.compose.AsyncImage
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.RoboHashCache
@@ -114,8 +115,8 @@ fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffol
Box {
val banner = accountUser.info?.banner
if (banner != null && banner.isNotBlank()) {
AsyncImageProxy(
model = ResizeImage(banner, 150.dp),
AsyncImage(
model = banner,
contentDescription = stringResource(id = R.string.profile_image),
contentScale = ContentScale.FillWidth,
modifier = Modifier
@@ -11,10 +11,12 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MilitaryTech
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -35,6 +37,8 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.ui.screen.BadgeCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -56,9 +60,11 @@ fun BadgeCompose(likeSetCard: BadgeCard, modifier: Modifier = Modifier, isInnerN
var isNew by remember { mutableStateOf<Boolean>(false) }
LaunchedEffect(key1 = likeSetCard) {
isNew = likeSetCard.createdAt() > NotificationCache.load(routeForLastRead, context)
withContext(Dispatchers.IO) {
isNew = likeSetCard.createdAt() > NotificationCache.load(routeForLastRead, context)
NotificationCache.markAsRead(routeForLastRead, likeSetCard.createdAt(), context)
NotificationCache.markAsRead(routeForLastRead, likeSetCard.createdAt(), context)
}
}
var backgroundColor = if (isNew) {
@@ -108,11 +114,33 @@ fun BadgeCompose(likeSetCard: BadgeCard, modifier: Modifier = Modifier, isInnerN
}
Column(modifier = Modifier.padding(start = if (!isInnerNote) 10.dp else 0.dp)) {
Text(
stringResource(R.string.new_badge_award_notif),
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 5.dp)
)
Row() {
Text(
stringResource(R.string.new_badge_award_notif),
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 5.dp).weight(1f)
)
Text(
timeAgo(note.createdAt(), context = context),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
maxLines = 1
)
IconButton(
modifier = Modifier.then(Modifier.size(24.dp)),
onClick = { popupExpanded = true }
) {
Icon(
imageVector = Icons.Default.MoreVert,
null,
modifier = Modifier.size(15.dp),
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
NoteDropDownMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
}
}
note.replyTo?.firstOrNull()?.let {
NoteCompose(
@@ -125,8 +153,6 @@ fun BadgeCompose(likeSetCard: BadgeCard, modifier: Modifier = Modifier, isInnerN
)
}
NoteDropDownMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
Divider(
modifier = Modifier.padding(top = 10.dp),
thickness = 0.25.dp
@@ -32,6 +32,8 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.ui.screen.BoostSetCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -53,9 +55,11 @@ fun BoostSetCompose(boostSetCard: BoostSetCard, isInnerNote: Boolean = false, ro
var isNew by remember { mutableStateOf<Boolean>(false) }
LaunchedEffect(key1 = boostSetCard) {
isNew = boostSetCard.createdAt > NotificationCache.load(routeForLastRead, context)
withContext(Dispatchers.IO) {
isNew = boostSetCard.createdAt > NotificationCache.load(routeForLastRead, context)
NotificationCache.markAsRead(routeForLastRead, boostSetCard.createdAt, context)
NotificationCache.markAsRead(routeForLastRead, boostSetCard.createdAt, context)
}
}
var backgroundColor = if (isNew) {
@@ -52,6 +52,8 @@ import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@Composable
fun ChatroomCompose(
@@ -62,9 +64,6 @@ fun ChatroomCompose(
val noteState by baseNote.live().metadata.observeAsState()
val note = noteState?.note
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
val notificationCacheState = NotificationCache.live.observeAsState()
val notificationCache = notificationCacheState.value ?: return
@@ -92,9 +91,11 @@ fun ChatroomCompose(
var hasNewMessages by remember { mutableStateOf<Boolean>(false) }
LaunchedEffect(key1 = notificationCache, key2 = note) {
note.createdAt()?.let {
hasNewMessages =
it > notificationCache.cache.load("Channel/${channel.idHex}", context)
withContext(Dispatchers.IO) {
note.createdAt()?.let {
hasNewMessages =
it > notificationCache.cache.load("Channel/${channel.idHex}", context)
}
}
}
@@ -146,7 +147,7 @@ fun ChatroomCompose(
var userToComposeOn = note.author!!
if (replyAuthorBase != null) {
if (note.author == account.userProfile()) {
if (note.author == accountViewModel.userProfile()) {
userToComposeOn = replyAuthorBase
}
}
@@ -157,11 +158,13 @@ fun ChatroomCompose(
var hasNewMessages by remember { mutableStateOf<Boolean>(false) }
LaunchedEffect(key1 = notificationCache, key2 = note) {
noteEvent?.let {
hasNewMessages = it.createdAt() > notificationCache.cache.load(
"Room/${userToComposeOn.pubkeyHex}",
context
)
withContext(Dispatchers.IO) {
noteEvent?.let {
hasNewMessages = it.createdAt() > notificationCache.cache.load(
"Room/${userToComposeOn.pubkeyHex}",
context
)
}
}
}
@@ -169,7 +172,7 @@ fun ChatroomCompose(
channelPicture = {
UserPicture(
userToComposeOn,
account.userProfile(),
accountViewModel.userProfile(),
size = 55.dp
)
},
@@ -64,6 +64,8 @@ import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
val ChatBubbleShapeMe = RoundedCornerShape(15.dp, 15.dp, 3.dp, 15.dp)
val ChatBubbleShapeThem = RoundedCornerShape(3.dp, 15.dp, 15.dp, 15.dp)
@@ -131,12 +133,14 @@ fun ChatroomMessageCompose(
LaunchedEffect(key1 = routeForLastRead) {
routeForLastRead?.let {
val lastTime = NotificationCache.load(it, context)
withContext(Dispatchers.IO) {
val lastTime = NotificationCache.load(it, context)
val createdAt = note.createdAt()
if (createdAt != null) {
NotificationCache.markAsRead(it, createdAt, context)
isNew = createdAt > lastTime
val createdAt = note.createdAt()
if (createdAt != null) {
NotificationCache.markAsRead(it, createdAt, context)
isNew = createdAt > lastTime
}
}
}
}
@@ -32,6 +32,8 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.ui.screen.LikeSetCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -53,9 +55,11 @@ fun LikeSetCompose(likeSetCard: LikeSetCard, modifier: Modifier = Modifier, isIn
var isNew by remember { mutableStateOf<Boolean>(false) }
LaunchedEffect(key1 = likeSetCard) {
isNew = likeSetCard.createdAt > NotificationCache.load(routeForLastRead, context)
withContext(Dispatchers.IO) {
isNew = likeSetCard.createdAt > NotificationCache.load(routeForLastRead, context)
NotificationCache.markAsRead(routeForLastRead, likeSetCard.createdAt, context)
NotificationCache.markAsRead(routeForLastRead, likeSetCard.createdAt, context)
}
}
var backgroundColor = if (isNew) {
@@ -0,0 +1,126 @@
package com.vitorpamplona.amethyst.ui.note
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.Icon
import androidx.compose.material.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.Modifier
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.ui.screen.MessageSetCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun MessageSetCompose(messageSetCard: MessageSetCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, navController: NavController) {
val noteState by messageSetCard.note.live().metadata.observeAsState()
val note = noteState?.note
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
val context = LocalContext.current.applicationContext
val noteEvent = note?.event
var popupExpanded by remember { mutableStateOf(false) }
if (note == null) {
BlankNote(Modifier, isInnerNote)
} else {
var isNew by remember { mutableStateOf<Boolean>(false) }
LaunchedEffect(key1 = messageSetCard) {
withContext(Dispatchers.IO) {
isNew =
messageSetCard.createdAt() > NotificationCache.load(routeForLastRead, context)
NotificationCache.markAsRead(routeForLastRead, messageSetCard.createdAt(), context)
}
}
var backgroundColor = if (isNew) {
MaterialTheme.colors.primary.copy(0.12f).compositeOver(MaterialTheme.colors.background)
} else {
MaterialTheme.colors.background
}
Column(
modifier = Modifier.background(backgroundColor).combinedClickable(
onClick = {
if (noteEvent !is ChannelMessageEvent) {
navController.navigate("Note/${note.idHex}") {
launchSingleTop = true
}
} else {
note.channel()?.let {
navController.navigate("Channel/${it.idHex}")
}
}
},
onLongClick = { popupExpanded = true }
)
) {
Row(
modifier = Modifier
.padding(
start = if (!isInnerNote) 12.dp else 0.dp,
end = if (!isInnerNote) 12.dp else 0.dp,
top = 10.dp
)
) {
// Draws the like picture outside the boosted card.
if (!isInnerNote) {
Box(
modifier = Modifier
.width(55.dp)
.padding(top = 5.dp)
) {
Icon(
painter = painterResource(R.drawable.ic_dm),
null,
modifier = Modifier.size(16.dp).align(Alignment.TopEnd),
tint = MaterialTheme.colors.primary
)
}
}
Column(modifier = Modifier.padding(start = if (!isInnerNote) 10.dp else 0.dp)) {
NoteCompose(
baseNote = note,
routeForLastRead = null,
isBoostedNote = true,
addMarginTop = false,
parentBackgroundColor = backgroundColor,
accountViewModel = accountViewModel,
navController = navController
)
NoteDropDownMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
}
}
}
}
}
@@ -36,6 +36,8 @@ import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.ui.screen.MultiSetCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -57,9 +59,11 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, modifier: Modifier = Modifier, r
var isNew by remember { mutableStateOf<Boolean>(false) }
LaunchedEffect(key1 = multiSetCard) {
isNew = multiSetCard.createdAt > NotificationCache.load(routeForLastRead, context)
withContext(Dispatchers.IO) {
isNew = multiSetCard.createdAt > NotificationCache.load(routeForLastRead, context)
NotificationCache.markAsRead(routeForLastRead, multiSetCard.createdAt, context)
NotificationCache.markAsRead(routeForLastRead, multiSetCard.createdAt, context)
}
}
var backgroundColor = if (isNew) {
@@ -60,6 +60,8 @@ import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
import com.vitorpamplona.amethyst.ui.theme.Following
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -71,6 +73,7 @@ fun NoteCompose(
isQuotedNote: Boolean = false,
unPackReply: Boolean = true,
makeItShort: Boolean = false,
addMarginTop: Boolean = true,
parentBackgroundColor: Color? = null,
accountViewModel: AccountViewModel,
navController: NavController
@@ -119,13 +122,15 @@ fun NoteCompose(
var isNew by remember { mutableStateOf<Boolean>(false) }
LaunchedEffect(key1 = routeForLastRead) {
routeForLastRead?.let {
val lastTime = NotificationCache.load(it, context)
withContext(Dispatchers.IO) {
routeForLastRead?.let {
val lastTime = NotificationCache.load(it, context)
val createdAt = note.createdAt()
if (createdAt != null) {
NotificationCache.markAsRead(it, createdAt, context)
isNew = createdAt > lastTime
val createdAt = note.createdAt()
if (createdAt != null) {
NotificationCache.markAsRead(it, createdAt, context)
isNew = createdAt > lastTime
}
}
}
}
@@ -168,7 +173,7 @@ fun NoteCompose(
.padding(
start = if (!isBoostedNote) 12.dp else 0.dp,
end = if (!isBoostedNote) 12.dp else 0.dp,
top = 10.dp
top = if (addMarginTop) 10.dp else 0.dp
)
) {
if (!isBoostedNote && !isQuotedNote) {
@@ -414,6 +419,34 @@ fun NoteCompose(
ReactionsRow(note, accountViewModel)
Divider(
modifier = Modifier.padding(top = 10.dp),
thickness = 0.25.dp
)
} else if (noteEvent is PrivateDmEvent &&
noteEvent.recipientPubKey() != account.userProfile().pubkeyHex &&
note.author != account.userProfile()
) {
val recepient = noteEvent.recipientPubKey()?.let { LocalCache.checkGetOrCreateUser(it) }
TranslateableRichTextViewer(
stringResource(
id = R.string.private_conversation_notification,
"@${note.author?.pubkeyNpub()}",
"@${recepient?.pubkeyNpub()}"
),
canPreview = !makeItShort,
Modifier.fillMaxWidth(),
noteEvent.tags(),
backgroundColor,
accountViewModel,
navController
)
if (!makeItShort) {
ReactionsRow(note, accountViewModel)
}
Divider(
modifier = Modifier.padding(top = 10.dp),
thickness = 0.25.dp
@@ -220,12 +220,12 @@ fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Uni
onDismiss()
}
) {
Text("Don't show again")
Text(stringResource(R.string.quick_action_dont_show_again_button))
}
Button(
onClick = { accountViewModel.delete(note); onDismiss() }
) {
Text("Delete")
Text(stringResource(R.string.quick_action_delete_button))
}
}
}
@@ -72,7 +72,9 @@ import com.vitorpamplona.amethyst.ui.actions.NewPostView
import com.vitorpamplona.amethyst.ui.actions.SaveButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.math.BigDecimal
import java.math.RoundingMode
@@ -99,7 +101,9 @@ fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel) {
}
Row(
modifier = Modifier.padding(top = 8.dp).fillMaxWidth(),
modifier = Modifier
.padding(top = 8.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
@@ -350,7 +354,12 @@ fun ZapReaction(
.show()
}
} else if (account.zapAmountChoices.size == 1) {
accountViewModel.zap(baseNote, account.zapAmountChoices.first() * 1000, "", context) {
accountViewModel.zap(
baseNote,
account.zapAmountChoices.first() * 1000,
"",
context
) {
scope.launch {
Toast
.makeText(context, it, Toast.LENGTH_SHORT)
@@ -405,8 +414,16 @@ fun ZapReaction(
}
}
var zapAmount by remember { mutableStateOf<BigDecimal?>(null) }
LaunchedEffect(key1 = zappedNote) {
withContext(Dispatchers.IO) {
zapAmount = zappedNote?.zappedAmount()
}
}
Text(
showAmount(zappedNote?.zappedAmount()),
showAmount(zapAmount),
fontSize = 14.sp,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
modifier = textModifier
@@ -9,9 +9,13 @@ import androidx.compose.material.Divider
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@@ -31,6 +35,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.UnfollowButton
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.math.BigDecimal
@Composable
fun ZapNoteCompose(baseNote: Pair<Note, Note>, accountViewModel: AccountViewModel, navController: NavController) {
@@ -88,15 +94,20 @@ fun ZapNoteCompose(baseNote: Pair<Note, Note>, accountViewModel: AccountViewMode
)
}
val amount =
(noteZap.event as? LnZapEvent)?.amount
var zapAmount by remember { mutableStateOf<BigDecimal?>(null) }
LaunchedEffect(key1 = noteZap) {
withContext(Dispatchers.IO) {
zapAmount = (noteZap.event as? LnZapEvent)?.amount
}
}
Column(
modifier = Modifier.padding(start = 10.dp),
verticalArrangement = Arrangement.Center
) {
Text(
"${showAmount(amount)} ${stringResource(R.string.sats)}",
"${showAmount(zapAmount)} ${stringResource(R.string.sats)}",
color = BitcoinOrange,
fontSize = 20.sp,
fontWeight = FontWeight.W500
@@ -34,6 +34,8 @@ import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.ui.screen.ZapSetCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -55,9 +57,11 @@ fun ZapSetCompose(zapSetCard: ZapSetCard, modifier: Modifier = Modifier, isInner
var isNew by remember { mutableStateOf<Boolean>(false) }
LaunchedEffect(key1 = zapSetCard) {
isNew = zapSetCard.createdAt > NotificationCache.load(routeForLastRead, context)
withContext(Dispatchers.IO) {
isNew = zapSetCard.createdAt > NotificationCache.load(routeForLastRead, context)
NotificationCache.markAsRead(routeForLastRead, zapSetCard.createdAt, context)
NotificationCache.markAsRead(routeForLastRead, zapSetCard.createdAt, context)
}
}
var backgroundColor = if (isNew) {
@@ -63,6 +63,14 @@ class BoostSetCard(val note: Note, val boostEvents: List<Note>) : Card() {
override fun id() = note.idHex + "B" + createdAt
}
class MessageSetCard(val note: Note) : Card() {
override fun createdAt(): Long {
return note.createdAt() ?: 0
}
override fun id() = note.idHex
}
sealed class CardFeedState {
object Loading : CardFeedState()
class Loaded(val feed: MutableState<List<Card>>) : CardFeedState()
@@ -21,6 +21,7 @@ import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.vitorpamplona.amethyst.ui.note.BadgeCompose
import com.vitorpamplona.amethyst.ui.note.BoostSetCompose
import com.vitorpamplona.amethyst.ui.note.LikeSetCompose
import com.vitorpamplona.amethyst.ui.note.MessageSetCompose
import com.vitorpamplona.amethyst.ui.note.MultiSetCompose
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.note.ZapSetCompose
@@ -134,6 +135,12 @@ private fun FeedLoaded(
navController = navController,
routeForLastRead = routeForLastRead
)
is MessageSetCard -> MessageSetCompose(
messageSetCard = item,
routeForLastRead = routeForLastRead,
accountViewModel = accountViewModel,
navController = navController
)
}
}
}
@@ -9,6 +9,7 @@ import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
import com.vitorpamplona.amethyst.service.model.LnZapEvent
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import com.vitorpamplona.amethyst.service.model.ReactionEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
@@ -111,7 +112,9 @@ open class CardFeedViewModel(val dataSource: FeedFilter<Note>) : ViewModel() {
}
val textNoteCards = notes.filter { it.event !is ReactionEvent && it.event !is RepostEvent && it.event !is LnZapEvent }.map {
if (it.event is BadgeAwardEvent) {
if (it.event is PrivateDmEvent) {
MessageSetCard(it)
} else if (it.event is BadgeAwardEvent) {
BadgeCard(it)
} else {
NoteCard(it)
@@ -63,7 +63,7 @@ open class LnZapFeedViewModel(val dataSource: FeedFilter<Pair<Note, Note>>) : Vi
var handlerWaiting = AtomicBoolean()
private fun invalidateData() {
fun invalidateData() {
if (handlerWaiting.getAndSet(true)) return
val scope = CoroutineScope(Job() + Dispatchers.Default)
@@ -76,7 +76,6 @@ open class LnZapFeedViewModel(val dataSource: FeedFilter<Pair<Note, Note>>) : Vi
handlerWaiting.set(false)
}
}
handlerWaiting.set(false)
}
}
@@ -67,7 +67,7 @@ open class UserFeedViewModel(val dataSource: FeedFilter<User>) : ViewModel() {
var handlerWaiting = AtomicBoolean()
private fun invalidateData() {
fun invalidateData() {
if (handlerWaiting.getAndSet(true)) return
val scope = CoroutineScope(Job() + Dispatchers.Default)
@@ -62,7 +62,6 @@ import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent
import com.vitorpamplona.amethyst.service.model.IdentityClaim
import com.vitorpamplona.amethyst.service.model.ReportEvent
import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataView
import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.DisplayNip05ProfileStatus
import com.vitorpamplona.amethyst.ui.components.InvoiceRequest
import com.vitorpamplona.amethyst.ui.components.ResizeImage
@@ -89,6 +88,8 @@ import com.vitorpamplona.amethyst.ui.screen.UserFeedView
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.math.BigDecimal
@OptIn(ExperimentalPagerApi::class)
@Composable
@@ -210,9 +211,20 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro
},
{
val userState by baseUser.live().zaps.observeAsState()
val userZaps = userState?.user?.zappedAmount()
val userZaps = userState?.user
Text(text = "${showAmount(userZaps)} ${stringResource(id = R.string.zaps)}")
var zapAmount by remember { mutableStateOf<BigDecimal?>(null) }
LaunchedEffect(key1 = userState) {
withContext(Dispatchers.IO) {
val tempAmount = userZaps?.zappedAmount()
withContext(Dispatchers.Main) {
zapAmount = tempAmount
}
}
}
Text(text = "${showAmount(zapAmount)} ${stringResource(id = R.string.zaps)}")
},
{
val userState by baseUser.live().reports.observeAsState()
@@ -610,8 +622,8 @@ private fun DrawBanner(baseUser: User) {
var zoomImageDialogOpen by remember { mutableStateOf(false) }
if (!banner.isNullOrBlank()) {
AsyncImageProxy(
model = ResizeImage(banner, 125.dp),
AsyncImage(
model = banner,
contentDescription = stringResource(id = R.string.profile_image),
contentScale = ContentScale.FillWidth,
modifier = Modifier
@@ -682,11 +694,13 @@ fun TabNotesConversations(user: User, accountViewModel: AccountViewModel, navCon
}
@Composable
fun TabFollows(user: User, accountViewModel: AccountViewModel, navController: NavController) {
fun TabFollows(baseUser: User, accountViewModel: AccountViewModel, navController: NavController) {
val feedViewModel: NostrUserProfileFollowsUserFeedViewModel = viewModel()
LaunchedEffect(Unit) {
feedViewModel.refresh()
val userState by baseUser.live().follows.observeAsState()
LaunchedEffect(userState) {
feedViewModel.invalidateData()
}
Column(Modifier.fillMaxHeight()) {
@@ -699,11 +713,13 @@ fun TabFollows(user: User, accountViewModel: AccountViewModel, navController: Na
}
@Composable
fun TabFollowers(user: User, accountViewModel: AccountViewModel, navController: NavController) {
fun TabFollowers(baseUser: User, accountViewModel: AccountViewModel, navController: NavController) {
val feedViewModel: NostrUserProfileFollowersUserFeedViewModel = viewModel()
LaunchedEffect(Unit) {
feedViewModel.refresh()
val userState by baseUser.live().follows.observeAsState()
LaunchedEffect(userState) {
feedViewModel.invalidateData()
}
Column(Modifier.fillMaxHeight()) {
@@ -716,41 +732,39 @@ fun TabFollowers(user: User, accountViewModel: AccountViewModel, navController:
}
@Composable
fun TabReceivedZaps(user: User, accountViewModel: AccountViewModel, navController: NavController) {
val accountState by accountViewModel.accountLiveData.observeAsState()
if (accountState != null) {
val feedViewModel: NostrUserProfileZapsFeedViewModel = viewModel()
fun TabReceivedZaps(baseUser: User, accountViewModel: AccountViewModel, navController: NavController) {
val feedViewModel: NostrUserProfileZapsFeedViewModel = viewModel()
LaunchedEffect(Unit) {
feedViewModel.refresh()
}
val userState by baseUser.live().zaps.observeAsState()
Column(Modifier.fillMaxHeight()) {
Column(
modifier = Modifier.padding(vertical = 0.dp)
) {
LnZapFeedView(feedViewModel, accountViewModel, navController)
}
LaunchedEffect(userState) {
feedViewModel.invalidateData()
}
Column(Modifier.fillMaxHeight()) {
Column(
modifier = Modifier.padding(vertical = 0.dp)
) {
LnZapFeedView(feedViewModel, accountViewModel, navController)
}
}
}
@Composable
fun TabReports(user: User, accountViewModel: AccountViewModel, navController: NavController) {
val accountState by accountViewModel.accountLiveData.observeAsState()
if (accountState != null) {
val feedViewModel: NostrUserProfileReportFeedViewModel = viewModel()
fun TabReports(baseUser: User, accountViewModel: AccountViewModel, navController: NavController) {
val feedViewModel: NostrUserProfileReportFeedViewModel = viewModel()
LaunchedEffect(Unit) {
feedViewModel.refresh()
}
val userState by baseUser.live().reports.observeAsState()
Column(Modifier.fillMaxHeight()) {
Column(
modifier = Modifier.padding(vertical = 0.dp)
) {
FeedView(feedViewModel, accountViewModel, navController, null)
}
LaunchedEffect(userState) {
feedViewModel.invalidateData()
}
Column(Modifier.fillMaxHeight()) {
Column(
modifier = Modifier.padding(vertical = 0.dp)
) {
FeedView(feedViewModel, accountViewModel, navController, null)
}
}
}
@@ -153,11 +153,16 @@ fun LoginPage(accountViewModel: AccountStateViewModel) {
onCheckedChange = { acceptedTerms.value = it }
)
val regularText =
SpanStyle(color = MaterialTheme.colors.onBackground)
val clickableTextStyle =
SpanStyle(color = MaterialTheme.colors.primary)
val annotatedTermsString = buildAnnotatedString {
append(stringResource(R.string.i_accept_the))
withStyle(regularText) {
append(stringResource(R.string.i_accept_the))
}
withStyle(clickableTextStyle) {
pushStringAnnotation("openTerms", "")