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