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