Restructuring follows observables

This commit is contained in:
Vitor Pamplona
2023-06-01 15:16:03 -04:00
parent b0e50e0c00
commit 92c61f317b
39 changed files with 968 additions and 736 deletions
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst
import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import androidx.compose.runtime.Immutable
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import com.vitorpamplona.amethyst.model.Account
@@ -30,6 +31,7 @@ import java.util.Locale
private const val DEBUG_PLAINTEXT_PREFERENCES = false
private const val DEBUG_PREFERENCES_NAME = "debug_prefs"
@Immutable
data class AccountInfo(
val npub: String,
val hasPrivKey: Boolean = false
@@ -1004,10 +1004,10 @@ class Account(
}
private fun updateContactListTo(newContactList: ContactListEvent?) {
if (newContactList?.unverifiedFollowKeySet().isNullOrEmpty()) return
if (newContactList == null || newContactList.tags.isEmpty()) return
// Events might be different objects, we have to compare their ids.
if (backupContactList?.id != newContactList?.id) {
if (backupContactList?.id != newContactList.id) {
backupContactList = newContactList
saveable.invalidateData()
}
@@ -249,15 +249,11 @@ class User(val pubkeyHex: String) {
}
fun isFollowing(user: User): Boolean {
return latestContactList?.unverifiedFollowKeySet()?.toSet()?.let {
return user.pubkeyHex in it
} ?: false
return latestContactList?.isTaggedUser(user.pubkeyHex) ?: false
}
fun isFollowingHashtag(tag: String): Boolean {
return latestContactList?.unverifiedFollowTagSet()?.toSet()?.let {
return tag in it
} ?: false
return latestContactList?.isTaggedHash(tag) ?: false
}
fun isFollowingHashtagCached(tag: String): Boolean {
@@ -272,11 +268,17 @@ class User(val pubkeyHex: String) {
} ?: false
}
fun isFollowingCached(userHex: String): Boolean {
return latestContactList?.verifiedFollowKeySet?.let {
return userHex in it
} ?: false
}
fun transientFollowCount(): Int? {
return latestContactList?.unverifiedFollowKeySet()?.size
}
fun transientFollowerCount(): Int {
suspend fun transientFollowerCount(): Int {
return LocalCache.users.values.count { it.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
}
@@ -292,7 +294,7 @@ class User(val pubkeyHex: String) {
return latestContactList?.verifiedFollowKeySet?.size
}
fun cachedFollowerCount(): Int {
suspend fun cachedFollowerCount(): Int {
return LocalCache.users.values.count { it.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
}
@@ -52,13 +52,13 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
import com.vitorpamplona.amethyst.ui.note.ChannelName
import com.vitorpamplona.amethyst.ui.note.UserPicture
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SearchBarViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
@@ -72,9 +72,9 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@Composable
fun JoinUserOrChannelView(onClose: () -> Unit, account: Account, nav: (String) -> Unit) {
fun JoinUserOrChannelView(onClose: () -> Unit, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val searchBarViewModel: SearchBarViewModel = viewModel()
searchBarViewModel.account = account
searchBarViewModel.account = accountViewModel.account
Dialog(
onDismissRequest = {
@@ -115,7 +115,7 @@ fun JoinUserOrChannelView(onClose: () -> Unit, account: Account, nav: (String) -
Spacer(modifier = Modifier.height(15.dp))
RenderSeach(searchBarViewModel, account, nav)
RenderSeach(searchBarViewModel, accountViewModel, nav)
}
}
}
@@ -125,7 +125,7 @@ fun JoinUserOrChannelView(onClose: () -> Unit, account: Account, nav: (String) -
@Composable
private fun RenderSeach(
searchBarViewModel: SearchBarViewModel,
account: Account,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val scope = rememberCoroutineScope()
@@ -273,7 +273,7 @@ private fun RenderSeach(
) { _, item ->
UserComposeForChat(
item,
account = account,
accountViewModel = accountViewModel,
nav = nav
)
}
@@ -305,7 +305,7 @@ private fun RenderSeach(
@Composable
fun UserComposeForChat(
baseUser: User,
account: Account,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
Column(
@@ -323,7 +323,7 @@ fun UserComposeForChat(
),
verticalAlignment = Alignment.CenterVertically
) {
UserPicture(baseUser, nav, account.userProfile(), 55.dp)
UserPicture(baseUser, nav, accountViewModel, 55.dp)
Column(modifier = Modifier.padding(start = 10.dp).weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically) {
@@ -24,13 +24,13 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
fun NewChannelView(onClose: () -> Unit, account: Account, channel: Channel? = null) {
fun NewChannelView(onClose: () -> Unit, accountViewModel: AccountViewModel, channel: Channel? = null) {
val postViewModel: NewChannelViewModel = viewModel()
postViewModel.load(account, channel)
postViewModel.load(accountViewModel.account, channel)
Dialog(
onDismissRequest = { onClose() },
@@ -23,16 +23,16 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import com.vitorpamplona.amethyst.ui.components.*
import com.vitorpamplona.amethyst.ui.note.ReplyInformation
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine
import kotlinx.coroutines.delay
@Composable
fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = null, account: Account) {
fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = null, accountViewModel: AccountViewModel) {
val pollViewModel: NewPostViewModel = viewModel()
val context = LocalContext.current
@@ -40,7 +40,7 @@ fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
val scrollState = rememberScrollState()
LaunchedEffect(Unit) {
pollViewModel.load(account, baseReplyTo, quote)
pollViewModel.load(accountViewModel.account, baseReplyTo, quote)
delay(100)
pollViewModel.imageUploadingError.collect { error ->
@@ -108,7 +108,7 @@ fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
.verticalScroll(scrollState)
) {
if (pollViewModel.replyTos != null && baseReplyTo?.event is TextNoteEvent) {
ReplyInformation(pollViewModel.replyTos, pollViewModel.mentions, account, "") {
ReplyInformation(pollViewModel.replyTos, pollViewModel.mentions, accountViewModel, "") {
pollViewModel.removeFromReplyList(it)
}
}
@@ -151,7 +151,7 @@ fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
userSuggestions,
key = { _, item -> item.pubkeyHex }
) { _, item ->
UserLine(item, account) {
UserLine(item, accountViewModel) {
pollViewModel.autocompleteWithUser(item)
}
}
@@ -319,7 +319,7 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
userSuggestions,
key = { _, item -> item.pubkeyHex }
) { _, item ->
UserLine(item, account) {
UserLine(item, accountViewModel) {
postViewModel.autocompleteWithUser(item)
}
}
@@ -16,12 +16,12 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.actions.JoinUserOrChannelView
import com.vitorpamplona.amethyst.ui.actions.NewChannelView
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
fun ChannelFabColumn(account: Account, nav: (String) -> Unit) {
fun ChannelFabColumn(accountViewModel: AccountViewModel, nav: (String) -> Unit) {
var isOpen by remember {
mutableStateOf(false)
}
@@ -35,11 +35,11 @@ fun ChannelFabColumn(account: Account, nav: (String) -> Unit) {
}
if (wantsToCreateChannel) {
NewChannelView({ wantsToCreateChannel = false }, account = account)
NewChannelView({ wantsToCreateChannel = false }, accountViewModel = accountViewModel)
}
if (wantsToJoinChannelOrUser) {
JoinUserOrChannelView({ wantsToJoinChannelOrUser = false }, account = account, nav = nav)
JoinUserOrChannelView({ wantsToJoinChannelOrUser = false }, accountViewModel = accountViewModel, nav = nav)
}
Column() {
@@ -19,17 +19,17 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.actions.NewChannelView
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
fun NewChannelButton(account: Account) {
fun NewChannelButton(accountViewModel: AccountViewModel) {
var wantsToPost by remember {
mutableStateOf(false)
}
if (wantsToPost) {
NewChannelView({ wantsToPost = false }, account = account)
NewChannelView({ wantsToPost = false }, accountViewModel = accountViewModel)
}
OutlinedButton(
@@ -3,8 +3,7 @@ package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.User
object HiddenAccountsFeedFilter : FeedFilter<User>() {
lateinit var account: Account
class HiddenAccountsFeedFilter(val account: Account) : FeedFilter<User>() {
override fun feed() = account.hiddenUsers()
}
@@ -4,18 +4,11 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
object UserProfileFollowersFeedFilter : FeedFilter<User>() {
lateinit var account: Account
var user: User? = null
fun loadUserProfile(accountLoggedIn: Account, user: User?) {
account = accountLoggedIn
this.user = user
}
class UserProfileFollowersFeedFilter(val user: User, val account: Account) : FeedFilter<User>() {
override fun feed(): List<User> {
return user?.let { myUser ->
LocalCache.users.values.filter { it.isFollowing(myUser) && account.isAcceptable(it) }
} ?: emptyList()
return user.let { myUser ->
LocalCache.users.values.filter { it.isFollowing(myUser) && !account.isHidden(it) }
}
}
}
@@ -3,21 +3,25 @@ package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.ContactListEvent
object UserProfileFollowsFeedFilter : FeedFilter<User>() {
lateinit var account: Account
var user: User? = null
class UserProfileFollowsFeedFilter(val user: User, val account: Account) : FeedFilter<User>() {
fun loadUserProfile(accountLoggedIn: Account, user: User?) {
account = accountLoggedIn
this.user = user
}
val cache: MutableMap<ContactListEvent, List<User>> = mutableMapOf()
override fun feed(): List<User> {
return user?.latestContactList?.unverifiedFollowKeySet()?.mapNotNull {
LocalCache.checkGetOrCreateUser(it)
}?.toSet()
?.filter { account.isAcceptable(it) }
val contactList = user.latestContactList ?: return emptyList()
val previousList = cache[contactList]
if (previousList != null) return previousList
cache[contactList] = user.latestContactList
?.unverifiedFollowKeySet()?.mapNotNull {
LocalCache.checkGetOrCreateUser(it)
}?.toSet()
?.filter { !account.isHidden(it) }
?.reversed() ?: emptyList()
return cache[contactList] ?: emptyList()
}
}
@@ -26,6 +26,8 @@ import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Logout
import androidx.compose.material.icons.filled.RadioButtonChecked
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
@@ -40,9 +42,11 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.vitorpamplona.amethyst.AccountInfo
import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.decodePublicKey
import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
@@ -52,6 +56,8 @@ import com.vitorpamplona.amethyst.ui.note.toShortenHex
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginPage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
fun AccountSwitchBottomSheet(
@@ -59,11 +65,6 @@ fun AccountSwitchBottomSheet(
accountStateViewModel: AccountStateViewModel
) {
val accounts = LocalPreferences.allSavedAccounts()
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
val accountUserState by account.userProfile().live().metadata.observeAsState()
val accountUser = accountUserState?.user ?: return
var popupExpanded by remember { mutableStateOf(false) }
val scrollState = rememberScrollState()
@@ -79,108 +80,7 @@ fun AccountSwitchBottomSheet(
Text(stringResource(R.string.account_switch_select_account), fontWeight = FontWeight.Bold)
}
accounts.forEach { acc ->
val current = accountUser.pubkeyNpub() == acc.npub
val baseUser = try {
LocalCache.getOrCreateUser(decodePublicKey(acc.npub).toHexKey())
} catch (e: Exception) {
null
}
if (baseUser != null) {
val userState by baseUser.live().metadata.observeAsState()
val user = userState?.user ?: return
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Row(
modifier = Modifier
.weight(1f)
.clickable {
accountStateViewModel.switchUser(acc.npub)
},
verticalAlignment = Alignment.CenterVertically
) {
Row(
modifier = Modifier
.padding(16.dp, 16.dp)
.weight(1f),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.width(55.dp)
.padding(0.dp)
) {
RobohashAsyncImageProxy(
robot = user.pubkeyHex,
model = ResizeImage(user.profilePicture(), 55.dp),
contentDescription = stringResource(R.string.profile_image),
modifier = Modifier
.width(55.dp)
.height(55.dp)
.clip(shape = CircleShape)
)/*
Box(
modifier = Modifier
.size(20.dp)
.align(Alignment.TopEnd)
) {
if (acc.hasPrivKey) {
Icon(
imageVector = Icons.Default.Key,
contentDescription = stringResource(R.string.account_switch_has_private_key),
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colors.primary
)
} else {
Icon(
imageVector = Icons.Default.Visibility,
contentDescription = stringResource(R.string.account_switch_pubkey_only),
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colors.primary
)
}
}*/
}
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
val npubShortHex = acc.npub.toShortenHex()
user.bestDisplayName()?.let {
CreateTextWithEmoji(
text = it,
tags = user.info?.latestMetadata?.tags
)
}
Text(npubShortHex)
}
Column(modifier = Modifier.width(32.dp)) {
if (current) {
Icon(
imageVector = Icons.Default.RadioButtonChecked,
contentDescription = stringResource(R.string.account_switch_active_account),
tint = MaterialTheme.colors.secondary
)
}
}
}
}
IconButton(
onClick = { accountStateViewModel.logOff(acc.npub) }
) {
Icon(
imageVector = Icons.Default.Logout,
contentDescription = stringResource(R.string.log_out),
tint = MaterialTheme.colors.onSurface
)
}
}
}
DisplayAccount(acc, accountViewModel, accountStateViewModel)
}
Row(
modifier = Modifier
@@ -222,3 +122,144 @@ fun AccountSwitchBottomSheet(
}
}
}
@Composable
fun DisplayAccount(
acc: AccountInfo,
accountViewModel: AccountViewModel,
accountStateViewModel: AccountStateViewModel
) {
var baseUser by remember { mutableStateOf<User?>(null) }
LaunchedEffect(key1 = acc.npub) {
launch(Dispatchers.IO) {
baseUser = try {
LocalCache.getOrCreateUser(decodePublicKey(acc.npub).toHexKey())
} catch (e: Exception) {
null
}
}
}
baseUser?.let {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Row(
modifier = Modifier
.weight(1f)
.clickable {
accountStateViewModel.switchUser(acc.npub)
},
verticalAlignment = Alignment.CenterVertically
) {
Row(
modifier = Modifier
.padding(16.dp, 16.dp)
.weight(1f),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.width(55.dp)
.padding(0.dp)
) {
AccountPicture(it)
}
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
AccountName(acc, it)
}
Column(modifier = Modifier.width(32.dp)) {
ActiveMarker(acc, accountViewModel)
}
}
}
LogoutButton(acc, accountStateViewModel)
}
}
}
@Composable
private fun ActiveMarker(acc: AccountInfo, accountViewModel: AccountViewModel) {
val isCurrentUser by remember(accountViewModel) {
derivedStateOf {
accountViewModel.account.userProfile().pubkeyNpub() == acc.npub
}
}
if (isCurrentUser) {
Icon(
imageVector = Icons.Default.RadioButtonChecked,
contentDescription = stringResource(R.string.account_switch_active_account),
tint = MaterialTheme.colors.secondary
)
}
}
@Composable
private fun AccountPicture(user: User) {
val userState by user.live().metadata.observeAsState()
val profilePicture by remember(userState) {
derivedStateOf {
ResizeImage(userState?.user?.profilePicture(), 55.dp)
}
}
RobohashAsyncImageProxy(
robot = remember(user) { user.pubkeyHex },
model = profilePicture,
contentDescription = stringResource(R.string.profile_image),
modifier = Modifier
.width(55.dp)
.height(55.dp)
.clip(shape = CircleShape)
)
}
@Composable
private fun AccountName(
acc: AccountInfo,
user: User
) {
val userState by user.live().metadata.observeAsState()
val displayName by remember(userState) {
derivedStateOf {
user.bestDisplayName()
}
}
val tags by remember(userState) {
derivedStateOf {
user.info?.latestMetadata?.tags
}
}
displayName?.let {
CreateTextWithEmoji(
text = it,
tags = tags
)
}
Text(
text = remember(user) { acc.npub.toShortenHex() }
)
}
@Composable
private fun LogoutButton(
acc: AccountInfo,
accountStateViewModel: AccountStateViewModel
) {
IconButton(
onClick = { accountStateViewModel.logOff(acc.npub) }
) {
Icon(
imageVector = Icons.Default.Logout,
contentDescription = stringResource(R.string.log_out),
tint = MaterialTheme.colors.onSurface
)
}
}
@@ -160,10 +160,6 @@ fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel)
@OptIn(coil.annotation.ExperimentalCoilApi::class)
@Composable
fun GenericTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, content: @Composable (AccountViewModel) -> Unit) {
val relayViewModel: RelayPoolViewModel = viewModel { RelayPoolViewModel() }
val connectedRelaysLiveData by relayViewModel.connectedRelaysLiveData.observeAsState()
val availableRelaysLiveData by relayViewModel.availableRelaysLiveData.observeAsState()
val coroutineScope = rememberCoroutineScope()
var wantsToEditRelays by remember {
@@ -195,29 +191,9 @@ fun GenericTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewMod
content(accountViewModel)
}
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(),
horizontalAlignment = Alignment.End
) {
Row(
modifier = Modifier.fillMaxHeight(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
"${connectedRelaysLiveData ?: "--"}/${availableRelaysLiveData ?: "--"}",
color = if (connectedRelaysLiveData == 0) Color.Red else MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
style = MaterialTheme.typography.subtitle1,
modifier = Modifier.clickable(
onClick = {
wantsToEditRelays = true
}
)
)
}
}
RelayStatus(
{ wantsToEditRelays = true }
)
}
}
},
@@ -229,38 +205,87 @@ fun GenericTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewMod
}
},
actions = {
IconButton(
onClick = { wantsToEditRelays = true },
modifier = Modifier
) {
Icon(
painter = painterResource(R.drawable.relays),
null,
modifier = Modifier.size(24.dp),
tint = Color.Unspecified
)
}
RelayIcon { wantsToEditRelays = true }
}
)
Divider(thickness = 0.25.dp)
}
}
@Composable
private fun RelayStatus(
onClick: () -> Unit
) {
val relayViewModel: RelayPoolViewModel = viewModel { RelayPoolViewModel() }
val connectedRelaysLiveData = relayViewModel.connectedRelaysLiveData.observeAsState()
val availableRelaysLiveData = relayViewModel.availableRelaysLiveData.observeAsState()
val connectedRelaysText by remember(connectedRelaysLiveData, availableRelaysLiveData) {
derivedStateOf {
"${connectedRelaysLiveData.value ?: "--"}/${availableRelaysLiveData.value ?: "--"}"
}
}
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(),
horizontalAlignment = Alignment.End
) {
Row(
modifier = Modifier.fillMaxHeight(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = connectedRelaysText,
color = if (connectedRelaysLiveData.value == 0) {
Color.Red
} else {
MaterialTheme.colors.onSurface.copy(
alpha = 0.32f
)
},
style = MaterialTheme.typography.subtitle1,
modifier = Modifier.clickable(
onClick = onClick
)
)
}
}
}
@Composable
private fun RelayIcon(onClick: () -> Unit) {
IconButton(
onClick = onClick,
modifier = Modifier
) {
Icon(
painter = painterResource(R.drawable.relays),
null,
modifier = Modifier.size(24.dp),
tint = Color.Unspecified
)
}
}
@Composable
private fun LoggedInUserPictureDrawer(
accountViewModel: AccountViewModel,
onClick: () -> Unit
) {
val accountUserState by accountViewModel.account.userProfile().live().metadata.observeAsState()
val accountUser = accountUserState?.user ?: return
val pubkeyHex = remember { accountUserState?.user?.pubkeyHex ?: "" }
val profilePicture = remember(accountUserState) { ResizeImage(accountUserState?.user?.profilePicture(), 34.dp) }
IconButton(
onClick = onClick,
modifier = Modifier
) {
RobohashAsyncImageProxy(
robot = remember { accountUser.pubkeyHex },
model = remember(accountUser) { ResizeImage(accountUser.profilePicture(), 34.dp) },
robot = pubkeyHex,
model = profilePicture,
contentDescription = stringResource(id = R.string.profile_image),
modifier = Modifier
.width(34.dp)
@@ -32,6 +32,7 @@ import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
@@ -64,6 +65,7 @@ import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountBackupDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ConnectOrbotDialog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class)
@@ -132,10 +134,6 @@ fun ProfileContent(
val tags = remember(accountUserState) { accountUserState?.user?.info?.latestMetadata?.tags }
val route = remember(accountUserState) { "User/${accountUserState?.user?.pubkeyHex}" }
val accountUserFollowsState by baseAccountUser.live().follows.observeAsState()
val followingCount = remember(accountUserFollowsState) { accountUserFollowsState?.user?.cachedFollowCount()?.toString() ?: "--" }
val followerCount = remember(accountUserFollowsState) { accountUserFollowsState?.user?.cachedFollowerCount()?.toString() ?: "--" }
Box {
if (profileBanner != null) {
AsyncImage(
@@ -221,25 +219,49 @@ fun ProfileContent(
}
})
) {
Row() {
Text(
text = followingCount,
fontWeight = FontWeight.Bold
)
Text(stringResource(R.string.following))
}
Row(modifier = Modifier.padding(start = 10.dp)) {
Text(
text = followerCount,
fontWeight = FontWeight.Bold
)
Text(stringResource(R.string.followers))
}
FollowingAndFollowerCounts(baseAccountUser)
}
}
}
}
@Composable
private fun FollowingAndFollowerCounts(baseAccountUser: User) {
val accountUserFollowsState by baseAccountUser.live().follows.observeAsState()
var followingCount by remember { mutableStateOf("--") }
var followerCount by remember { mutableStateOf("--") }
LaunchedEffect(key1 = accountUserFollowsState) {
launch(Dispatchers.IO) {
val newFollowing = accountUserFollowsState?.user?.cachedFollowCount()?.toString() ?: "--"
val newFollower = accountUserFollowsState?.user?.cachedFollowerCount()?.toString() ?: "--"
if (followingCount != newFollowing) {
followingCount = newFollowing
}
if (followerCount != newFollower) {
followerCount = newFollower
}
}
}
Row() {
Text(
text = followingCount,
fontWeight = FontWeight.Bold
)
Text(stringResource(R.string.following))
}
Row(modifier = Modifier.padding(start = 10.dp)) {
Text(
text = followerCount,
fontWeight = FontWeight.Bold
)
Text(stringResource(R.string.followers))
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun ListContent(
@@ -257,7 +279,11 @@ fun ListContent(
var conectOrbotDialogOpen by remember { mutableStateOf(false) }
var proxyPort = remember { mutableStateOf(account.proxyPort.toString()) }
Column(modifier = modifier.fillMaxHeight().verticalScroll(rememberScrollState())) {
Column(
modifier = modifier
.fillMaxHeight()
.verticalScroll(rememberScrollState())
) {
if (accountUserPubKey != null) {
NavigationRow(
title = stringResource(R.string.profile),
@@ -2,7 +2,6 @@ package com.vitorpamplona.amethyst.ui.note
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -16,14 +15,11 @@ import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
@@ -38,6 +34,7 @@ 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.screen.loggedIn.AccountViewModel
class AddBountyAmountViewModel : ViewModel() {
private var account: Account? = null
@@ -75,16 +72,10 @@ class AddBountyAmountViewModel : ViewModel() {
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun AddBountyAmountDialog(bounty: Note, account: Account, onClose: () -> Unit) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
fun AddBountyAmountDialog(bounty: Note, accountViewModel: AccountViewModel, onClose: () -> Unit) {
val postViewModel: AddBountyAmountViewModel = viewModel()
LaunchedEffect(account) {
postViewModel.load(account, bounty)
}
postViewModel.load(accountViewModel.account, bounty)
Dialog(
onDismissRequest = { onClose() },
@@ -20,7 +20,7 @@ import androidx.compose.ui.unit.dp
import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
fun BlankNote(modifier: Modifier = Modifier, isQuote: Boolean = false, idHex: String? = null) {
@@ -54,7 +54,14 @@ fun BlankNote(modifier: Modifier = Modifier, isQuote: Boolean = false, idHex: St
}
@Composable
fun HiddenNote(reports: Set<Note>, loggedIn: User, modifier: Modifier = Modifier, isQuote: Boolean = false, nav: (String) -> Unit, onClick: () -> Unit) {
fun HiddenNote(
reports: Set<Note>,
accountViewModel: AccountViewModel,
modifier: Modifier = Modifier,
isQuote: Boolean = false,
nav: (String) -> Unit,
onClick: () -> Unit
) {
Column(modifier = modifier) {
Row(modifier = Modifier.padding(horizontal = if (!isQuote) 12.dp else 6.dp)) {
Column(modifier = Modifier.padding(start = if (!isQuote) 10.dp else 5.dp)) {
@@ -75,7 +82,7 @@ fun HiddenNote(reports: Set<Note>, loggedIn: User, modifier: Modifier = Modifier
NoteAuthorPicture(
baseNote = it,
nav = nav,
userAccount = loggedIn,
accountViewModel = accountViewModel,
size = 35.dp
)
}
@@ -114,7 +114,7 @@ fun BoostSetCompose(boostSetCard: BoostSetCard, isInnerNote: Boolean = false, ro
NoteAuthorPicture(
baseNote = it,
nav = nav,
userAccount = account.userProfile(),
accountViewModel = accountViewModel,
size = 35.dp
)
}
@@ -163,7 +163,7 @@ fun ChatroomCompose(
channelPicture = {
UserPicture(
userToComposeOn,
accountViewModel.userProfile(),
accountViewModel = accountViewModel,
size = 55.dp
)
},
@@ -133,7 +133,7 @@ fun ChatroomMessageCompose(
if (!isAcceptableAndCanPreview.first && !showHiddenNote) {
HiddenNote(
account.getRelevantReports(noteForReports),
account.userProfile(),
accountViewModel,
Modifier,
innerQuote,
nav,
@@ -114,7 +114,7 @@ fun LikeSetCompose(likeSetCard: LikeSetCard, isInnerNote: Boolean = false, route
NoteAuthorPicture(
baseNote = it,
nav = nav,
userAccount = account.userProfile(),
accountViewModel = accountViewModel,
size = 35.dp
)
}
@@ -277,10 +277,9 @@ private fun AuthorPictureAndComment(
accountViewModel: AccountViewModel
) {
var content by remember { mutableStateOf<Triple<User?, String?, String?>>(Triple(zapRequest.author, null, null)) }
val scope = rememberCoroutineScope()
LaunchedEffect(key1 = zapRequest.idHex, key2 = zapEvent?.idHex) {
scope.launch(Dispatchers.Default) {
launch(Dispatchers.Default) {
(zapRequest.event as? LnZapRequestEvent)?.let {
val decryptedContent = accountViewModel.decryptZap(zapRequest)
val amount = (zapEvent?.event as? LnZapEvent)?.amount
@@ -438,22 +437,11 @@ fun FastNoteAuthorPicture(
author.pubkeyHex
}
val accountState by accountViewModel.accountLiveData.observeAsState()
val loggedInLiveFollows = remember(accountState) { accountState?.account?.userProfile()?.live()?.follows } ?: return
val accountFollowsState by loggedInLiveFollows.observeAsState()
val showFollowingMark by remember(accountFollowsState) {
derivedStateOf {
accountFollowsState?.user?.isFollowingCached(author) == true || (author.pubkeyHex == accountFollowsState?.user?.pubkeyHex)
}
}
UserPicture(
userHex = authorPubKey,
userPicture = profilePicture,
showFollowingMark = showFollowingMark,
size = size,
modifier = pictureModifier
modifier = pictureModifier,
accountViewModel = accountViewModel
)
}
@@ -55,6 +55,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Alignment.Companion.TopEnd
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
@@ -82,7 +83,6 @@ import coil.request.SuccessResult
import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
@@ -286,7 +286,7 @@ fun LoadedNoteCompose(
if (showHiddenNote) {
HiddenNote(
state.relevantReports,
accountViewModel.userProfile(),
accountViewModel,
modifier,
isBoostedNote,
nav,
@@ -332,17 +332,13 @@ fun NormalNote(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = remember(accountState) { accountState?.account } ?: return
val loggedIn = remember(accountState) { accountState?.account?.userProfile() } ?: return
val noteEvent = remember { baseNote.event }
val channelHex = remember { baseNote.channelHex() }
var popupExpanded by remember { mutableStateOf(false) }
if ((noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) && channelHex != null) {
ChannelHeader(channelHex = channelHex, account = account, nav = nav)
ChannelHeader(channelHex = channelHex, accountViewModel = accountViewModel, nav = nav)
} else if (noteEvent is BadgeDefinitionEvent) {
BadgeDisplay(baseNote = baseNote)
} else if (noteEvent is FileHeaderEvent) {
@@ -392,7 +388,7 @@ fun NormalNote(
.combinedClickable(
onClick = {
scope.launch {
routeFor(baseNote, loggedIn)?.let {
routeFor(baseNote, accountViewModel.userProfile())?.let {
nav(it)
}
}
@@ -414,7 +410,7 @@ fun NormalNote(
}
) {
if (!isBoostedNote && !isQuotedNote) {
DrawAuthorImages(baseNote, loggedIn, nav)
DrawAuthorImages(baseNote, accountViewModel, nav)
}
Column(
@@ -426,7 +422,6 @@ fun NormalNote(
FirstUserInfoRow(
baseNote = baseNote,
showAuthorPicture = isQuotedNote,
account = account,
accountViewModel = accountViewModel,
nav = nav
)
@@ -434,7 +429,7 @@ fun NormalNote(
if (noteEvent !is RepostEvent && !makeItShort && !isQuotedNote) {
SecondUserInfoRow(
baseNote,
account,
accountViewModel,
nav
)
}
@@ -446,7 +441,6 @@ fun NormalNote(
baseNote,
unPackReply,
backgroundColor,
account,
accountViewModel,
nav
)
@@ -466,7 +460,7 @@ fun NormalNote(
}
is LongTextNoteEvent -> {
RenderLongFormContent(baseNote, loggedIn, accountViewModel, nav)
RenderLongFormContent(baseNote, accountViewModel, nav)
}
is BadgeAwardEvent -> {
@@ -478,7 +472,7 @@ fun NormalNote(
}
is AudioTrackEvent -> {
RenderAudioTrack(baseNote, loggedIn, accountViewModel, nav)
RenderAudioTrack(baseNote, accountViewModel, nav)
}
is PinListEvent -> {
@@ -812,7 +806,7 @@ fun DisplayPeopleList(
)
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
launch(Dispatchers.IO) {
members = noteEvent.bookmarkedPeople().mapNotNull { hex ->
LocalCache.checkGetOrCreateUser(hex)
}.sortedBy { account.isFollowing(it) }.reversed()
@@ -883,7 +877,7 @@ private fun RenderBadgeAward(
Text(text = stringResource(R.string.award_granted_to))
LaunchedEffect(key1 = note) {
withContext(Dispatchers.IO) {
launch(Dispatchers.IO) {
awardees = noteEvent.awardees().mapNotNull { hex ->
LocalCache.checkGetOrCreateUser(hex)
}.sortedBy { account.isFollowing(it) }.reversed()
@@ -902,7 +896,7 @@ private fun RenderBadgeAward(
) {
UserPicture(
baseUser = user,
baseUserAccount = accountViewModel.userProfile(),
accountViewModel = accountViewModel,
size = 35.dp
)
}
@@ -1098,13 +1092,12 @@ fun PinListHeader(
@Composable
private fun RenderAudioTrack(
note: Note,
loggedIn: User,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val noteEvent = note.event as? AudioTrackEvent ?: return
AudioTrackHeader(noteEvent, note, loggedIn, nav)
AudioTrackHeader(noteEvent, accountViewModel, nav)
ReactionsRow(note, accountViewModel, nav)
@@ -1117,13 +1110,12 @@ private fun RenderAudioTrack(
@Composable
private fun RenderLongFormContent(
note: Note,
loggedIn: User,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val noteEvent = note.event as? LongTextNoteEvent ?: return
LongFormHeader(noteEvent, note, loggedIn)
LongFormHeader(noteEvent, note, accountViewModel)
ReactionsRow(note, accountViewModel, nav)
@@ -1205,7 +1197,6 @@ private fun ReplyRow(
note: Note,
unPackReply: Boolean,
backgroundColor: Color,
account: Account,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
@@ -1236,13 +1227,13 @@ private fun ReplyRow(
nav = nav
)
} else {
ReplyInformation(note.replyTo, noteEvent.mentions(), account, nav)
ReplyInformation(note.replyTo, noteEvent.mentions(), accountViewModel, nav)
}
Spacer(modifier = Modifier.height(5.dp))
} else if (noteEvent is ChannelMessageEvent && (note.replyTo != null || noteEvent.hasAnyTaggedUser())) {
note.channel()?.let {
ReplyInformationChannel(note.replyTo, noteEvent.mentions(), it, account, nav)
ReplyInformationChannel(note.replyTo, noteEvent.mentions(), it, accountViewModel, nav)
}
Spacer(modifier = Modifier.height(5.dp))
@@ -1252,7 +1243,7 @@ private fun ReplyRow(
@Composable
private fun SecondUserInfoRow(
note: Note,
account: Account,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val noteEvent = remember { note.event } ?: return
@@ -1263,7 +1254,7 @@ private fun SecondUserInfoRow(
val baseReward = remember { noteEvent.getReward() }
if (baseReward != null) {
DisplayReward(baseReward, note, account, nav)
DisplayReward(baseReward, note, accountViewModel, nav)
}
val pow = remember { noteEvent.getPoWRank() }
@@ -1277,21 +1268,19 @@ private fun SecondUserInfoRow(
private fun FirstUserInfoRow(
baseNote: Note,
showAuthorPicture: Boolean,
account: Account,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
var moreActionsExpanded by remember { mutableStateOf(false) }
val eventNote = remember { baseNote.event } ?: return
val time = remember { baseNote.createdAt() } ?: return
val loggedIn = remember { account.userProfile() }
val padding = remember {
Modifier.padding(horizontal = 5.dp)
}
Row(verticalAlignment = Alignment.CenterVertically) {
if (showAuthorPicture) {
NoteAuthorPicture(baseNote, nav, loggedIn, 25.dp)
NoteAuthorPicture(baseNote, nav, accountViewModel, 25.dp)
Spacer(padding)
NoteUsernameDisplay(baseNote, Modifier.weight(1f))
} else {
@@ -1305,7 +1294,7 @@ private fun FirstUserInfoRow(
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
} else {
DisplayFollowingHashtagsInPost(eventNote, account, nav)
DisplayFollowingHashtagsInPost(eventNote, accountViewModel, nav)
}
TimeAgo(time)
@@ -1353,17 +1342,17 @@ fun TimeAgo(time: Long) {
}
@Composable
private fun DrawAuthorImages(baseNote: Note, loggedIn: User, nav: (String) -> Unit) {
private fun DrawAuthorImages(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val baseChannel = remember { baseNote.channel() }
val modifier = remember { Modifier.width(55.dp) }
Column(modifier) {
// Draws the boosted picture outside the boosted card.
Box(modifier = modifier, contentAlignment = Alignment.BottomEnd) {
NoteAuthorPicture(baseNote, nav, loggedIn, 55.dp)
NoteAuthorPicture(baseNote, nav, accountViewModel, 55.dp)
if (baseNote.event is RepostEvent) {
RepostNoteAuthorPicture(baseNote, nav, loggedIn)
RepostNoteAuthorPicture(baseNote, accountViewModel, nav)
}
if (baseNote.event is ChannelMessageEvent && baseChannel != null) {
@@ -1427,8 +1416,8 @@ private fun ChannelNotePicture(baseChannel: Channel) {
@Composable
private fun RepostNoteAuthorPicture(
baseNote: Note,
nav: (String) -> Unit,
loggedIn: User
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val baseRepost = remember { baseNote.replyTo?.lastOrNull() }
@@ -1441,10 +1430,10 @@ private fun RepostNoteAuthorPicture(
baseRepost?.let {
Box(modifier) {
NoteAuthorPicture(
it,
nav,
loggedIn,
35.dp,
baseNote = it,
nav = nav,
accountViewModel = accountViewModel,
size = 35.dp,
pictureModifier = Modifier.border(
2.dp,
MaterialTheme.colors.background,
@@ -1535,23 +1524,31 @@ fun DisplayHighlight(
@Composable
fun DisplayFollowingHashtagsInPost(
noteEvent: EventInterface,
account: Account,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val followingTags = remember(accountState) {
accountState?.account?.followingTagSet() ?: emptySet()
}
var firstTag by remember { mutableStateOf<String?>(null) }
LaunchedEffect(key1 = noteEvent.id()) {
LaunchedEffect(key1 = noteEvent.id(), followingTags) {
withContext(Dispatchers.IO) {
firstTag = noteEvent.firstIsTaggedHashes(account.followingTagSet())
firstTag = noteEvent.firstIsTaggedHashes(followingTags)
}
}
Column() {
Row(verticalAlignment = Alignment.CenterVertically) {
firstTag?.let {
val displayTag = remember(firstTag) { AnnotatedString(" #$firstTag") }
val route = remember(firstTag) { "Hashtag/$firstTag" }
ClickableText(
text = AnnotatedString(" #$firstTag"),
onClick = { nav("Hashtag/$firstTag") },
text = displayTag,
onClick = { nav(route) },
style = LocalTextStyle.current.copy(
color = MaterialTheme.colors.primary.copy(
alpha = 0.52f
@@ -1608,7 +1605,7 @@ fun DisplayPoW(
fun DisplayReward(
baseReward: BigDecimal,
baseNote: Note,
account: Account,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
var popupExpanded by remember { mutableStateOf(false) }
@@ -1628,53 +1625,67 @@ fun DisplayReward(
)
)
val repliesState by baseNote.live().replies.observeAsState()
val replyNote = repliesState?.note
if (replyNote?.hasPledgeBy(account.userProfile()) == true) {
Icon(
imageVector = Icons.Default.Bolt,
contentDescription = stringResource(R.string.zaps),
modifier = Modifier.size(20.dp),
tint = BitcoinOrange
)
} else {
Icon(
imageVector = Icons.Default.Bolt,
contentDescription = stringResource(R.string.zaps),
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
}
var rewardAmount by remember {
mutableStateOf<BigDecimal?>(
baseReward
)
}
LaunchedEffect(key1 = repliesState) {
withContext(Dispatchers.IO) {
replyNote?.pledgedAmountByOthers()?.let {
rewardAmount = baseReward.add(it)
}
}
}
Text(
showAmount(rewardAmount),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
RenderPledgeAmount(baseNote, baseReward, accountViewModel)
}
if (popupExpanded) {
AddBountyAmountDialog(baseNote, account) {
AddBountyAmountDialog(baseNote, accountViewModel) {
popupExpanded = false
}
}
}
}
@Composable
private fun RenderPledgeAmount(
baseNote: Note,
baseReward: BigDecimal,
accountViewModel: AccountViewModel
) {
val repliesState by baseNote.live().replies.observeAsState()
var rewardAmount by remember {
mutableStateOf<BigDecimal?>(
baseReward
)
}
var hasPledge by remember {
mutableStateOf<Boolean>(
false
)
}
LaunchedEffect(key1 = repliesState) {
withContext(Dispatchers.IO) {
repliesState?.note?.pledgedAmountByOthers()?.let {
rewardAmount = baseReward.add(it)
}
hasPledge = repliesState?.note?.hasPledgeBy(accountViewModel.userProfile()) == true
}
}
if (hasPledge) {
Icon(
imageVector = Icons.Default.Bolt,
contentDescription = stringResource(R.string.zaps),
modifier = Modifier.size(20.dp),
tint = BitcoinOrange
)
} else {
Icon(
imageVector = Icons.Default.Bolt,
contentDescription = stringResource(R.string.zaps),
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
}
Text(
showAmount(rewardAmount),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
}
@Composable
fun BadgeDisplay(baseNote: Note) {
val background = MaterialTheme.colors.background
@@ -1868,7 +1879,7 @@ fun FileStorageHeaderDisplay(baseNote: Note) {
}
@Composable
fun AudioTrackHeader(noteEvent: AudioTrackEvent, note: Note, loggedIn: User, nav: (String) -> Unit) {
fun AudioTrackHeader(noteEvent: AudioTrackEvent, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val media = remember { noteEvent.media() }
val cover = remember { noteEvent.cover() }
val subject = remember { noteEvent.subject() }
@@ -1908,7 +1919,7 @@ fun AudioTrackHeader(noteEvent: AudioTrackEvent, note: Note, loggedIn: User, nav
nav("User/${it.second.pubkeyHex}")
}
) {
UserPicture(it.second, loggedIn, 25.dp)
UserPicture(it.second, 25.dp, accountViewModel)
Spacer(Modifier.width(5.dp))
UsernameDisplay(it.second, Modifier.weight(1f))
Spacer(Modifier.width(5.dp))
@@ -1945,7 +1956,11 @@ fun AudioTrackHeader(noteEvent: AudioTrackEvent, note: Note, loggedIn: User, nav
}
@Composable
private fun LongFormHeader(noteEvent: LongTextNoteEvent, note: Note, loggedIn: User) {
private fun LongFormHeader(noteEvent: LongTextNoteEvent, note: Note, accountViewModel: AccountViewModel) {
val image = remember(noteEvent) { noteEvent.image() }
val title = remember(noteEvent) { noteEvent.title() }
val summary = remember(noteEvent) { noteEvent.summary() ?: noteEvent.content.take(200).ifBlank { null } }
Row(
modifier = Modifier
.clip(shape = RoundedCornerShape(15.dp))
@@ -1956,7 +1971,7 @@ private fun LongFormHeader(noteEvent: LongTextNoteEvent, note: Note, loggedIn: U
)
) {
Column {
noteEvent.image()?.let {
image?.let {
AsyncImage(
model = it,
contentDescription = stringResource(
@@ -1966,38 +1981,9 @@ private fun LongFormHeader(noteEvent: LongTextNoteEvent, note: Note, loggedIn: U
contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth()
)
} ?: Box() {
note.author?.info?.banner?.let {
AsyncImage(
model = it,
contentDescription = stringResource(
R.string.preview_card_image_for,
it
),
contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth()
)
} ?: Image(
painter = painterResource(R.drawable.profile_banner),
contentDescription = stringResource(R.string.profile_banner),
contentScale = ContentScale.FillWidth,
modifier = Modifier
.fillMaxWidth()
.height(150.dp)
)
} ?: CreateImageHeader(note, accountViewModel)
Box(
Modifier
.width(75.dp)
.height(75.dp)
.padding(10.dp)
.align(Alignment.BottomStart)
) {
NoteAuthorPicture(baseNote = note, baseUserAccount = loggedIn, size = 55.dp)
}
}
noteEvent.title()?.let {
title?.let {
Text(
text = it,
style = MaterialTheme.typography.body1,
@@ -2007,7 +1993,7 @@ private fun LongFormHeader(noteEvent: LongTextNoteEvent, note: Note, loggedIn: U
)
}
noteEvent.summary()?.let {
summary?.let {
Text(
text = it,
style = MaterialTheme.typography.caption,
@@ -2019,16 +2005,45 @@ private fun LongFormHeader(noteEvent: LongTextNoteEvent, note: Note, loggedIn: U
overflow = TextOverflow.Ellipsis
)
}
?: Text(
text = noteEvent.content.take(200),
style = MaterialTheme.typography.caption,
modifier = Modifier
.fillMaxWidth()
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp),
color = Color.Gray,
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
}
}
}
@Composable
private fun CreateImageHeader(
note: Note,
accountViewModel: AccountViewModel
) {
val banner = remember(note.author?.info) { note.author?.info?.banner }
Box() {
banner?.let {
AsyncImage(
model = it,
contentDescription = stringResource(
R.string.preview_card_image_for,
it
),
contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth()
)
} ?: Image(
painter = painterResource(R.drawable.profile_banner),
contentDescription = stringResource(R.string.profile_banner),
contentScale = ContentScale.FillWidth,
modifier = Modifier
.fillMaxWidth()
.height(150.dp)
)
Box(
Modifier
.width(75.dp)
.height(75.dp)
.padding(10.dp)
.align(Alignment.BottomStart)
) {
NoteAuthorPicture(baseNote = note, accountViewModel = accountViewModel, size = 55.dp)
}
}
}
@@ -2120,11 +2135,11 @@ private fun ShowMoreRelaysButton(onClick: () -> Unit) {
fun NoteAuthorPicture(
baseNote: Note,
nav: (String) -> Unit,
userAccount: User,
accountViewModel: AccountViewModel,
size: Dp,
pictureModifier: Modifier = Modifier
) {
NoteAuthorPicture(baseNote, userAccount, size, pictureModifier) {
NoteAuthorPicture(baseNote, size, accountViewModel, pictureModifier) {
nav("User/${it.pubkeyHex}")
}
}
@@ -2132,8 +2147,8 @@ fun NoteAuthorPicture(
@Composable
fun NoteAuthorPicture(
baseNote: Note,
baseUserAccount: User,
size: Dp,
accountViewModel: AccountViewModel,
modifier: Modifier = Modifier,
onClick: ((User) -> Unit)? = null
) {
@@ -2164,7 +2179,7 @@ fun NoteAuthorPicture(
modifier = nullModifier.background(MaterialTheme.colors.background)
)
} else {
UserPicture(author, baseUserAccount, size, modifier, onClick)
UserPicture(author, size, accountViewModel, modifier, onClick)
}
}
}
@@ -2173,11 +2188,11 @@ fun NoteAuthorPicture(
fun UserPicture(
user: User,
nav: (String) -> Unit,
userAccount: User,
accountViewModel: AccountViewModel,
size: Dp,
pictureModifier: Modifier = Modifier
) {
UserPicture(user, userAccount, size, pictureModifier) {
UserPicture(user, size, accountViewModel, pictureModifier) {
nav("User/${it.pubkeyHex}")
}
}
@@ -2186,14 +2201,13 @@ fun UserPicture(
@Composable
fun UserPicture(
baseUser: User,
baseUserAccount: User,
size: Dp,
accountViewModel: AccountViewModel,
modifier: Modifier = remember { Modifier },
onClick: ((User) -> Unit)? = null,
onLongClick: ((User) -> Unit)? = null
) {
val userState by baseUser.live().metadata.observeAsState()
val accountState by baseUserAccount.live().follows.observeAsState()
val userPubkey = remember {
baseUser.pubkeyHex
@@ -2205,12 +2219,6 @@ fun UserPicture(
}
}
val showFollowingMark by remember(accountState) {
derivedStateOf {
accountState?.user?.isFollowingCached(baseUser) == true || baseUser.pubkeyHex == accountState?.user?.pubkeyHex
}
}
// BaseUser is the same reference as accountState.user
val myModifier = remember {
if (onClick != null && onLongClick != null) {
@@ -2229,9 +2237,9 @@ fun UserPicture(
UserPicture(
userHex = userPubkey,
userPicture = userProfile,
showFollowingMark = showFollowingMark,
size = size,
modifier = modifier
modifier = modifier,
accountViewModel = accountViewModel
)
}
}
@@ -2240,9 +2248,9 @@ fun UserPicture(
fun UserPicture(
userHex: String,
userPicture: String?,
showFollowingMark: Boolean,
size: Dp,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
accountViewModel: AccountViewModel
) {
val myBoxModifier = remember {
Modifier
@@ -2257,36 +2265,42 @@ fun UserPicture(
.clip(shape = CircleShape)
}
val myResizeImage = remember(userPicture) {
ResizeImage(userPicture, size)
}
Box(myBoxModifier) {
Box(myBoxModifier, contentAlignment = TopEnd) {
RobohashAsyncImageProxy(
robot = userHex,
model = myResizeImage,
model = remember(userPicture) {
ResizeImage(userPicture, size)
},
contentDescription = stringResource(id = R.string.profile_image),
modifier = myImageModifier.background(MaterialTheme.colors.background)
)
if (showFollowingMark) {
val myIconBoxModifier = remember {
Modifier
.width(size.div(3.5f))
.height(size.div(3.5f))
.align(Alignment.TopEnd)
}
FollowingIcon(myIconBoxModifier, size)
}
ObserveAndDisplayFollowingMark(userHex, size.div(3.5f), accountViewModel)
}
}
@Composable
private fun FollowingIcon(
myIconBoxModifier: Modifier,
size: Dp
) {
private fun ObserveAndDisplayFollowingMark(userHex: String, iconSize: Dp, accountViewModel: AccountViewModel) {
val accountFollowsState by accountViewModel.account.userProfile().live().follows.observeAsState()
val showFollowingMark by remember(accountFollowsState) {
derivedStateOf {
accountFollowsState?.user?.isFollowingCached(userHex) == true ||
(userHex == accountViewModel.account.userProfile().pubkeyHex)
}
}
if (showFollowingMark) {
FollowingIcon(iconSize)
}
}
@Composable
private fun FollowingIcon(iconSize: Dp) {
val myIconBoxModifier = remember {
Modifier.size(iconSize)
}
Box(myIconBoxModifier, contentAlignment = Alignment.Center) {
val myIconBackgroundModifier = remember {
Modifier
@@ -2299,9 +2313,7 @@ private fun FollowingIcon(
)
val myIconModifier = remember {
Modifier
.width(size.div(3.5f))
.height(size.div(3.5f))
Modifier.size(iconSize)
}
Icon(
@@ -18,11 +18,17 @@ import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.*
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
fun ReplyInformation(replyTo: List<Note>?, mentions: List<String>, account: Account, nav: (String) -> Unit) {
fun ReplyInformation(
replyTo: List<Note>?,
mentions: List<String>,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
var dupMentions by remember { mutableStateOf<List<User>?>(null) }
LaunchedEffect(Unit) {
@@ -32,15 +38,21 @@ fun ReplyInformation(replyTo: List<Note>?, mentions: List<String>, account: Acco
}
if (dupMentions != null) {
ReplyInformation(replyTo, dupMentions, account) {
ReplyInformation(replyTo, dupMentions, accountViewModel) {
nav("User/${it.pubkeyHex}")
}
}
}
@Composable
fun ReplyInformation(replyTo: List<Note>?, dupMentions: List<User>?, account: Account, prefix: String = "", onUserTagClick: (User) -> Unit) {
val mentions = dupMentions?.toSet()?.sortedBy { !account.userProfile().isFollowingCached(it) }
fun ReplyInformation(
replyTo: List<Note>?,
dupMentions: List<User>?,
accountViewModel: AccountViewModel,
prefix: String = "",
onUserTagClick: (User) -> Unit
) {
val mentions = dupMentions?.toSet()?.sortedBy { !accountViewModel.account.userProfile().isFollowingCached(it) }
var expanded by remember { mutableStateOf((mentions?.size ?: 0) <= 2) }
FlowRow() {
@@ -66,7 +78,7 @@ fun ReplyInformation(replyTo: List<Note>?, dupMentions: List<User>?, account: Ac
)
} else if (idx < repliesToDisplay.size - 1) {
Text(
"${stringResource(R.string.and)}",
stringResource(R.string.and),
fontSize = 13.sp,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
@@ -80,7 +92,7 @@ fun ReplyInformation(replyTo: List<Note>?, dupMentions: List<User>?, account: Ac
)
} else if (idx < repliesToDisplay.size) {
Text(
"${stringResource(R.string.and)}",
stringResource(R.string.and),
fontSize = 13.sp,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
@@ -105,7 +117,13 @@ fun ReplyInformation(replyTo: List<Note>?, dupMentions: List<User>?, account: Ac
}
@Composable
fun ReplyInformationChannel(replyTo: List<Note>?, mentions: List<String>, channel: Channel, account: Account, nav: (String) -> Unit) {
fun ReplyInformationChannel(
replyTo: List<Note>?,
mentions: List<String>,
channel: Channel,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
var sortedMentions by remember { mutableStateOf<List<User>?>(null) }
LaunchedEffect(Unit) {
@@ -113,7 +131,7 @@ fun ReplyInformationChannel(replyTo: List<Note>?, mentions: List<String>, channe
sortedMentions = mentions
.mapNotNull { LocalCache.checkGetOrCreateUser(it) }
.toSet()
.sortedBy { account.isFollowing(it) }
.sortedBy { accountViewModel.account.isFollowing(it) }
}
}
@@ -7,18 +7,11 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material.Divider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.FollowButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ShowUserButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.UnfollowButton
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
fun UserCompose(
@@ -32,14 +25,6 @@ fun UserCompose(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
val userState by account.userProfile().live().follows.observeAsState()
val userFollows = userState?.user ?: return
val coroutineScope = rememberCoroutineScope()
Column(
modifier =
Modifier.clickable(
@@ -50,7 +35,7 @@ fun UserCompose(
modifier = overallModifier,
verticalAlignment = Alignment.CenterVertically
) {
UserPicture(baseUser, nav, account.userProfile(), 55.dp)
UserPicture(baseUser, nav, accountViewModel, 55.dp)
Column(modifier = Modifier.padding(start = 10.dp).weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically) {
@@ -61,15 +46,7 @@ fun UserCompose(
}
Column(modifier = Modifier.padding(start = 10.dp)) {
if (account.isHidden(baseUser)) {
ShowUserButton {
account.showUser(baseUser.pubkeyHex)
}
} else if (userFollows.isFollowingCached(baseUser)) {
UnfollowButton { coroutineScope.launch(Dispatchers.IO) { account.unfollow(baseUser) } }
} else {
FollowButton({ coroutineScope.launch(Dispatchers.IO) { account.follow(baseUser) } })
}
UserActionOptions(baseUser, accountViewModel)
}
}
@@ -19,41 +19,32 @@ 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.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.LnZapEvent
import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.FollowButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ShowUserButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.UnfollowButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.showAmountAxis
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.math.BigDecimal
@Composable
fun ZapNoteCompose(baseNote: Pair<Note, Note>, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
val userState by account.userProfile().live().follows.observeAsState()
val userFollows = userState?.user ?: return
val noteState by baseNote.second.live().metadata.observeAsState()
val noteZap = noteState?.note ?: return
val baseNoteRequest by baseNote.first.live().metadata.observeAsState()
val noteZapRequest = baseNoteRequest?.note ?: return
val noteZapRequest = remember(baseNoteRequest) { baseNoteRequest?.note } ?: return
val baseAuthor = noteZapRequest.author
val coroutineScope = rememberCoroutineScope()
var baseAuthor by remember {
mutableStateOf(noteZapRequest.author)
}
if (baseAuthor == null) {
BlankNote()
@@ -61,62 +52,25 @@ fun ZapNoteCompose(baseNote: Pair<Note, Note>, accountViewModel: AccountViewMode
Column(
modifier =
Modifier.clickable(
onClick = { nav("User/${baseAuthor.pubkeyHex}") }
onClick = { nav("User/${baseAuthor?.pubkeyHex}") }
),
verticalArrangement = Arrangement.Center
) {
Row(
modifier = Modifier
.padding(
start = 12.dp,
end = 12.dp,
top = 10.dp
),
verticalAlignment = Alignment.CenterVertically
) {
UserPicture(baseAuthor, nav, account.userProfile(), 55.dp)
Column(modifier = Modifier.padding(start = 10.dp).weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically) {
UsernameDisplay(baseAuthor)
}
AboutDisplay(baseAuthor)
}
var zapAmount by remember { mutableStateOf<BigDecimal?>(null) }
LaunchedEffect(key1 = noteZap) {
launch(Dispatchers.IO) {
zapAmount = (noteZap.event as? LnZapEvent)?.amount
}
}
Column(
modifier = Modifier.padding(start = 10.dp),
verticalArrangement = Arrangement.Center
) {
Text(
"${showAmount(zapAmount)} ${stringResource(R.string.sats)}",
color = BitcoinOrange,
fontSize = 20.sp,
fontWeight = FontWeight.W500
)
}
Column(modifier = Modifier.padding(start = 10.dp)) {
if (account.isHidden(baseAuthor)) {
ShowUserButton {
account.showUser(baseAuthor.pubkeyHex)
LaunchedEffect(Unit) {
launch(Dispatchers.Default) {
(noteZapRequest.event as? LnZapRequestEvent)?.let {
val decryptedContent = accountViewModel.decryptZap(noteZapRequest)
if (decryptedContent != null) {
baseAuthor = LocalCache.getOrCreateUser(decryptedContent.pubKey)
}
} else if (userFollows.isFollowingCached(baseAuthor)) {
UnfollowButton { coroutineScope.launch(Dispatchers.IO) { account.unfollow(baseAuthor) } }
} else {
FollowButton({ coroutineScope.launch(Dispatchers.IO) { account.follow(baseAuthor) } })
}
}
}
baseAuthor?.let {
RenderZapNote(it, baseNote.second, nav, accountViewModel)
}
Divider(
modifier = Modifier.padding(top = 10.dp),
thickness = 0.25.dp
@@ -125,6 +79,96 @@ fun ZapNoteCompose(baseNote: Pair<Note, Note>, accountViewModel: AccountViewMode
}
}
@Composable
private fun RenderZapNote(
baseAuthor: User,
zapNote: Note,
nav: (String) -> Unit,
accountViewModel: AccountViewModel
) {
Row(
modifier = Modifier
.padding(
start = 12.dp,
end = 12.dp,
top = 10.dp
),
verticalAlignment = Alignment.CenterVertically
) {
UserPicture(baseAuthor, nav, accountViewModel, 55.dp)
Column(
modifier = Modifier
.padding(start = 10.dp)
.weight(1f)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
UsernameDisplay(baseAuthor)
}
AboutDisplay(baseAuthor)
}
Column(
modifier = Modifier.padding(start = 10.dp),
verticalArrangement = Arrangement.Center
) {
ZapAmount(zapNote)
}
Column(modifier = Modifier.padding(start = 10.dp)) {
UserActionOptions(baseAuthor, accountViewModel)
}
}
}
@Composable
private fun ZapAmount(zapEventNote: Note) {
val noteState by zapEventNote.live().metadata.observeAsState()
val noteZap = remember(noteState) { noteState?.note } ?: return
var zapAmount by remember { mutableStateOf<String?>(null) }
LaunchedEffect(key1 = noteZap) {
launch(Dispatchers.IO) {
zapAmount = showAmountAxis((noteZap.event as? LnZapEvent)?.amount)
}
}
zapAmount?.let {
Text(
text = it,
color = BitcoinOrange,
fontSize = 20.sp,
fontWeight = FontWeight.W500
)
}
}
@Composable
fun UserActionOptions(
baseAuthor: User,
accountViewModel: AccountViewModel
) {
val coroutineScope = rememberCoroutineScope()
val accountState by accountViewModel.accountLiveData.observeAsState()
val isHidden = remember(accountState) { accountState?.account?.isHidden(baseAuthor) } ?: return
val userState by accountViewModel.account.userProfile().live().follows.observeAsState()
val isFollowing = remember(userState) { userState?.user?.isFollowingCached(baseAuthor) } ?: return
if (isHidden) {
ShowUserButton {
accountViewModel.show(baseAuthor)
}
} else if (isFollowing) {
UnfollowButton { coroutineScope.launch(Dispatchers.IO) { accountViewModel.unfollow(baseAuthor) } }
} else {
FollowButton({ coroutineScope.launch(Dispatchers.IO) { accountViewModel.follow(baseAuthor) } })
}
}
@Composable
fun AboutDisplay(baseAuthor: User) {
val baseAuthorState by baseAuthor.live().metadata.observeAsState()
@@ -43,10 +43,6 @@ fun ZapSetCompose(zapSetCard: ZapSetCard, isInnerNote: Boolean = false, routeFor
val noteState by zapSetCard.note.live().metadata.observeAsState()
val note = noteState?.note
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
val noteEvent = note?.event
var popupExpanded by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
@@ -77,7 +73,7 @@ fun ZapSetCompose(zapSetCard: ZapSetCard, isInnerNote: Boolean = false, routeFor
scope.launch {
routeFor(
note,
account.userProfile()
accountViewModel.userProfile()
)?.let { nav(it) }
}
},
@@ -116,7 +112,7 @@ fun ZapSetCompose(zapSetCard: ZapSetCard, isInnerNote: Boolean = false, routeFor
NoteAuthorPicture(
baseNote = it.key,
nav = nav,
userAccount = account.userProfile(),
accountViewModel = accountViewModel,
size = 35.dp
)
}
@@ -1,6 +1,5 @@
package com.vitorpamplona.amethyst.ui.note
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
@@ -16,7 +15,6 @@ import androidx.compose.material.icons.filled.Bolt
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
@@ -35,12 +33,8 @@ import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ZapUserSetCompose(zapSetCard: ZapUserSetCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
var isNew by remember { mutableStateOf<Boolean>(false) }
LaunchedEffect(key1 = zapSetCard.createdAt()) {
@@ -96,7 +90,7 @@ fun ZapUserSetCompose(zapSetCard: ZapUserSetCard, isInnerNote: Boolean = false,
NoteAuthorPicture(
baseNote = it.key,
nav = nav,
userAccount = account.userProfile(),
accountViewModel = accountViewModel,
size = 35.dp
)
}
@@ -231,7 +231,7 @@ fun NoteMaster(
} else if (!account.isAcceptable(noteForReports) && !showHiddenNote) {
HiddenNote(
account.getRelevantReports(noteForReports),
account.userProfile(),
accountViewModel,
Modifier,
false,
nav,
@@ -255,7 +255,7 @@ fun NoteMaster(
NoteAuthorPicture(
baseNote = baseNote,
nav = nav,
userAccount = account.userProfile(),
accountViewModel = accountViewModel,
size = 55.dp
)
@@ -263,7 +263,7 @@ fun NoteMaster(
Row(verticalAlignment = Alignment.CenterVertically) {
NoteUsernameDisplay(baseNote, Modifier.weight(1f))
DisplayFollowingHashtagsInPost(noteEvent, account, nav)
DisplayFollowingHashtagsInPost(noteEvent, accountViewModel, nav)
Text(
timeAgo(note.createdAt(), context = context),
@@ -291,7 +291,7 @@ fun NoteMaster(
val baseReward = noteEvent.getReward()
if (baseReward != null) {
DisplayReward(baseReward, baseNote, account, nav)
DisplayReward(baseReward, baseNote, accountViewModel, nav)
}
val pow = noteEvent.getPoWRank()
@@ -357,7 +357,7 @@ fun NoteMaster(
if (noteEvent is PeopleListEvent) {
DisplayPeopleList(baseNote, MaterialTheme.colors.background, accountViewModel, nav)
} else if (noteEvent is AudioTrackEvent) {
AudioTrackHeader(noteEvent, note, account.userProfile(), nav)
AudioTrackHeader(noteEvent, accountViewModel, nav)
} else if (noteEvent is PinListEvent) {
PinListHeader(baseNote, MaterialTheme.colors.background, accountViewModel, nav)
} else if (noteEvent is HighlightEvent) {
@@ -26,14 +26,12 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun UserFeedView(
fun RefreshingFeedUserFeedView(
viewModel: UserFeedViewModel,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
enablePullRefresh: Boolean = true
) {
val feedState by viewModel.feedContent.collectAsState()
var refreshing by remember { mutableStateOf(false) }
val refresh = { refreshing = true; viewModel.invalidateData(); refreshing = false }
val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh)
@@ -46,27 +44,7 @@ fun UserFeedView(
Box(modifier) {
Column() {
Crossfade(targetState = feedState, animationSpec = tween(durationMillis = 100)) { state ->
when (state) {
is UserFeedState.Empty -> {
FeedEmpty {
refreshing = true
}
}
is UserFeedState.FeedError -> {
FeedError(state.errorMessage) {
refreshing = true
}
}
is UserFeedState.Loaded -> {
refreshing = false
FeedLoaded(state, accountViewModel, nav)
}
is UserFeedState.Loading -> {
LoadingFeed()
}
}
}
UserFeedView(viewModel, accountViewModel, nav)
}
if (enablePullRefresh) {
@@ -75,6 +53,39 @@ fun UserFeedView(
}
}
@Composable
fun UserFeedView(
viewModel: UserFeedViewModel,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val feedState by viewModel.feedContent.collectAsState()
Crossfade(targetState = feedState, animationSpec = tween(durationMillis = 100)) { state ->
when (state) {
is UserFeedState.Empty -> {
FeedEmpty {
viewModel.invalidateData()
}
}
is UserFeedState.FeedError -> {
FeedError(state.errorMessage) {
viewModel.invalidateData()
}
}
is UserFeedState.Loaded -> {
FeedLoaded(state, accountViewModel, nav)
}
is UserFeedState.Loading -> {
LoadingFeed()
}
}
}
}
@Composable
private fun FeedLoaded(
state: UserFeedState.Loaded,
@@ -2,7 +2,9 @@ package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
@@ -18,9 +20,29 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class NostrUserProfileFollowsUserFeedViewModel : UserFeedViewModel(UserProfileFollowsFeedFilter)
class NostrUserProfileFollowersUserFeedViewModel : UserFeedViewModel(UserProfileFollowersFeedFilter)
class NostrHiddenAccountsFeedViewModel : UserFeedViewModel(HiddenAccountsFeedFilter)
class NostrUserProfileFollowsUserFeedViewModel(val user: User, val account: Account) : UserFeedViewModel(UserProfileFollowsFeedFilter(user, account)) {
class Factory(val user: User, val account: Account) : ViewModelProvider.Factory {
override fun <NostrUserProfileFollowsUserFeedViewModel : ViewModel> create(modelClass: Class<NostrUserProfileFollowsUserFeedViewModel>): NostrUserProfileFollowsUserFeedViewModel {
return NostrUserProfileFollowsUserFeedViewModel(user, account) as NostrUserProfileFollowsUserFeedViewModel
}
}
}
class NostrUserProfileFollowersUserFeedViewModel(val user: User, val account: Account) : UserFeedViewModel(UserProfileFollowersFeedFilter(user, account)) {
class Factory(val user: User, val account: Account) : ViewModelProvider.Factory {
override fun <NostrUserProfileFollowersUserFeedViewModel : ViewModel> create(modelClass: Class<NostrUserProfileFollowersUserFeedViewModel>): NostrUserProfileFollowersUserFeedViewModel {
return NostrUserProfileFollowersUserFeedViewModel(user, account) as NostrUserProfileFollowersUserFeedViewModel
}
}
}
class NostrHiddenAccountsFeedViewModel(val account: Account) : UserFeedViewModel(HiddenAccountsFeedFilter(account)) {
class Factory(val account: Account) : ViewModelProvider.Factory {
override fun <NostrHiddenAccountsFeedViewModel : ViewModel> create(modelClass: Class<NostrHiddenAccountsFeedViewModel>): NostrHiddenAccountsFeedViewModel {
return NostrHiddenAccountsFeedViewModel(account) as NostrHiddenAccountsFeedViewModel
}
}
}
open class UserFeedViewModel(val dataSource: FeedFilter<User>) : ViewModel() {
private val _feedContent = MutableStateFlow<UserFeedState>(UserFeedState.Loading)
@@ -36,6 +36,7 @@ import androidx.compose.material.icons.filled.Share
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
@@ -62,7 +63,6 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
@@ -145,7 +145,7 @@ fun ChannelScreen(
Column(Modifier.fillMaxHeight()) {
ChannelHeader(
channel,
account,
accountViewModel,
nav = nav
)
@@ -260,7 +260,7 @@ fun ChannelScreen(
}
@Composable
fun ChannelHeader(channelHex: String, account: Account, nav: (String) -> Unit) {
fun ChannelHeader(channelHex: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
var baseChannel by remember { mutableStateOf<Channel?>(null) }
val scope = rememberCoroutineScope()
@@ -271,12 +271,12 @@ fun ChannelHeader(channelHex: String, account: Account, nav: (String) -> Unit) {
}
baseChannel?.let {
ChannelHeader(it, account, nav)
ChannelHeader(it, accountViewModel, nav)
}
}
@Composable
fun ChannelHeader(baseChannel: Channel, account: Account, nav: (String) -> Unit) {
fun ChannelHeader(baseChannel: Channel, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val channelState by baseChannel.live.observeAsState()
val channel = remember(channelState) { channelState?.channel } ?: return
@@ -327,17 +327,7 @@ fun ChannelHeader(baseChannel: Channel, account: Account, nav: (String) -> Unit)
.height(35.dp)
.padding(bottom = 3.dp)
) {
NoteCopyButton(channel)
if (channel.creator == account.userProfile()) {
EditButton(account, channel)
}
if (account.followingChannels.contains(channel.idHex)) {
LeaveButton(account, channel, nav)
} else {
JoinButton(account, channel, nav)
}
ChannelActionOptions(channel, accountViewModel, nav)
}
}
}
@@ -349,6 +339,38 @@ fun ChannelHeader(baseChannel: Channel, account: Account, nav: (String) -> Unit)
}
}
@Composable
private fun ChannelActionOptions(
channel: Channel,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
NoteCopyButton(channel)
val isMe by remember(accountViewModel) {
derivedStateOf {
channel.creator == accountViewModel.account.userProfile()
}
}
val accountState by accountViewModel.accountLiveData.observeAsState()
val isFollowing by remember(accountState) {
derivedStateOf {
accountState?.account?.followingChannels?.contains(channel.idHex) ?: false
}
}
if (isMe) {
EditButton(accountViewModel, channel)
}
if (isFollowing) {
LeaveButton(accountViewModel, channel, nav)
} else {
JoinButton(accountViewModel, channel, nav)
}
}
@Composable
private fun NoteCopyButton(
note: Channel
@@ -385,13 +407,13 @@ private fun NoteCopyButton(
}
@Composable
private fun EditButton(account: Account, channel: Channel) {
private fun EditButton(accountViewModel: AccountViewModel, channel: Channel) {
var wantsToPost by remember {
mutableStateOf(false)
}
if (wantsToPost) {
NewChannelView({ wantsToPost = false }, account = account, channel)
NewChannelView({ wantsToPost = false }, accountViewModel, channel)
}
Button(
@@ -414,11 +436,11 @@ private fun EditButton(account: Account, channel: Channel) {
}
@Composable
private fun JoinButton(account: Account, channel: Channel, nav: (String) -> Unit) {
private fun JoinButton(accountViewModel: AccountViewModel, channel: Channel, nav: (String) -> Unit) {
Button(
modifier = Modifier.padding(horizontal = 3.dp),
onClick = {
account.joinChannel(channel.idHex)
accountViewModel.account.joinChannel(channel.idHex)
},
shape = RoundedCornerShape(20.dp),
colors = ButtonDefaults
@@ -432,11 +454,11 @@ private fun JoinButton(account: Account, channel: Channel, nav: (String) -> Unit
}
@Composable
private fun LeaveButton(account: Account, channel: Channel, nav: (String) -> Unit) {
private fun LeaveButton(accountViewModel: AccountViewModel, channel: Channel, nav: (String) -> Unit) {
Button(
modifier = Modifier.padding(horizontal = 3.dp),
onClick = {
account.leaveChannel(channel.idHex)
accountViewModel.account.leaveChannel(channel.idHex)
nav(Route.Message.route)
},
shape = RoundedCornerShape(20.dp),
@@ -28,7 +28,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
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.ui.Alignment
@@ -61,18 +60,15 @@ import com.vitorpamplona.amethyst.ui.screen.NostrChatRoomFeedViewModel
@Composable
fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account
val context = LocalContext.current
val chatRoomScreenModel: NewPostViewModel = viewModel()
chatRoomScreenModel.account = accountViewModel.account
chatRoomScreenModel.account = account
if (account != null && userId != null) {
if (userId != null) {
val replyTo = remember { mutableStateOf<Note?>(null) }
ChatroomFeedFilter.loadMessagesBetween(account, userId)
NostrChatroomDataSource.loadMessagesBetween(account, userId)
ChatroomFeedFilter.loadMessagesBetween(accountViewModel.account, userId)
NostrChatroomDataSource.loadMessagesBetween(accountViewModel.account, userId)
val feedViewModel: NostrChatRoomFeedViewModel = viewModel()
val lifeCycleOwner = LocalLifecycleOwner.current
@@ -105,7 +101,7 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, nav: (St
Column(Modifier.fillMaxHeight()) {
NostrChatroomDataSource.withUser?.let {
ChatroomHeader(it, account.userProfile(), nav = nav)
ChatroomHeader(it, accountViewModel, nav = nav)
}
Column(
@@ -178,7 +174,7 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, nav: (St
trailingIcon = {
PostButton(
onPost = {
account.sendPrivateMessage(chatRoomScreenModel.message.text, userId, replyTo.value, null, wantsToMarkAsSensitive = false)
accountViewModel.account.sendPrivateMessage(chatRoomScreenModel.message.text, userId, replyTo.value, null, wantsToMarkAsSensitive = false)
chatRoomScreenModel.message = TextFieldValue("")
replyTo.value = null
feedViewModel.invalidateData() // Don't wait a full second before updating
@@ -193,7 +189,7 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, nav: (St
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
modifier = Modifier.padding(start = 5.dp)
) {
chatRoomScreenModel.upload(it, "", account.defaultFileServer, context)
chatRoomScreenModel.upload(it, "", accountViewModel.account.defaultFileServer, context)
}
},
colors = TextFieldDefaults.textFieldColors(
@@ -207,7 +203,7 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, nav: (St
}
@Composable
fun ChatroomHeader(baseUser: User, accountUser: User, nav: (String) -> Unit) {
fun ChatroomHeader(baseUser: User, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
Column(
modifier = Modifier.clickable(
onClick = { nav("User/${baseUser.pubkeyHex}") }
@@ -217,7 +213,7 @@ fun ChatroomHeader(baseUser: User, accountUser: User, nav: (String) -> Unit) {
Row(verticalAlignment = Alignment.CenterVertically) {
UserPicture(
baseUser = baseUser,
baseUserAccount = accountUser,
accountViewModel = accountViewModel,
size = 35.dp
)
@@ -11,8 +11,10 @@ import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -22,7 +24,6 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.NostrHashtagDataSource
import com.vitorpamplona.amethyst.ui.dal.HashtagFeedFilter
import com.vitorpamplona.amethyst.ui.screen.NostrHashtagFeedViewModel
@@ -32,17 +33,14 @@ import kotlinx.coroutines.launch
@Composable
fun HashtagScreen(tag: String?, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
val lifeCycleOwner = LocalLifecycleOwner.current
if (tag != null) {
HashtagFeedFilter.loadHashtag(account, tag)
HashtagFeedFilter.loadHashtag(accountViewModel.account, tag)
val feedViewModel: NostrHashtagFeedViewModel = viewModel()
LaunchedEffect(tag) {
HashtagFeedFilter.loadHashtag(account, tag)
HashtagFeedFilter.loadHashtag(accountViewModel.account, tag)
NostrHashtagDataSource.loadHashtag(tag)
feedViewModel.invalidateData()
}
@@ -51,14 +49,14 @@ fun HashtagScreen(tag: String?, accountViewModel: AccountViewModel, nav: (String
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
println("Hashtag Start")
HashtagFeedFilter.loadHashtag(account, tag)
HashtagFeedFilter.loadHashtag(accountViewModel.account, tag)
NostrHashtagDataSource.loadHashtag(tag)
NostrHashtagDataSource.start()
feedViewModel.invalidateData()
}
if (event == Lifecycle.Event.ON_PAUSE) {
println("Hashtag Stop")
HashtagFeedFilter.loadHashtag(account, null)
HashtagFeedFilter.loadHashtag(accountViewModel.account, null)
NostrHashtagDataSource.loadHashtag(null)
NostrHashtagDataSource.stop()
}
@@ -74,7 +72,7 @@ fun HashtagScreen(tag: String?, accountViewModel: AccountViewModel, nav: (String
Column(
modifier = Modifier.padding(vertical = 0.dp)
) {
HashtagHeader(tag, account)
HashtagHeader(tag, accountViewModel)
RefresheableView(feedViewModel, accountViewModel, nav, null)
}
}
@@ -82,12 +80,7 @@ fun HashtagScreen(tag: String?, accountViewModel: AccountViewModel, nav: (String
}
@Composable
fun HashtagHeader(tag: String, account: Account) {
val userState by account.userProfile().live().follows.observeAsState()
val userFollows = userState?.user ?: return
val coroutineScope = rememberCoroutineScope()
fun HashtagHeader(tag: String, account: AccountViewModel) {
Column() {
Column(modifier = Modifier.padding(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
@@ -107,11 +100,7 @@ fun HashtagHeader(tag: String, account: Account) {
modifier = Modifier.weight(1f)
)
if (userFollows.isFollowingHashtagCached(tag)) {
UnfollowButton { coroutineScope.launch(Dispatchers.IO) { account.unfollow(tag) } }
} else {
FollowButton({ coroutineScope.launch(Dispatchers.IO) { account.follow(tag) } })
}
HashtagActionOptions(tag, account)
}
}
}
@@ -123,3 +112,24 @@ fun HashtagHeader(tag: String, account: Account) {
)
}
}
@Composable
private fun HashtagActionOptions(
tag: String,
accountViewModel: AccountViewModel
) {
val coroutineScope = rememberCoroutineScope()
val userState by accountViewModel.userProfile().live().follows.observeAsState()
val isFollowingTag by remember(userState) {
derivedStateOf {
userState?.user?.isFollowingHashtagCached(tag) ?: false
}
}
if (isFollowingTag) {
UnfollowButton { coroutineScope.launch(Dispatchers.IO) { accountViewModel.account.unfollow(tag) } }
} else {
FollowButton({ coroutineScope.launch(Dispatchers.IO) { accountViewModel.account.follow(tag) } })
}
}
@@ -24,69 +24,89 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.dal.HiddenAccountsFeedFilter
import com.vitorpamplona.amethyst.ui.screen.NostrHiddenAccountsFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.UserFeedView
import com.vitorpamplona.amethyst.ui.screen.RefreshingFeedUserFeedView
import kotlinx.coroutines.launch
@Composable
fun HiddenUsersScreen(accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val feedViewModel: NostrHiddenAccountsFeedViewModel = viewModel(
factory = NostrHiddenAccountsFeedViewModel.Factory(accountViewModel.account)
)
HiddenUsersScreen(feedViewModel, accountViewModel, nav)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun HiddenUsersScreen(accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account
fun HiddenUsersScreen(
feedViewModel: NostrHiddenAccountsFeedViewModel,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val lifeCycleOwner = LocalLifecycleOwner.current
val feedViewModel: NostrHiddenAccountsFeedViewModel = viewModel()
if (account != null) {
HiddenAccountsFeedFilter.account = account
LaunchedEffect(accountViewModel) {
HiddenAccountsFeedFilter.account = account
feedViewModel.invalidateData()
}
DisposableEffect(accountViewModel) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
println("Hidden Users Start")
HiddenAccountsFeedFilter.account = account
feedViewModel.invalidateData()
}
if (event == Lifecycle.Event.ON_PAUSE) {
println("Hidden Users Stop")
}
DisposableEffect(accountViewModel) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
println("Hidden Users Start")
feedViewModel.invalidateData()
}
lifeCycleOwner.lifecycle.addObserver(observer)
onDispose {
lifeCycleOwner.lifecycle.removeObserver(observer)
if (event == Lifecycle.Event.ON_PAUSE) {
println("Hidden Users Stop")
}
}
Column(Modifier.fillMaxHeight()) {
Column(modifier = Modifier.padding(start = 10.dp, end = 10.dp)) {
val pagerState = rememberPagerState()
val coroutineScope = rememberCoroutineScope()
lifeCycleOwner.lifecycle.addObserver(observer)
onDispose {
lifeCycleOwner.lifecycle.removeObserver(observer)
}
}
TabRow(
backgroundColor = MaterialTheme.colors.background,
selectedTabIndex = pagerState.currentPage
) {
Tab(
selected = pagerState.currentPage == 0,
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(0) } },
text = {
Text(text = stringResource(R.string.blocked_users))
}
)
}
HorizontalPager(pageCount = 1, state = pagerState) { page ->
when (page) {
0 -> UserFeedView(feedViewModel, accountViewModel, nav)
Column(Modifier.fillMaxHeight()) {
Column(modifier = Modifier.padding(start = 10.dp, end = 10.dp)) {
val pagerState = rememberPagerState()
val coroutineScope = rememberCoroutineScope()
TabRow(
backgroundColor = MaterialTheme.colors.background,
selectedTabIndex = pagerState.currentPage
) {
Tab(
selected = pagerState.currentPage == 0,
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(0) } },
text = {
Text(text = stringResource(R.string.blocked_users))
}
)
}
HorizontalPager(pageCount = 1, state = pagerState) { page ->
when (page) {
0 -> RefreshingUserFeedView(feedViewModel, accountViewModel, nav)
}
}
}
}
}
@Composable
fun RefreshingUserFeedView(
feedViewModel: NostrHiddenAccountsFeedViewModel,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
WatchAccount(feedViewModel, accountViewModel)
RefreshingFeedUserFeedView(feedViewModel, accountViewModel, nav)
}
@Composable
fun WatchAccount(
feedViewModel: NostrHiddenAccountsFeedViewModel,
accountViewModel: AccountViewModel
) {
val accountState by accountViewModel.accountLiveData.observeAsState()
LaunchedEffect(accountViewModel, accountState) {
feedViewModel.invalidateData()
}
}
@@ -137,7 +137,7 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
}
},
floatingActionButton = {
FloatingButtons(navController, accountViewModel, accountStateViewModel)
FloatingButtons(navController, accountViewModel, accountStateViewModel, nav)
},
scaffoldState = scaffoldState
) {
@@ -161,17 +161,14 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
}
@Composable
fun FloatingButtons(navController: NavHostController, accountViewModel: AccountViewModel, accountStateViewModel: AccountStateViewModel) {
fun FloatingButtons(
navController: NavHostController,
accountViewModel: AccountViewModel,
accountStateViewModel: AccountStateViewModel,
nav: (String) -> Unit
) {
val accountState by accountStateViewModel.accountContent.collectAsState()
val nav = remember {
{ route: String ->
if (getRouteWithArguments(navController) != route) {
navController.navigate(route)
}
}
}
Crossfade(targetState = accountState, animationSpec = tween(durationMillis = 100)) { state ->
when (state) {
is AccountState.LoggedInViewOnly -> {
@@ -185,7 +182,7 @@ fun FloatingButtons(navController: NavHostController, accountViewModel: AccountV
NewNoteButton(accountViewModel, nav)
}
if (currentRoute(navController) == Route.Message.base) {
ChannelFabColumn(state.account, nav)
ChannelFabColumn(accountViewModel, nav)
}
if (currentRoute(navController)?.substringBefore("?") == Route.Video.base) {
NewImageButton(accountViewModel, nav)
@@ -66,7 +66,6 @@ import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataView
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.DisplayNip05ProfileStatus
import com.vitorpamplona.amethyst.ui.components.InvoiceRequest
import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
@@ -74,14 +73,11 @@ import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog
import com.vitorpamplona.amethyst.ui.components.figureOutMimeType
import com.vitorpamplona.amethyst.ui.dal.UserProfileBookmarksFeedFilter
import com.vitorpamplona.amethyst.ui.dal.UserProfileConversationsFeedFilter
import com.vitorpamplona.amethyst.ui.dal.UserProfileFollowersFeedFilter
import com.vitorpamplona.amethyst.ui.dal.UserProfileFollowsFeedFilter
import com.vitorpamplona.amethyst.ui.dal.UserProfileNewThreadFeedFilter
import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter
import com.vitorpamplona.amethyst.ui.dal.UserProfileZapsFeedFilter
import com.vitorpamplona.amethyst.ui.navigation.ShowQRDialog
import com.vitorpamplona.amethyst.ui.note.UserPicture
import com.vitorpamplona.amethyst.ui.note.showAmount
import com.vitorpamplona.amethyst.ui.screen.LnZapFeedView
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileBookmarksFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileConversationsFeedViewModel
@@ -91,9 +87,10 @@ import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileNewThreadsFeedViewMo
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileReportFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileZapsFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.RefresheableView
import com.vitorpamplona.amethyst.ui.screen.RefreshingFeedUserFeedView
import com.vitorpamplona.amethyst.ui.screen.RelayFeedView
import com.vitorpamplona.amethyst.ui.screen.RelayFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.UserFeedView
import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
@@ -108,14 +105,17 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, nav: (Str
var userBase by remember { mutableStateOf<User?>(null) }
LaunchedEffect(Unit) {
LaunchedEffect(userId) {
withContext(Dispatchers.IO) {
userBase = LocalCache.checkGetOrCreateUser(userId)
val newUserBase = LocalCache.checkGetOrCreateUser(userId)
if (newUserBase != userBase) {
userBase = newUserBase
}
}
}
userBase?.let {
ProfileScreen(
PrepareViewModels(
baseUser = it,
accountViewModel = accountViewModel,
nav = nav
@@ -123,13 +123,44 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, nav: (Str
}
}
@Composable
fun PrepareViewModels(baseUser: User, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val followsFeedViewModel: NostrUserProfileFollowsUserFeedViewModel = viewModel(
key = baseUser.pubkeyHex + "UserProfileFollowsUserFeedViewModel",
factory = NostrUserProfileFollowsUserFeedViewModel.Factory(
baseUser,
accountViewModel.account
)
)
val followersFeedViewModel: NostrUserProfileFollowersUserFeedViewModel = viewModel(
key = baseUser.pubkeyHex + "UserProfileFollowersUserFeedViewModel",
factory = NostrUserProfileFollowersUserFeedViewModel.Factory(
baseUser,
accountViewModel.account
)
)
ProfileScreen(
baseUser = baseUser,
followsFeedViewModel,
followersFeedViewModel,
accountViewModel = accountViewModel,
nav = nav
)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ProfileScreen(baseUser: User, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
fun ProfileScreen(
baseUser: User,
followsFeedViewModel: NostrUserProfileFollowsUserFeedViewModel,
followersFeedViewModel: NostrUserProfileFollowersUserFeedViewModel,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
UserProfileNewThreadFeedFilter.loadUserProfile(accountViewModel.account, baseUser)
UserProfileConversationsFeedFilter.loadUserProfile(accountViewModel.account, baseUser)
UserProfileFollowersFeedFilter.loadUserProfile(accountViewModel.account, baseUser)
UserProfileFollowsFeedFilter.loadUserProfile(accountViewModel.account, baseUser)
UserProfileZapsFeedFilter.loadUserProfile(baseUser)
UserProfileReportsFeedFilter.loadUserProfile(baseUser)
UserProfileBookmarksFeedFilter.loadUserProfile(accountViewModel.account, baseUser)
@@ -245,8 +276,8 @@ fun ProfileScreen(baseUser: User, accountViewModel: AccountViewModel, nav: (Stri
when (page) {
0 -> TabNotesNewThreads(accountViewModel, nav)
1 -> TabNotesConversations(accountViewModel, nav)
2 -> TabFollows(baseUser, accountViewModel, nav)
3 -> TabFollowers(baseUser, accountViewModel, nav)
2 -> TabFollows(baseUser, followsFeedViewModel, accountViewModel, nav)
3 -> TabFollows(baseUser, followersFeedViewModel, accountViewModel, nav)
4 -> TabReceivedZaps(baseUser, accountViewModel, nav)
5 -> TabBookmarks(baseUser, accountViewModel, nav)
6 -> TabReports(baseUser, accountViewModel, nav)
@@ -307,31 +338,53 @@ private fun ZapTabHeader(baseUser: User) {
var zapAmount by remember { mutableStateOf<BigDecimal?>(null) }
LaunchedEffect(key1 = userState) {
withContext(Dispatchers.IO) {
launch(Dispatchers.IO) {
val tempAmount = baseUser.zappedAmount()
withContext(Dispatchers.Main) {
zapAmount = tempAmount
if (zapAmount != tempAmount) {
zapAmount = tempAmount
}
}
}
}
Text(text = "${showAmount(zapAmount)} ${stringResource(id = R.string.zaps)}")
Text(text = "${showAmountAxis(zapAmount)} ${stringResource(id = R.string.zaps)}")
}
@Composable
private fun FollowersTabHeader(baseUser: User) {
val userState by baseUser.live().follows.observeAsState()
val userFollowers = remember(userState) { userState?.user?.transientFollowerCount() ?: "--" }
var followerCount by remember { mutableStateOf("--") }
Text(text = "$userFollowers ${stringResource(id = R.string.followers)}")
LaunchedEffect(key1 = userState) {
launch(Dispatchers.IO) {
val newFollower = userState?.user?.transientFollowerCount()?.toString() ?: "--"
if (followerCount != newFollower) {
followerCount = newFollower
}
}
}
Text(text = "$followerCount ${stringResource(id = R.string.followers)}")
}
@Composable
private fun FollowTabHeader(baseUser: User) {
val userState by baseUser.live().follows.observeAsState()
val userFollows = remember(userState) { userState?.user?.transientFollowCount() ?: "--" }
var followCount by remember { mutableStateOf("--") }
Text(text = "$userFollows ${stringResource(R.string.follows)}")
LaunchedEffect(key1 = userState) {
launch(Dispatchers.IO) {
val newFollow = userState?.user?.transientFollowCount()?.toString() ?: "--"
if (followCount != newFollow) {
followCount = newFollow
}
}
}
Text(text = "$followCount ${stringResource(R.string.follows)}")
}
@Composable
@@ -389,7 +442,7 @@ private fun ProfileHeader(
) {
UserPicture(
baseUser = baseUser,
baseUserAccount = accountViewModel.userProfile(),
accountViewModel = accountViewModel,
size = 100.dp,
modifier = Modifier.border(
3.dp,
@@ -402,7 +455,7 @@ private fun ProfileHeader(
}
},
onLongClick = {
ResizeImage(it.info?.picture, 100.dp).proxyUrl()?.let { it1 ->
it.info?.picture?.let { it1 ->
clipboardManager.setText(
AnnotatedString(it1)
)
@@ -465,7 +518,7 @@ private fun ProfileActions(
val isUserFollowingLoggedIn by remember(accountUserState, accountLocalUserState) {
derivedStateOf {
baseUser.isFollowingCached(accountUser)
baseUser.isFollowing(accountUser)
}
}
@@ -937,41 +990,28 @@ fun TabBookmarks(baseUser: User, accountViewModel: AccountViewModel, nav: (Strin
}
@Composable
fun TabFollows(baseUser: User, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val feedViewModel: NostrUserProfileFollowsUserFeedViewModel = viewModel()
val userState by baseUser.live().follows.observeAsState()
LaunchedEffect(userState) {
feedViewModel.invalidateData()
}
fun TabFollows(baseUser: User, feedViewModel: UserFeedViewModel, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
WatchFollowChanges(baseUser, feedViewModel)
Column(Modifier.fillMaxHeight()) {
Column(
modifier = Modifier.padding(vertical = 0.dp)
) {
UserFeedView(feedViewModel, accountViewModel, nav, enablePullRefresh = false)
RefreshingFeedUserFeedView(feedViewModel, accountViewModel, nav, enablePullRefresh = false)
}
}
}
@Composable
fun TabFollowers(baseUser: User, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val feedViewModel: NostrUserProfileFollowersUserFeedViewModel = viewModel()
private fun WatchFollowChanges(
baseUser: User,
feedViewModel: UserFeedViewModel
) {
val userState by baseUser.live().follows.observeAsState()
LaunchedEffect(userState) {
feedViewModel.invalidateData()
}
Column(Modifier.fillMaxHeight()) {
Column(
modifier = Modifier.padding(vertical = 0.dp)
) {
UserFeedView(feedViewModel, accountViewModel, nav, enablePullRefresh = false)
}
}
}
@Composable
@@ -389,7 +389,7 @@ fun HashtagLine(tag: String, onClick: () -> Unit) {
@Composable
fun UserLine(
baseUser: User,
account: Account,
accountViewModel: AccountViewModel,
onClick: () -> Unit
) {
Column(
@@ -405,7 +405,7 @@ fun UserLine(
top = 10.dp
)
) {
UserPicture(baseUser, account.userProfile(), 55.dp, Modifier, null)
UserPicture(baseUser, 55.dp, accountViewModel, Modifier, null)
Column(
modifier = Modifier
@@ -247,11 +247,6 @@ private fun RenderVideoOrPictureNote(
nav: (String) -> Unit
) {
val noteEvent = note.event
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
val loggedIn = account.userProfile()
var moreActionsExpanded by remember { mutableStateOf(false) }
Column(Modifier.fillMaxSize(1f)) {
@@ -268,7 +263,7 @@ private fun RenderVideoOrPictureNote(
Column(Modifier.weight(1f)) {
Row(Modifier.padding(10.dp), verticalAlignment = Alignment.Bottom) {
Column(Modifier.size(55.dp), verticalArrangement = Arrangement.Center) {
NoteAuthorPicture(note, nav, loggedIn, 55.dp)
NoteAuthorPicture(note, nav, accountViewModel, 55.dp)
}
Column(