Simplifying observables

This commit is contained in:
Vitor Pamplona
2023-06-01 16:01:35 -04:00
parent 92c61f317b
commit 99c30816c6
9 changed files with 175 additions and 144 deletions
@@ -449,10 +449,12 @@ private fun ObserveNIP19Event(
var baseNote by remember(it) { mutableStateOf<Note?>(null) } var baseNote by remember(it) { mutableStateOf<Note?>(null) }
LaunchedEffect(key1 = it.hex) { LaunchedEffect(key1 = it.hex) {
launch(Dispatchers.IO) { if (baseNote == null) {
if (it.type == Nip19.Type.NOTE || it.type == Nip19.Type.EVENT || it.type == Nip19.Type.ADDRESS) { launch(Dispatchers.IO) {
LocalCache.checkGetOrCreateNote(it.hex)?.let { note -> if (it.type == Nip19.Type.NOTE || it.type == Nip19.Type.EVENT || it.type == Nip19.Type.ADDRESS) {
baseNote = note LocalCache.checkGetOrCreateNote(it.hex)?.let { note ->
baseNote = note
}
} }
} }
} }
@@ -479,10 +481,12 @@ private fun ObserveNIP19User(
var baseUser by remember(it) { mutableStateOf<User?>(null) } var baseUser by remember(it) { mutableStateOf<User?>(null) }
LaunchedEffect(key1 = it.hex) { LaunchedEffect(key1 = it.hex) {
launch(Dispatchers.IO) { if (baseUser == null) {
if (it.type == Nip19.Type.USER) { launch(Dispatchers.IO) {
LocalCache.checkGetOrCreateUser(it.hex)?.let { user -> if (it.type == Nip19.Type.USER) {
baseUser = user LocalCache.checkGetOrCreateUser(it.hex)?.let { user ->
baseUser = user
}
} }
} }
} }
@@ -782,27 +786,29 @@ fun TagLink(word: String, tags: List<List<String>>, canPreview: Boolean, backgro
var baseNotePair by remember { mutableStateOf<Pair<Note, String?>?>(null) } var baseNotePair by remember { mutableStateOf<Pair<Note, String?>?>(null) }
LaunchedEffect(key1 = word) { LaunchedEffect(key1 = word) {
launch(Dispatchers.IO) { if (baseUserPair == null && baseNotePair == null) {
val matcher = tagIndex.matcher(word) launch(Dispatchers.IO) {
val (index, suffix) = try { val matcher = tagIndex.matcher(word)
matcher.find() val (index, suffix) = try {
Pair(matcher.group(1)?.toInt(), matcher.group(2) ?: "") matcher.find()
} catch (e: Exception) { Pair(matcher.group(1)?.toInt(), matcher.group(2) ?: "")
Log.w("Tag Parser", "Couldn't link tag $word", e) } catch (e: Exception) {
Pair(null, null) Log.w("Tag Parser", "Couldn't link tag $word", e)
} Pair(null, null)
}
if (index != null && index >= 0 && index < tags.size) { if (index != null && index >= 0 && index < tags.size) {
val tag = tags[index] val tag = tags[index]
if (tag.size > 1) { if (tag.size > 1) {
if (tag[0] == "p") { if (tag[0] == "p") {
LocalCache.checkGetOrCreateUser(tag[1])?.let { LocalCache.checkGetOrCreateUser(tag[1])?.let {
baseUserPair = Pair(it, suffix) baseUserPair = Pair(it, suffix)
} }
} else if (tag[0] == "e" || tag[0] == "a") { } else if (tag[0] == "e" || tag[0] == "a") {
LocalCache.checkGetOrCreateNote(tag[1])?.let { LocalCache.checkGetOrCreateNote(tag[1])?.let {
baseNotePair = Pair(it, suffix) baseNotePair = Pair(it, suffix)
}
} }
} }
} }
@@ -110,31 +110,11 @@ private fun RowScope.HasNewItemsIcon(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
navController: NavHostController navController: NavHostController
) { ) {
val accountState by accountViewModel.accountLiveData.observeAsState() var hasNewItems by remember { mutableStateOf(false) }
val account = remember(accountState) { accountState?.account } ?: return
val notifState by NotificationCache.live.observeAsState() WatchPossibleNotificationChanges(route, accountViewModel) {
val notif = remember(notifState) { notifState?.cache } ?: return if (it != hasNewItems) {
hasNewItems = it
var hasNewItems by remember { mutableStateOf<Boolean>(false) }
LaunchedEffect(key1 = notifState, key2 = accountState) {
launch(Dispatchers.IO) {
val newHasNewItems = route.hasNewItems(account, notif, emptySet())
if (newHasNewItems != hasNewItems) {
hasNewItems = newHasNewItems
}
}
}
LaunchedEffect(Unit) {
launch(Dispatchers.IO) {
LocalCache.live.newEventBundles.collect {
val newHasNewItems = route.hasNewItems(account, notif, it)
if (newHasNewItems != hasNewItems) {
hasNewItems = newHasNewItems
}
}
} }
} }
@@ -167,6 +147,33 @@ private fun RowScope.HasNewItemsIcon(
} }
} }
@Composable
fun WatchPossibleNotificationChanges(
route: Route,
accountViewModel: AccountViewModel,
onChange: (Boolean) -> Unit
) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = remember(accountState) { accountState?.account } ?: return
val notifState by NotificationCache.live.observeAsState()
val notif = remember(notifState) { notifState?.cache } ?: return
LaunchedEffect(key1 = notifState, key2 = accountState) {
launch(Dispatchers.IO) {
onChange(route.hasNewItems(account, notif, emptySet()))
}
}
LaunchedEffect(Unit) {
launch(Dispatchers.IO) {
LocalCache.live.newEventBundles.collect {
onChange(route.hasNewItems(account, notif, it))
}
}
}
}
@Composable @Composable
private fun RowScope.BottomIcon( private fun RowScope.BottomIcon(
icon: Int, icon: Int,
@@ -54,7 +54,6 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.NostrAccountDataSource import com.vitorpamplona.amethyst.service.NostrAccountDataSource
import com.vitorpamplona.amethyst.service.NostrChannelDataSource import com.vitorpamplona.amethyst.service.NostrChannelDataSource
import com.vitorpamplona.amethyst.service.NostrChatroomDataSource import com.vitorpamplona.amethyst.service.NostrChatroomDataSource
@@ -103,16 +102,20 @@ fun AppTopBar(followLists: FollowListViewModel, navController: NavHostController
fun StoriesTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) { fun StoriesTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
GenericTopBar(scaffoldState, accountViewModel) { accountViewModel -> GenericTopBar(scaffoldState, accountViewModel) { accountViewModel ->
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
accountState?.account?.let { account ->
FollowList( val list by remember(accountState) {
followLists, derivedStateOf {
account.defaultStoriesFollowList, accountState?.account?.defaultStoriesFollowList ?: GLOBAL_FOLLOWS
account.userProfile(),
true
) { listName ->
account.changeDefaultStoriesFollowList(listName)
} }
} }
FollowList(
followLists,
list,
true
) { listName ->
accountViewModel.account.changeDefaultStoriesFollowList(listName)
}
} }
} }
@@ -120,16 +123,20 @@ fun StoriesTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState
fun HomeTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) { fun HomeTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
GenericTopBar(scaffoldState, accountViewModel) { accountViewModel -> GenericTopBar(scaffoldState, accountViewModel) { accountViewModel ->
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
accountState?.account?.let { account ->
FollowList( val list by remember(accountState) {
followLists, derivedStateOf {
account.defaultHomeFollowList, accountState?.account?.defaultHomeFollowList ?: GLOBAL_FOLLOWS
account.userProfile(),
false
) { listName ->
account.changeDefaultHomeFollowList(listName)
} }
} }
FollowList(
followLists,
list,
false
) { listName ->
accountViewModel.account.changeDefaultHomeFollowList(listName)
}
} }
} }
@@ -137,16 +144,20 @@ fun HomeTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, a
fun NotificationTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) { fun NotificationTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
GenericTopBar(scaffoldState, accountViewModel) { accountViewModel -> GenericTopBar(scaffoldState, accountViewModel) { accountViewModel ->
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
accountState?.account?.let { account ->
FollowList( val list by remember(accountState) {
followLists, derivedStateOf {
account.defaultNotificationFollowList, accountState?.account?.defaultNotificationFollowList ?: GLOBAL_FOLLOWS
account.userProfile(),
true
) { listName ->
account.changeDefaultNotificationFollowList(listName)
} }
} }
FollowList(
followLists,
list,
true
) { listName ->
accountViewModel.account.changeDefaultNotificationFollowList(listName)
}
} }
} }
@@ -296,7 +307,7 @@ private fun LoggedInUserPictureDrawer(
} }
@Composable @Composable
fun FollowList(followListsModel: FollowListViewModel, listName: String, loggedIn: User, withGlobal: Boolean, onChange: (String) -> Unit) { fun FollowList(followListsModel: FollowListViewModel, listName: String, withGlobal: Boolean, onChange: (String) -> Unit) {
val kind3Follow = Pair(KIND3_FOLLOWS, stringResource(id = R.string.follow_list_kind3follows)) val kind3Follow = Pair(KIND3_FOLLOWS, stringResource(id = R.string.follow_list_kind3follows))
val globalFollow = Pair(GLOBAL_FOLLOWS, stringResource(id = R.string.follow_list_global)) val globalFollow = Pair(GLOBAL_FOLLOWS, stringResource(id = R.string.follow_list_global))
@@ -339,7 +350,10 @@ class FollowListViewModel(val account: Account) : ViewModel() {
val newFollowLists = LocalCache.addressables.mapNotNull { val newFollowLists = LocalCache.addressables.mapNotNull {
val event = (it.value.event as? PeopleListEvent) val event = (it.value.event as? PeopleListEvent)
// Has to have an list // Has to have an list
if (event != null && event.pubKey == account.userProfile().pubkeyHex && (event.tags.size > 1 || event.content.length > 50)) { if (event != null &&
event.pubKey == account.userProfile().pubkeyHex &&
(event.tags.size > 1 || event.content.length > 50)
) {
Pair(event.dTag(), event.dTag()) Pair(event.dTag(), event.dTag())
} else { } else {
null null
@@ -76,16 +76,13 @@ fun DrawerContent(
sheetState: ModalBottomSheetState, sheetState: ModalBottomSheetState,
accountViewModel: AccountViewModel accountViewModel: AccountViewModel
) { ) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
Surface( Surface(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colors.background color = MaterialTheme.colors.background
) { ) {
Column() { Column() {
ProfileContent( ProfileContent(
account.userProfile(), accountViewModel.account.userProfile(),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 25.dp) .padding(horizontal = 25.dp)
@@ -98,17 +95,16 @@ fun DrawerContent(
modifier = Modifier.padding(top = 20.dp) modifier = Modifier.padding(top = 20.dp)
) )
ListContent( ListContent(
account.userProfile().pubkeyHex,
nav, nav,
scaffoldState, scaffoldState,
sheetState, sheetState,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.weight(1f), .weight(1f),
account accountViewModel
) )
BottomContent(account.userProfile(), scaffoldState, nav) BottomContent(accountViewModel.account.userProfile(), scaffoldState, nav)
} }
} }
} }
@@ -265,13 +261,15 @@ private fun FollowingAndFollowerCounts(baseAccountUser: User) {
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun ListContent( fun ListContent(
accountUserPubKey: String?,
nav: (String) -> Unit, nav: (String) -> Unit,
scaffoldState: ScaffoldState, scaffoldState: ScaffoldState,
sheetState: ModalBottomSheetState, sheetState: ModalBottomSheetState,
modifier: Modifier, modifier: Modifier,
account: Account accountViewModel: AccountViewModel
) { ) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = remember(accountState) { accountState?.account } ?: return
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
var backupDialogOpen by remember { mutableStateOf(false) } var backupDialogOpen by remember { mutableStateOf(false) }
var checked by remember { mutableStateOf(account.proxy != null) } var checked by remember { mutableStateOf(account.proxy != null) }
@@ -284,25 +282,23 @@ fun ListContent(
.fillMaxHeight() .fillMaxHeight()
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
if (accountUserPubKey != null) { NavigationRow(
NavigationRow( title = stringResource(R.string.profile),
title = stringResource(R.string.profile), icon = Route.Profile.icon,
icon = Route.Profile.icon, tint = MaterialTheme.colors.primary,
tint = MaterialTheme.colors.primary, nav = nav,
nav = nav, scaffoldState = scaffoldState,
scaffoldState = scaffoldState, route = "User/${account.userProfile().pubkeyHex}"
route = "User/$accountUserPubKey" )
)
NavigationRow( NavigationRow(
title = stringResource(R.string.bookmarks), title = stringResource(R.string.bookmarks),
icon = Route.Bookmarks.icon, icon = Route.Bookmarks.icon,
tint = MaterialTheme.colors.onBackground, tint = MaterialTheme.colors.onBackground,
nav = nav, nav = nav,
scaffoldState = scaffoldState, scaffoldState = scaffoldState,
route = Route.Bookmarks.route route = Route.Bookmarks.route
) )
}
NavigationRow( NavigationRow(
title = stringResource(R.string.security_filters), title = stringResource(R.string.security_filters),
@@ -65,9 +65,6 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun
val noteState by baseNote.live().metadata.observeAsState() val noteState by baseNote.live().metadata.observeAsState()
val note = remember(noteState) { noteState?.note } ?: return val note = remember(noteState) { noteState?.note } ?: return
val accountState by accountViewModel.accountLiveData.observeAsState()
val loggedIn = remember(accountState) { accountState?.account?.userProfile() } ?: return
var popupExpanded by remember { mutableStateOf(false) } var popupExpanded by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -108,7 +105,7 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun
.combinedClickable( .combinedClickable(
onClick = { onClick = {
scope.launch { scope.launch {
routeFor(baseNote, loggedIn)?.let { nav(it) } routeFor(baseNote, accountViewModel.userProfile())?.let { nav(it) }
} }
}, },
onLongClick = { popupExpanded = true } onLongClick = { popupExpanded = true }
@@ -15,6 +15,7 @@ import androidx.compose.material.icons.filled.Report
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -97,11 +98,14 @@ fun nip05VerificationAsAState(user: UserMetadata, pubkeyHex: String): State<Bool
@Composable @Composable
fun ObserveDisplayNip05Status(baseNote: Note, columnModifier: Modifier = Modifier) { fun ObserveDisplayNip05Status(baseNote: Note, columnModifier: Modifier = Modifier) {
val noteState by baseNote.live().metadata.observeAsState() val noteState by baseNote.live().metadata.observeAsState()
val note = noteState?.note ?: return val author by remember(noteState) {
derivedStateOf {
noteState?.note?.author
}
}
val author = note.author author?.let {
if (author != null) { ObserveDisplayNip05Status(it, columnModifier)
ObserveDisplayNip05Status(author, columnModifier)
} }
} }
@@ -269,19 +269,18 @@ fun ZapVote(
nonClickablePrepend: @Composable () -> Unit, nonClickablePrepend: @Composable () -> Unit,
clickablePrepend: @Composable () -> Unit clickablePrepend: @Composable () -> Unit
) { ) {
val zapsState by baseNote.live().zaps.observeAsState() val isLoggedUser by remember {
val zappedNote = zapsState?.note ?: return derivedStateOf {
accountViewModel.isLoggedUser(baseNote.author)
}
}
var wantsToZap by remember { mutableStateOf(false) } var wantsToZap by remember { mutableStateOf(false) }
var zappingProgress by remember { mutableStateOf(0f) }
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var zappingProgress by remember { mutableStateOf(0f) }
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
nonClickablePrepend() nonClickablePrepend()
Row( Row(
@@ -311,7 +310,7 @@ fun ZapVote(
) )
.show() .show()
} }
} else if (accountViewModel.isLoggedUser(zappedNote.author)) { } else if (isLoggedUser) {
scope.launch { scope.launch {
Toast Toast
.makeText( .makeText(
@@ -333,11 +332,13 @@ fun ZapVote(
.show() .show()
} }
return@combinedClickable return@combinedClickable
} else if (account.zapAmountChoices.size == 1 && pollViewModel.isValidInputVoteAmount(account.zapAmountChoices.first())) { } else if (accountViewModel.account.zapAmountChoices.size == 1 &&
pollViewModel.isValidInputVoteAmount(accountViewModel.account.zapAmountChoices.first())
) {
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
accountViewModel.zap( accountViewModel.zap(
baseNote, baseNote,
account.zapAmountChoices.first() * 1000, accountViewModel.account.zapAmountChoices.first() * 1000,
poolOption.option, poolOption.option,
"", "",
context, context,
@@ -354,7 +355,7 @@ fun ZapVote(
zappingProgress = it zappingProgress = it
} }
}, },
zapType = account.defaultZapType zapType = accountViewModel.account.defaultZapType
) )
} }
} else { } else {
@@ -519,10 +519,8 @@ private fun ZapIcon(
var wasZappedByLoggedInUser by remember { mutableStateOf(false) } var wasZappedByLoggedInUser by remember { mutableStateOf(false) }
val zapsState by baseNote.live().zaps.observeAsState() val zapsState by baseNote.live().zaps.observeAsState()
val scope = rememberCoroutineScope()
LaunchedEffect(key1 = zapsState) { LaunchedEffect(key1 = zapsState) {
scope.launch(Dispatchers.IO) { launch(Dispatchers.IO) {
zapsState?.note?.let { zapsState?.note?.let {
if (!wasZappedByLoggedInUser) { if (!wasZappedByLoggedInUser) {
val newWasZapped = accountViewModel.calculateIfNoteWasZappedByAccount(it) val newWasZapped = accountViewModel.calculateIfNoteWasZappedByAccount(it)
@@ -559,17 +557,16 @@ private fun ZapAmountText(
accountViewModel: AccountViewModel accountViewModel: AccountViewModel
) { ) {
val zapsState by baseNote.live().zaps.observeAsState() val zapsState by baseNote.live().zaps.observeAsState()
val zappedNote = remember(zapsState) { zapsState?.note } ?: return
val scope = rememberCoroutineScope()
var zapAmountTxt by remember { mutableStateOf("") } var zapAmountTxt by remember { mutableStateOf("") }
LaunchedEffect(key1 = zapsState) { LaunchedEffect(key1 = zapsState) {
scope.launch(Dispatchers.IO) { launch(Dispatchers.IO) {
val newZapAmount = showAmount(accountViewModel.calculateZapAmount(zappedNote)) zapsState?.note?.let {
if (newZapAmount != zapAmountTxt) { val newZapAmount = showAmount(accountViewModel.calculateZapAmount(it))
zapAmountTxt = newZapAmount if (newZapAmount != zapAmountTxt) {
zapAmountTxt = newZapAmount
}
} }
} }
} }
@@ -307,11 +307,13 @@ private fun ReportsTabHeader(baseUser: User) {
var userReports by remember { mutableStateOf(0) } var userReports by remember { mutableStateOf(0) }
LaunchedEffect(key1 = userState) { LaunchedEffect(key1 = userState) {
UserProfileReportsFeedFilter.user = baseUser launch(Dispatchers.IO) {
val newSize = UserProfileReportsFeedFilter.feed().size UserProfileReportsFeedFilter.user = baseUser
val newSize = UserProfileReportsFeedFilter.feed().size
if (newSize != userReports) { if (newSize != userReports) {
userReports = newSize userReports = newSize
}
} }
} }
@@ -321,12 +323,19 @@ private fun ReportsTabHeader(baseUser: User) {
@Composable @Composable
private fun BookmarkTabHeader(baseUser: User) { private fun BookmarkTabHeader(baseUser: User) {
val userState by baseUser.live().bookmarks.observeAsState() val userState by baseUser.live().bookmarks.observeAsState()
val userBookmarks = remember(userState) {
val bookmarkList = userState?.user?.latestBookmarkList var userBookmarks by remember { mutableStateOf(0) }
(bookmarkList?.taggedEvents()?.count() ?: 0) + (
bookmarkList?.taggedAddresses()?.count() LaunchedEffect(key1 = userState) {
?: 0 launch(Dispatchers.IO) {
) val bookmarkList = userState?.user?.latestBookmarkList
val newBookmarks = (bookmarkList?.taggedEvents()?.count() ?: 0) + (bookmarkList?.taggedAddresses()?.count() ?: 0)
if (newBookmarks != userBookmarks) {
userBookmarks = newBookmarks
}
}
} }
Text(text = "$userBookmarks ${stringResource(R.string.bookmarks)}") Text(text = "$userBookmarks ${stringResource(R.string.bookmarks)}")