Turn FollowList into a ViewModel and a LocalCache LiveData into a SharedFlow (it was killing some coroutines that were supposed to finish)
This commit is contained in:
@@ -2,7 +2,6 @@ package com.vitorpamplona.amethyst.model
|
|||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
import com.vitorpamplona.amethyst.Amethyst
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
@@ -11,6 +10,8 @@ import com.vitorpamplona.amethyst.service.relays.Relay
|
|||||||
import com.vitorpamplona.amethyst.ui.components.BundledInsert
|
import com.vitorpamplona.amethyst.ui.components.BundledInsert
|
||||||
import fr.acinq.secp256k1.Hex
|
import fr.acinq.secp256k1.Hex
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import nostr.postr.toNpub
|
import nostr.postr.toNpub
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -969,16 +970,16 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class LocalCacheLiveData : LiveData<Set<Note>>(setOf<Note>()) {
|
class LocalCacheLiveData {
|
||||||
|
private val _newEventBundles = MutableSharedFlow<Set<Note>>()
|
||||||
|
val newEventBundles = _newEventBundles.asSharedFlow() // read-only public view
|
||||||
|
|
||||||
// Refreshes observers in batches.
|
// Refreshes observers in batches.
|
||||||
private val bundler = BundledInsert<Note>(300, Dispatchers.IO)
|
private val bundler = BundledInsert<Note>(300, Dispatchers.IO)
|
||||||
|
|
||||||
fun invalidateData(newNote: Note) {
|
fun invalidateData(newNote: Note) {
|
||||||
bundler.invalidateList(newNote) { bundledNewNotes ->
|
bundler.invalidateList(newNote) { bundledNewNotes ->
|
||||||
if (hasActiveObservers()) {
|
_newEventBundles.emit(bundledNewNotes)
|
||||||
postValue(bundledNewNotes)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,9 +137,6 @@ private fun RenderSeach(
|
|||||||
|
|
||||||
val onlineSearch = NostrSearchEventOrUserDataSource
|
val onlineSearch = NostrSearchEventOrUserDataSource
|
||||||
|
|
||||||
val dbState = LocalCache.live.observeAsState()
|
|
||||||
val db = dbState.value ?: return
|
|
||||||
|
|
||||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||||
|
|
||||||
// Create a channel for processing search queries.
|
// Create a channel for processing search queries.
|
||||||
@@ -147,10 +144,12 @@ private fun RenderSeach(
|
|||||||
Channel<String>(Channel.CONFLATED)
|
Channel<String>(Channel.CONFLATED)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(db) {
|
LaunchedEffect(Unit) {
|
||||||
withContext(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
if (searchBarViewModel.isSearching()) {
|
LocalCache.live.newEventBundles.collect {
|
||||||
searchBarViewModel.invalidateData()
|
if (searchBarViewModel.isSearching()) {
|
||||||
|
searchBarViewModel.invalidateData()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import kotlinx.coroutines.NonCancellable
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class is designed to have a waiting time between two calls of invalidate
|
* This class is designed to have a waiting time between two calls of invalidate
|
||||||
@@ -54,23 +54,26 @@ class BundledInsert<T>(
|
|||||||
val dispatcher: CoroutineDispatcher = Dispatchers.Default
|
val dispatcher: CoroutineDispatcher = Dispatchers.Default
|
||||||
) {
|
) {
|
||||||
private var onlyOneInBlock = AtomicBoolean()
|
private var onlyOneInBlock = AtomicBoolean()
|
||||||
private var atomicSet = AtomicReference<Set<T>>(setOf<T>())
|
private var queue = LinkedBlockingQueue<T>()
|
||||||
|
|
||||||
fun invalidateList(newObject: T, onUpdate: (Set<T>) -> Unit) {
|
|
||||||
atomicSet.updateAndGet() {
|
|
||||||
it + newObject
|
|
||||||
}
|
|
||||||
|
|
||||||
|
fun invalidateList(newObject: T, onUpdate: suspend (Set<T>) -> Unit) {
|
||||||
|
queue.put(newObject)
|
||||||
if (onlyOneInBlock.getAndSet(true)) {
|
if (onlyOneInBlock.getAndSet(true)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val scope = CoroutineScope(Job() + dispatcher)
|
val scope = CoroutineScope(Job() + dispatcher)
|
||||||
scope.launch {
|
scope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
onUpdate(atomicSet.getAndSet(emptySet()))
|
val mySet = mutableSetOf<T>()
|
||||||
|
queue.drainTo(mySet)
|
||||||
|
onUpdate(mySet)
|
||||||
|
|
||||||
delay(delay)
|
delay(delay)
|
||||||
onUpdate(atomicSet.getAndSet(emptySet()))
|
|
||||||
|
mySet.clear()
|
||||||
|
queue.drainTo(mySet)
|
||||||
|
onUpdate(mySet)
|
||||||
} finally {
|
} finally {
|
||||||
withContext(NonCancellable) {
|
withContext(NonCancellable) {
|
||||||
onlyOneInBlock.set(false)
|
onlyOneInBlock.set(false)
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.time.ExperimentalTime
|
|
||||||
|
|
||||||
val bottomNavigationItems = listOf(
|
val bottomNavigationItems = listOf(
|
||||||
Route.Home,
|
Route.Home,
|
||||||
@@ -136,9 +135,10 @@ fun AppBottomBar(navController: NavHostController, accountViewModel: AccountView
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTime::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun NotifiableIcon(route: Route, selected: Boolean, accountViewModel: AccountViewModel) {
|
private fun NotifiableIcon(route: Route, selected: Boolean, accountViewModel: AccountViewModel) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
Box(Modifier.size(if ("Home" == route.base) 25.dp else 23.dp)) {
|
Box(Modifier.size(if ("Home" == route.base) 25.dp else 23.dp)) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = route.icon),
|
painter = painterResource(id = route.icon),
|
||||||
@@ -150,15 +150,10 @@ private fun NotifiableIcon(route: Route, selected: Boolean, accountViewModel: Ac
|
|||||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||||
val account = accountState?.account ?: return
|
val account = accountState?.account ?: return
|
||||||
|
|
||||||
// Notification
|
|
||||||
val dbState = LocalCache.live.observeAsState()
|
|
||||||
val db = dbState.value ?: return
|
|
||||||
|
|
||||||
val notifState = NotificationCache.live.observeAsState()
|
val notifState = NotificationCache.live.observeAsState()
|
||||||
val notif = notifState.value ?: return
|
val notif = notifState.value ?: return
|
||||||
|
|
||||||
var hasNewItems by remember { mutableStateOf<Boolean>(false) }
|
var hasNewItems by remember { mutableStateOf<Boolean>(false) }
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
LaunchedEffect(key1 = notif) {
|
LaunchedEffect(key1 = notif) {
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
@@ -166,9 +161,11 @@ private fun NotifiableIcon(route: Route, selected: Boolean, accountViewModel: Ac
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(key1 = db) {
|
LaunchedEffect(Unit) {
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
hasNewItems = route.hasNewItems(account, notif.cache, db)
|
LocalCache.live.newEventBundles.collect {
|
||||||
|
hasNewItems = route.hasNewItems(account, notif.cache, it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import androidx.compose.material.TopAppBar
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
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
|
||||||
@@ -40,6 +39,8 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import coil.Coil
|
import coil.Coil
|
||||||
@@ -71,43 +72,45 @@ import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.RelayPoolViewModel
|
import com.vitorpamplona.amethyst.ui.screen.RelayPoolViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SpinnerSelectionDialog
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SpinnerSelectionDialog
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppTopBar(navController: NavHostController, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
fun AppTopBar(followLists: FollowListViewModel, navController: NavHostController, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
||||||
when (currentRoute(navController)?.substringBefore("?")) {
|
when (currentRoute(navController)?.substringBefore("?")) {
|
||||||
// Route.Profile.route -> TopBarWithBackButton(navController)
|
// Route.Profile.route -> TopBarWithBackButton(navController)
|
||||||
Route.Home.base -> HomeTopBar(scaffoldState, accountViewModel)
|
Route.Home.base -> HomeTopBar(followLists, scaffoldState, accountViewModel)
|
||||||
Route.Video.base -> StoriesTopBar(scaffoldState, accountViewModel)
|
Route.Video.base -> StoriesTopBar(followLists, scaffoldState, accountViewModel)
|
||||||
Route.Notification.base -> NotificationTopBar(scaffoldState, accountViewModel)
|
Route.Notification.base -> NotificationTopBar(followLists, scaffoldState, accountViewModel)
|
||||||
else -> MainTopBar(scaffoldState, accountViewModel)
|
else -> MainTopBar(scaffoldState, accountViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StoriesTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
fun StoriesTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
||||||
GenericTopBar(scaffoldState, accountViewModel) { account ->
|
GenericTopBar(scaffoldState, accountViewModel) { account ->
|
||||||
FollowList(account.defaultStoriesFollowList, account.userProfile(), true) { listName ->
|
FollowList(followLists, account.defaultStoriesFollowList, account.userProfile(), true) { listName ->
|
||||||
account.changeDefaultStoriesFollowList(listName)
|
account.changeDefaultStoriesFollowList(listName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
fun HomeTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
||||||
GenericTopBar(scaffoldState, accountViewModel) { account ->
|
GenericTopBar(scaffoldState, accountViewModel) { account ->
|
||||||
FollowList(account.defaultHomeFollowList, account.userProfile(), false) { listName ->
|
FollowList(followLists, account.defaultHomeFollowList, account.userProfile(), false) { listName ->
|
||||||
account.changeDefaultHomeFollowList(listName)
|
account.changeDefaultHomeFollowList(listName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NotificationTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
fun NotificationTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
||||||
GenericTopBar(scaffoldState, accountViewModel) { account ->
|
GenericTopBar(scaffoldState, accountViewModel) { account ->
|
||||||
FollowList(account.defaultNotificationFollowList, account.userProfile(), true) { listName ->
|
FollowList(followLists, account.defaultNotificationFollowList, account.userProfile(), true) { listName ->
|
||||||
account.changeDefaultNotificationFollowList(listName)
|
account.changeDefaultNotificationFollowList(listName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,30 +240,19 @@ private fun LoggedInUserPictureDrawer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FollowList(listName: String, loggedIn: User, withGlobal: Boolean, onChange: (String) -> Unit) {
|
fun FollowList(followListsModel: FollowListViewModel, listName: String, loggedIn: User, withGlobal: Boolean, onChange: (String) -> Unit) {
|
||||||
// Notification
|
|
||||||
val dbState = LocalCache.live.observeAsState()
|
|
||||||
val db = dbState.value ?: return
|
|
||||||
|
|
||||||
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))
|
||||||
|
|
||||||
val defaultOptions = if (withGlobal) listOf(kind3Follow, globalFollow) else listOf(kind3Follow)
|
val defaultOptions = if (withGlobal) listOf(kind3Follow, globalFollow) else listOf(kind3Follow)
|
||||||
|
|
||||||
var followLists by remember { mutableStateOf(defaultOptions) }
|
val followLists = remember(followListsModel.followLists) {
|
||||||
val followNames = remember { derivedStateOf { followLists.map { it.second } } }
|
(defaultOptions + followListsModel.followLists)
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(key1 = db) {
|
val followNames = remember(followLists) {
|
||||||
withContext(Dispatchers.IO) {
|
derivedStateOf {
|
||||||
followLists = defaultOptions + LocalCache.addressables.mapNotNull {
|
followLists.map { it.second }
|
||||||
val event = (it.value.event as? PeopleListEvent)
|
|
||||||
// Has to have an list
|
|
||||||
if (event != null && event.pubKey == loggedIn.pubkeyHex && (event.tags.size > 1 || event.content.length > 50)) {
|
|
||||||
Pair(event.dTag(), event.dTag())
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}.sortedBy { it.second }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,6 +265,62 @@ fun FollowList(listName: String, loggedIn: User, withGlobal: Boolean, onChange:
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class FollowListViewModel : ViewModel() {
|
||||||
|
var followLists by mutableStateOf<List<Pair<String, String>>>(emptyList())
|
||||||
|
var account: Account? = null
|
||||||
|
|
||||||
|
fun load(account: Account?) {
|
||||||
|
this.account = account
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refresh() {
|
||||||
|
val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||||
|
scope.launch {
|
||||||
|
refreshFollows()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun refreshFollows() {
|
||||||
|
val myAccount = account ?: return
|
||||||
|
|
||||||
|
val newFollowLists = LocalCache.addressables.mapNotNull {
|
||||||
|
val event = (it.value.event as? PeopleListEvent)
|
||||||
|
// Has to have an list
|
||||||
|
if (event != null && event.pubKey == myAccount.userProfile().pubkeyHex && (event.tags.size > 1 || event.content.length > 50)) {
|
||||||
|
Pair(event.dTag(), event.dTag())
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}.sortedBy { it.second }
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if (followLists != newFollowLists) {
|
||||||
|
followLists = newFollowLists
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var collectorJob: Job? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
collectorJob = viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
LocalCache.live.newEventBundles.collect { newNotes ->
|
||||||
|
newNotes.forEach {
|
||||||
|
if (it.event is PeopleListEvent) {
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
collectorJob?.cancel()
|
||||||
|
super.onCleared()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SimpleTextSpinner(
|
fun SimpleTextSpinner(
|
||||||
placeholder: String,
|
placeholder: String,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.screen
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
@@ -222,17 +223,19 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val cacheListener: (Set<Note>) -> Unit = { newNotes ->
|
var collectorJob: Job? = null
|
||||||
if (localFilter is AdditiveFeedFilter && _feedContent.value is CardFeedState.Loaded) {
|
|
||||||
invalidateInsertData(newNotes)
|
|
||||||
} else {
|
|
||||||
// Refresh Everything
|
|
||||||
invalidateData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
LocalCache.live.observeForever(cacheListener)
|
collectorJob = viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
LocalCache.live.newEventBundles.collect { newNotes ->
|
||||||
|
if (localFilter is AdditiveFeedFilter && _feedContent.value is CardFeedState.Loaded) {
|
||||||
|
invalidateInsertData(newNotes)
|
||||||
|
} else {
|
||||||
|
// Refresh Everything
|
||||||
|
invalidateData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
@@ -242,7 +245,7 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
|||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
clear()
|
clear()
|
||||||
LocalCache.live.removeObserver(cacheListener)
|
collectorJob?.cancel()
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.screen
|
|||||||
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.ui.components.BundledInsert
|
import com.vitorpamplona.amethyst.ui.components.BundledInsert
|
||||||
@@ -124,21 +125,23 @@ abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val cacheListener: (Set<Note>) -> Unit = { newNotes ->
|
var collectorJob: Job? = null
|
||||||
if (localFilter is AdditiveFeedFilter && _feedContent.value is FeedState.Loaded) {
|
|
||||||
invalidateInsertData(newNotes)
|
init {
|
||||||
} else {
|
collectorJob = viewModelScope.launch(Dispatchers.IO) {
|
||||||
// Refresh Everything
|
LocalCache.live.newEventBundles.collect { newNotes ->
|
||||||
invalidateData()
|
if (localFilter is AdditiveFeedFilter && _feedContent.value is FeedState.Loaded) {
|
||||||
|
invalidateInsertData(newNotes)
|
||||||
|
} else {
|
||||||
|
// Refresh Everything
|
||||||
|
invalidateData()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
|
||||||
LocalCache.live.observeForever(cacheListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
LocalCache.live.removeObserver(cacheListener)
|
collectorJob?.cancel()
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.screen
|
|||||||
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
||||||
@@ -67,16 +68,18 @@ open class LnZapFeedViewModel(val dataSource: FeedFilter<Pair<Note, Note>>) : Vi
|
|||||||
bundler.invalidate()
|
bundler.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val cacheListener: (Set<Note>) -> Unit = { newNotes ->
|
var collectorJob: Job? = null
|
||||||
invalidateData()
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
LocalCache.live.observeForever(cacheListener)
|
collectorJob = viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
LocalCache.live.newEventBundles.collect { newNotes ->
|
||||||
|
invalidateData()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
LocalCache.live.removeObserver(cacheListener)
|
collectorJob?.cancel()
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package com.vitorpamplona.amethyst.ui.screen
|
|||||||
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
||||||
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
|
||||||
@@ -72,16 +72,18 @@ open class UserFeedViewModel(val dataSource: FeedFilter<User>) : ViewModel() {
|
|||||||
bundler.invalidate()
|
bundler.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val cacheListener: (Set<Note>) -> Unit = { newNotes ->
|
var collectorJob: Job? = null
|
||||||
invalidateData()
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
LocalCache.live.observeForever(cacheListener)
|
collectorJob = viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
LocalCache.live.newEventBundles.collect { newNotes ->
|
||||||
|
invalidateData()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
LocalCache.live.removeObserver(cacheListener)
|
collectorJob?.cancel()
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,11 @@ import androidx.compose.material.rememberScaffoldState
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import com.vitorpamplona.amethyst.ui.buttons.ChannelFabColumn
|
import com.vitorpamplona.amethyst.ui.buttons.ChannelFabColumn
|
||||||
@@ -50,6 +53,12 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
|
|||||||
skipHalfExpanded = true
|
skipHalfExpanded = true
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||||
|
val account = remember(accountState) { accountState?.account }
|
||||||
|
|
||||||
|
val followLists: FollowListViewModel = viewModel()
|
||||||
|
followLists.load(account)
|
||||||
|
|
||||||
ModalBottomSheetLayout(
|
ModalBottomSheetLayout(
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
sheetContent = {
|
sheetContent = {
|
||||||
@@ -64,7 +73,7 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
|
|||||||
AppBottomBar(navController, accountViewModel)
|
AppBottomBar(navController, accountViewModel)
|
||||||
},
|
},
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopBar(navController, scaffoldState, accountViewModel)
|
AppTopBar(followLists, navController, scaffoldState, accountViewModel)
|
||||||
},
|
},
|
||||||
drawerContent = {
|
drawerContent = {
|
||||||
DrawerContent(navController, scaffoldState, sheetState, accountViewModel)
|
DrawerContent(navController, scaffoldState, sheetState, accountViewModel)
|
||||||
|
|||||||
@@ -191,19 +191,17 @@ private fun SearchBar(accountViewModel: AccountViewModel, navController: NavCont
|
|||||||
|
|
||||||
val onlineSearch = NostrSearchEventOrUserDataSource
|
val onlineSearch = NostrSearchEventOrUserDataSource
|
||||||
|
|
||||||
val dbState = LocalCache.live.observeAsState()
|
|
||||||
val db = dbState.value ?: return
|
|
||||||
|
|
||||||
// Create a channel for processing search queries.
|
// Create a channel for processing search queries.
|
||||||
val searchTextChanges = remember {
|
val searchTextChanges = remember {
|
||||||
CoroutineChannel<String>(CoroutineChannel.CONFLATED)
|
CoroutineChannel<String>(CoroutineChannel.CONFLATED)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(db) {
|
LaunchedEffect(Unit) {
|
||||||
withContext(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
if (searchBarViewModel.isSearching()) {
|
LocalCache.live.newEventBundles.collect {
|
||||||
println("Search Active")
|
if (searchBarViewModel.isSearching()) {
|
||||||
searchBarViewModel.invalidateData()
|
searchBarViewModel.invalidateData()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user