mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-10-05 18:52:41 +02:00
Moves follow list states to the AccountViewModel
This commit is contained in:
@@ -3149,6 +3149,17 @@ class Account(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getAllPeopleLists(): List<AddressableNote> = getAllPeopleLists(keyPair.pubKeyHex)
|
||||||
|
|
||||||
|
fun getAllPeopleLists(pubkey: HexKey): List<AddressableNote> =
|
||||||
|
LocalCache.addressables
|
||||||
|
.filter { _, addressableNote ->
|
||||||
|
val event = (addressableNote.event as? PeopleListEvent)
|
||||||
|
event != null &&
|
||||||
|
event.pubKey == pubkey &&
|
||||||
|
(event.hasAnyTaggedUser() || event.publicAndPrivateUserCache?.isNotEmpty() == true)
|
||||||
|
}
|
||||||
|
|
||||||
fun setHideDeleteRequestDialog() {
|
fun setHideDeleteRequestDialog() {
|
||||||
hideDeleteRequestDialog = true
|
hideDeleteRequestDialog = true
|
||||||
saveable.invalidateData()
|
saveable.invalidateData()
|
||||||
|
@@ -51,8 +51,6 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.Immutable
|
|
||||||
import androidx.compose.runtime.Stable
|
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -69,20 +67,14 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.map
|
import androidx.lifecycle.map
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import androidx.navigation.NavBackStackEntry
|
import androidx.navigation.NavBackStackEntry
|
||||||
import coil.Coil
|
import coil.Coil
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
|
||||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
import com.vitorpamplona.amethyst.model.FeatureSetType
|
||||||
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
|
||||||
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
|
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
|
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
|
||||||
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
|
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
|
||||||
@@ -100,7 +92,6 @@ import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
|
|||||||
import com.vitorpamplona.amethyst.service.NostrThreadDataSource
|
import com.vitorpamplona.amethyst.service.NostrThreadDataSource
|
||||||
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
|
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
|
||||||
import com.vitorpamplona.amethyst.service.NostrVideoDataSource
|
import com.vitorpamplona.amethyst.service.NostrVideoDataSource
|
||||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
|
||||||
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
||||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||||
import com.vitorpamplona.amethyst.ui.note.AmethystIcon
|
import com.vitorpamplona.amethyst.ui.note.AmethystIcon
|
||||||
@@ -117,6 +108,15 @@ import com.vitorpamplona.amethyst.ui.note.UserCompose
|
|||||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.LongCommunityHeader
|
import com.vitorpamplona.amethyst.ui.note.types.LongCommunityHeader
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.ShortCommunityHeader
|
import com.vitorpamplona.amethyst.ui.note.types.ShortCommunityHeader
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.CodeName
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.CodeNameType
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.CommunityName
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.FollowListState
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.GeoHashName
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.HashtagName
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.Name
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.PeopleListName
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.ResourceName
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.DislayGeoTagHeader
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.DislayGeoTagHeader
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.GeoHashActionOptions
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.GeoHashActionOptions
|
||||||
@@ -142,27 +142,11 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
|||||||
import com.vitorpamplona.ammolite.relays.Client
|
import com.vitorpamplona.ammolite.relays.Client
|
||||||
import com.vitorpamplona.ammolite.relays.RelayPool
|
import com.vitorpamplona.ammolite.relays.RelayPool
|
||||||
import com.vitorpamplona.quartz.events.ChatroomKey
|
import com.vitorpamplona.quartz.events.ChatroomKey
|
||||||
import com.vitorpamplona.quartz.events.ContactListEvent
|
|
||||||
import com.vitorpamplona.quartz.events.DeletionEvent
|
|
||||||
import com.vitorpamplona.quartz.events.MuteListEvent
|
|
||||||
import com.vitorpamplona.quartz.events.PeopleListEvent
|
import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
|
||||||
import kotlinx.coroutines.flow.combineTransform
|
|
||||||
import kotlinx.coroutines.flow.emitAll
|
|
||||||
import kotlinx.coroutines.flow.flow
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
|
||||||
import kotlinx.coroutines.flow.transformLatest
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppTopBar(
|
fun AppTopBar(
|
||||||
followLists: FollowListViewModel,
|
|
||||||
navEntryState: State<NavBackStackEntry?>,
|
navEntryState: State<NavBackStackEntry?>,
|
||||||
openDrawer: () -> Unit,
|
openDrawer: () -> Unit,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
@@ -184,14 +168,14 @@ fun AppTopBar(
|
|||||||
derivedStateOf { navEntryState.value?.arguments?.getString("id") }
|
derivedStateOf { navEntryState.value?.arguments?.getString("id") }
|
||||||
}
|
}
|
||||||
|
|
||||||
RenderTopRouteBar(currentRoute, id, followLists, openDrawer, accountViewModel, nav, navPopBack)
|
RenderTopRouteBar(currentRoute, id, accountViewModel.feedStates.feedListOptions, openDrawer, accountViewModel, nav, navPopBack)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RenderTopRouteBar(
|
private fun RenderTopRouteBar(
|
||||||
currentRoute: String?,
|
currentRoute: String?,
|
||||||
id: String?,
|
id: String?,
|
||||||
followLists: FollowListViewModel,
|
followLists: FollowListState,
|
||||||
openDrawer: () -> Unit,
|
openDrawer: () -> Unit,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (String) -> Unit,
|
nav: (String) -> Unit,
|
||||||
@@ -458,7 +442,7 @@ private fun ChannelTopBar(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StoriesTopBar(
|
fun StoriesTopBar(
|
||||||
followLists: FollowListViewModel,
|
followLists: FollowListState,
|
||||||
openDrawer: () -> Unit,
|
openDrawer: () -> Unit,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (String) -> Unit,
|
nav: (String) -> Unit,
|
||||||
@@ -477,7 +461,7 @@ fun StoriesTopBar(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeTopBar(
|
fun HomeTopBar(
|
||||||
followLists: FollowListViewModel,
|
followLists: FollowListState,
|
||||||
openDrawer: () -> Unit,
|
openDrawer: () -> Unit,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (String) -> Unit,
|
nav: (String) -> Unit,
|
||||||
@@ -500,7 +484,7 @@ fun HomeTopBar(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NotificationTopBar(
|
fun NotificationTopBar(
|
||||||
followLists: FollowListViewModel,
|
followLists: FollowListState,
|
||||||
openDrawer: () -> Unit,
|
openDrawer: () -> Unit,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (String) -> Unit,
|
nav: (String) -> Unit,
|
||||||
@@ -519,7 +503,7 @@ fun NotificationTopBar(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DiscoveryTopBar(
|
fun DiscoveryTopBar(
|
||||||
followLists: FollowListViewModel,
|
followLists: FollowListState,
|
||||||
openDrawer: () -> Unit,
|
openDrawer: () -> Unit,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (String) -> Unit,
|
nav: (String) -> Unit,
|
||||||
@@ -609,7 +593,7 @@ private fun LoggedInUserPictureDrawer(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FollowListWithRoutes(
|
fun FollowListWithRoutes(
|
||||||
followListsModel: FollowListViewModel,
|
followListsModel: FollowListState,
|
||||||
listName: String,
|
listName: String,
|
||||||
onChange: (CodeName) -> Unit,
|
onChange: (CodeName) -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -624,7 +608,7 @@ fun FollowListWithRoutes(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FollowListWithoutRoutes(
|
fun FollowListWithoutRoutes(
|
||||||
followListsModel: FollowListViewModel,
|
followListsModel: FollowListState,
|
||||||
listName: String,
|
listName: String,
|
||||||
onChange: (CodeName) -> Unit,
|
onChange: (CodeName) -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -637,191 +621,6 @@ fun FollowListWithoutRoutes(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class CodeNameType {
|
|
||||||
HARDCODED,
|
|
||||||
PEOPLE_LIST,
|
|
||||||
ROUTE,
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class Name {
|
|
||||||
abstract fun name(): String
|
|
||||||
|
|
||||||
open fun name(context: Context) = name()
|
|
||||||
}
|
|
||||||
|
|
||||||
class GeoHashName(
|
|
||||||
val geoHashTag: String,
|
|
||||||
) : Name() {
|
|
||||||
override fun name() = "/g/$geoHashTag"
|
|
||||||
}
|
|
||||||
|
|
||||||
class HashtagName(
|
|
||||||
val hashTag: String,
|
|
||||||
) : Name() {
|
|
||||||
override fun name() = "#$hashTag"
|
|
||||||
}
|
|
||||||
|
|
||||||
class ResourceName(
|
|
||||||
val resourceId: Int,
|
|
||||||
) : Name() {
|
|
||||||
override fun name() = " $resourceId " // Space to make sure it goes first
|
|
||||||
|
|
||||||
override fun name(context: Context) = stringRes(context, resourceId)
|
|
||||||
}
|
|
||||||
|
|
||||||
class PeopleListName(
|
|
||||||
val note: AddressableNote,
|
|
||||||
) : Name() {
|
|
||||||
override fun name() = (note.event as? PeopleListEvent)?.nameOrTitle() ?: note.dTag() ?: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
class CommunityName(
|
|
||||||
val note: AddressableNote,
|
|
||||||
) : Name() {
|
|
||||||
override fun name() = "/n/${(note.dTag() ?: "")}"
|
|
||||||
}
|
|
||||||
|
|
||||||
@Immutable data class CodeName(
|
|
||||||
val code: String,
|
|
||||||
val name: Name,
|
|
||||||
val type: CodeNameType,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Stable
|
|
||||||
class FollowListViewModel(
|
|
||||||
val account: Account,
|
|
||||||
) : ViewModel() {
|
|
||||||
val kind3Follow =
|
|
||||||
CodeName(KIND3_FOLLOWS, ResourceName(R.string.follow_list_kind3follows), CodeNameType.HARDCODED)
|
|
||||||
val globalFollow =
|
|
||||||
CodeName(GLOBAL_FOLLOWS, ResourceName(R.string.follow_list_global), CodeNameType.HARDCODED)
|
|
||||||
val muteListFollow =
|
|
||||||
CodeName(
|
|
||||||
MuteListEvent.blockListFor(account.userProfile().pubkeyHex),
|
|
||||||
ResourceName(R.string.follow_list_mute_list),
|
|
||||||
CodeNameType.HARDCODED,
|
|
||||||
)
|
|
||||||
val defaultLists = persistentListOf(kind3Follow, globalFollow, muteListFollow)
|
|
||||||
|
|
||||||
val livePeopleListsFlow: Flow<List<CodeName>> by lazy {
|
|
||||||
flow {
|
|
||||||
checkNotInMainThread()
|
|
||||||
|
|
||||||
emit(getPeopleLists())
|
|
||||||
emitAll(livePeopleListsFlowObservable)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPeopleLists(): List<CodeName> =
|
|
||||||
LocalCache.addressables
|
|
||||||
.mapNotNull { _, addressableNote ->
|
|
||||||
val event = (addressableNote.event as? PeopleListEvent)
|
|
||||||
// Has to have an list
|
|
||||||
if (
|
|
||||||
event != null &&
|
|
||||||
event.pubKey == account.userProfile().pubkeyHex &&
|
|
||||||
(event.hasAnyTaggedUser() || event.publicAndPrivateUserCache?.isNotEmpty() == true)
|
|
||||||
) {
|
|
||||||
CodeName(event.address().toTag(), PeopleListName(addressableNote), CodeNameType.PEOPLE_LIST)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}.sortedBy { it.name.name() }
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
val livePeopleListsFlowObservable: Flow<List<CodeName>> =
|
|
||||||
LocalCache.live.newEventBundles.transformLatest { newNotes ->
|
|
||||||
val hasNewList =
|
|
||||||
newNotes.any {
|
|
||||||
val noteEvent = it.event
|
|
||||||
|
|
||||||
noteEvent?.pubKey() == account.userProfile().pubkeyHex &&
|
|
||||||
(
|
|
||||||
(
|
|
||||||
noteEvent is PeopleListEvent ||
|
|
||||||
noteEvent is MuteListEvent ||
|
|
||||||
noteEvent is ContactListEvent
|
|
||||||
) ||
|
|
||||||
(
|
|
||||||
noteEvent is DeletionEvent &&
|
|
||||||
(
|
|
||||||
noteEvent.deleteEvents().any {
|
|
||||||
LocalCache.getNoteIfExists(it)?.event is PeopleListEvent
|
|
||||||
} ||
|
|
||||||
noteEvent.deleteAddresses().any {
|
|
||||||
it.kind == PeopleListEvent.KIND
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasNewList) {
|
|
||||||
emit(getPeopleLists())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
val liveKind3FollowsFlow: Flow<List<CodeName>> =
|
|
||||||
account.liveKind3FollowsFlow.transformLatest {
|
|
||||||
checkNotInMainThread()
|
|
||||||
|
|
||||||
val communities =
|
|
||||||
it.communities.mapNotNull {
|
|
||||||
LocalCache.checkGetOrCreateAddressableNote(it)?.let { communityNote ->
|
|
||||||
CodeName(
|
|
||||||
"Community/${communityNote.idHex}",
|
|
||||||
CommunityName(communityNote),
|
|
||||||
CodeNameType.ROUTE,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val hashtags =
|
|
||||||
it.hashtags.map {
|
|
||||||
CodeName("Hashtag/$it", HashtagName(it), CodeNameType.ROUTE)
|
|
||||||
}
|
|
||||||
|
|
||||||
val geotags =
|
|
||||||
it.geotags.map {
|
|
||||||
CodeName("Geohash/$it", GeoHashName(it), CodeNameType.ROUTE)
|
|
||||||
}
|
|
||||||
|
|
||||||
emit(
|
|
||||||
(communities + hashtags + geotags).sortedBy { it.name.name() },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val _kind3GlobalPeopleRoutes =
|
|
||||||
combineTransform(livePeopleListsFlow, liveKind3FollowsFlow) { myLivePeopleListsFlow, myLiveKind3FollowsFlow ->
|
|
||||||
checkNotInMainThread()
|
|
||||||
emit(
|
|
||||||
listOf(listOf(kind3Follow, globalFollow), myLivePeopleListsFlow, myLiveKind3FollowsFlow, listOf(muteListFollow))
|
|
||||||
.flatten()
|
|
||||||
.toImmutableList(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val kind3GlobalPeopleRoutes = _kind3GlobalPeopleRoutes.flowOn(Dispatchers.IO).stateIn(viewModelScope, SharingStarted.Eagerly, defaultLists)
|
|
||||||
|
|
||||||
private val _kind3GlobalPeople =
|
|
||||||
combineTransform(livePeopleListsFlow, liveKind3FollowsFlow) { myLivePeopleListsFlow, myLiveKind3FollowsFlow ->
|
|
||||||
checkNotInMainThread()
|
|
||||||
emit(
|
|
||||||
listOf(listOf(kind3Follow, globalFollow), myLivePeopleListsFlow, listOf(muteListFollow))
|
|
||||||
.flatten()
|
|
||||||
.toImmutableList(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val kind3GlobalPeople = _kind3GlobalPeople.flowOn(Dispatchers.IO).stateIn(viewModelScope, SharingStarted.Eagerly, defaultLists)
|
|
||||||
|
|
||||||
class Factory(
|
|
||||||
val account: Account,
|
|
||||||
) : ViewModelProvider.Factory {
|
|
||||||
override fun <FollowListViewModel : ViewModel> create(modelClass: Class<FollowListViewModel>): FollowListViewModel = FollowListViewModel(account) as FollowListViewModel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SimpleTextSpinner(
|
fun SimpleTextSpinner(
|
||||||
placeholderCode: String,
|
placeholderCode: String,
|
||||||
|
@@ -0,0 +1,240 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
|
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||||
|
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.quartz.events.ContactListEvent
|
||||||
|
import com.vitorpamplona.quartz.events.DeletionEvent
|
||||||
|
import com.vitorpamplona.quartz.events.MuteListEvent
|
||||||
|
import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.combineTransform
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.flow.transformLatest
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
class FollowListState(
|
||||||
|
val account: Account,
|
||||||
|
val viewModelScope: CoroutineScope,
|
||||||
|
) {
|
||||||
|
val kind3Follow =
|
||||||
|
CodeName(
|
||||||
|
KIND3_FOLLOWS,
|
||||||
|
ResourceName(R.string.follow_list_kind3follows),
|
||||||
|
CodeNameType.HARDCODED,
|
||||||
|
)
|
||||||
|
val globalFollow =
|
||||||
|
CodeName(GLOBAL_FOLLOWS, ResourceName(R.string.follow_list_global), CodeNameType.HARDCODED)
|
||||||
|
val muteListFollow =
|
||||||
|
CodeName(
|
||||||
|
MuteListEvent.blockListFor(account.userProfile().pubkeyHex),
|
||||||
|
ResourceName(R.string.follow_list_mute_list),
|
||||||
|
CodeNameType.HARDCODED,
|
||||||
|
)
|
||||||
|
val defaultLists = persistentListOf(kind3Follow, globalFollow, muteListFollow)
|
||||||
|
|
||||||
|
fun getPeopleLists(): List<CodeName> =
|
||||||
|
account
|
||||||
|
.getAllPeopleLists()
|
||||||
|
.map {
|
||||||
|
CodeName(
|
||||||
|
it.idHex,
|
||||||
|
PeopleListName(it),
|
||||||
|
CodeNameType.PEOPLE_LIST,
|
||||||
|
)
|
||||||
|
}.sortedBy { it.name.name() }
|
||||||
|
|
||||||
|
val livePeopleListsFlow = MutableStateFlow(emptyList<CodeName>())
|
||||||
|
|
||||||
|
fun updateFeedWith(newNotes: Set<Note>) {
|
||||||
|
checkNotInMainThread()
|
||||||
|
|
||||||
|
val hasNewList =
|
||||||
|
newNotes.any {
|
||||||
|
val noteEvent = it.event
|
||||||
|
|
||||||
|
noteEvent?.pubKey() == account.userProfile().pubkeyHex &&
|
||||||
|
(
|
||||||
|
(
|
||||||
|
noteEvent is PeopleListEvent ||
|
||||||
|
noteEvent is MuteListEvent ||
|
||||||
|
noteEvent is ContactListEvent
|
||||||
|
) ||
|
||||||
|
(
|
||||||
|
noteEvent is DeletionEvent &&
|
||||||
|
(
|
||||||
|
noteEvent.deleteEvents().any {
|
||||||
|
LocalCache.getNoteIfExists(it)?.event is PeopleListEvent
|
||||||
|
} ||
|
||||||
|
noteEvent.deleteAddresses().any {
|
||||||
|
it.kind == PeopleListEvent.KIND
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasNewList) {
|
||||||
|
livePeopleListsFlow.tryEmit(getPeopleLists())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
val liveKind3FollowsFlow: Flow<List<CodeName>> =
|
||||||
|
account.liveKind3Follows.transformLatest {
|
||||||
|
checkNotInMainThread()
|
||||||
|
|
||||||
|
val communities =
|
||||||
|
it.communities.mapNotNull {
|
||||||
|
LocalCache.checkGetOrCreateAddressableNote(it)?.let { communityNote ->
|
||||||
|
CodeName(
|
||||||
|
"Community/${communityNote.idHex}",
|
||||||
|
CommunityName(communityNote),
|
||||||
|
CodeNameType.ROUTE,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val hashtags =
|
||||||
|
it.hashtags.map {
|
||||||
|
CodeName("Hashtag/$it", HashtagName(it), CodeNameType.ROUTE)
|
||||||
|
}
|
||||||
|
|
||||||
|
val geotags =
|
||||||
|
it.geotags.map {
|
||||||
|
CodeName("Geohash/$it", GeoHashName(it), CodeNameType.ROUTE)
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(
|
||||||
|
(communities + hashtags + geotags).sortedBy { it.name.name() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _kind3GlobalPeopleRoutes =
|
||||||
|
combineTransform(
|
||||||
|
livePeopleListsFlow,
|
||||||
|
liveKind3FollowsFlow,
|
||||||
|
) { myLivePeopleListsFlow, myLiveKind3FollowsFlow ->
|
||||||
|
checkNotInMainThread()
|
||||||
|
emit(
|
||||||
|
listOf(
|
||||||
|
listOf(kind3Follow, globalFollow),
|
||||||
|
myLivePeopleListsFlow,
|
||||||
|
myLiveKind3FollowsFlow,
|
||||||
|
listOf(muteListFollow),
|
||||||
|
).flatten().toImmutableList(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _kind3GlobalPeople =
|
||||||
|
combineTransform(
|
||||||
|
livePeopleListsFlow,
|
||||||
|
liveKind3FollowsFlow,
|
||||||
|
) { myLivePeopleListsFlow, myLiveKind3FollowsFlow ->
|
||||||
|
checkNotInMainThread()
|
||||||
|
emit(
|
||||||
|
listOf(
|
||||||
|
listOf(kind3Follow, globalFollow),
|
||||||
|
myLivePeopleListsFlow,
|
||||||
|
listOf(muteListFollow),
|
||||||
|
).flatten().toImmutableList(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val kind3GlobalPeopleRoutes = _kind3GlobalPeopleRoutes.flowOn(Dispatchers.Default).stateIn(viewModelScope, SharingStarted.Eagerly, defaultLists)
|
||||||
|
val kind3GlobalPeople = _kind3GlobalPeople.flowOn(Dispatchers.Default).stateIn(viewModelScope, SharingStarted.Eagerly, defaultLists)
|
||||||
|
|
||||||
|
suspend fun initializeSuspend() {
|
||||||
|
checkNotInMainThread()
|
||||||
|
|
||||||
|
livePeopleListsFlow.emit(getPeopleLists())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class CodeNameType {
|
||||||
|
HARDCODED,
|
||||||
|
PEOPLE_LIST,
|
||||||
|
ROUTE,
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class Name {
|
||||||
|
abstract fun name(): String
|
||||||
|
|
||||||
|
open fun name(context: Context) = name()
|
||||||
|
}
|
||||||
|
|
||||||
|
class GeoHashName(
|
||||||
|
val geoHashTag: String,
|
||||||
|
) : Name() {
|
||||||
|
override fun name() = "/g/$geoHashTag"
|
||||||
|
}
|
||||||
|
|
||||||
|
class HashtagName(
|
||||||
|
val hashTag: String,
|
||||||
|
) : Name() {
|
||||||
|
override fun name() = "#$hashTag"
|
||||||
|
}
|
||||||
|
|
||||||
|
class ResourceName(
|
||||||
|
val resourceId: Int,
|
||||||
|
) : Name() {
|
||||||
|
override fun name() = " $resourceId " // Space to make sure it goes first
|
||||||
|
|
||||||
|
override fun name(context: Context) = stringRes(context, resourceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
class PeopleListName(
|
||||||
|
val note: AddressableNote,
|
||||||
|
) : Name() {
|
||||||
|
override fun name() = (note.event as? PeopleListEvent)?.nameOrTitle() ?: note.dTag() ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
class CommunityName(
|
||||||
|
val note: AddressableNote,
|
||||||
|
) : Name() {
|
||||||
|
override fun name() = "/n/${(note.dTag() ?: "")}"
|
||||||
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class CodeName(
|
||||||
|
val code: String,
|
||||||
|
val name: Name,
|
||||||
|
val type: CodeNameType,
|
||||||
|
)
|
@@ -34,6 +34,7 @@ import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter
|
|||||||
import com.vitorpamplona.amethyst.ui.dal.NotificationFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.NotificationFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.VideoFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.VideoFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
|
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.FollowListState
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CardFeedContentState
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CardFeedContentState
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationSummaryState
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationSummaryState
|
||||||
|
|
||||||
@@ -57,8 +58,11 @@ class AccountFeedContentStates(
|
|||||||
val notifications = CardFeedContentState(NotificationFeedFilter(accountViewModel.account), accountViewModel.viewModelScope)
|
val notifications = CardFeedContentState(NotificationFeedFilter(accountViewModel.account), accountViewModel.viewModelScope)
|
||||||
val notificationSummary = NotificationSummaryState(accountViewModel.account)
|
val notificationSummary = NotificationSummaryState(accountViewModel.account)
|
||||||
|
|
||||||
|
val feedListOptions = FollowListState(accountViewModel.account, accountViewModel.viewModelScope)
|
||||||
|
|
||||||
suspend fun init() {
|
suspend fun init() {
|
||||||
notificationSummary.initializeSuspend()
|
notificationSummary.initializeSuspend()
|
||||||
|
feedListOptions.initializeSuspend()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateFeedsWith(newNotes: Set<Note>) {
|
fun updateFeedsWith(newNotes: Set<Note>) {
|
||||||
@@ -78,6 +82,8 @@ class AccountFeedContentStates(
|
|||||||
|
|
||||||
notifications.updateFeedWith(newNotes)
|
notifications.updateFeedWith(newNotes)
|
||||||
notificationSummary.invalidateInsertData(newNotes)
|
notificationSummary.invalidateInsertData(newNotes)
|
||||||
|
|
||||||
|
feedListOptions.updateFeedWith(newNotes)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun destroy() {
|
fun destroy() {
|
||||||
@@ -97,5 +103,7 @@ class AccountFeedContentStates(
|
|||||||
|
|
||||||
notifications.destroy()
|
notifications.destroy()
|
||||||
notificationSummary.destroy()
|
notificationSummary.destroy()
|
||||||
|
|
||||||
|
// feedListOptions.destroy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -73,7 +73,6 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import androidx.navigation.NavBackStackEntry
|
import androidx.navigation.NavBackStackEntry
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
@@ -91,7 +90,6 @@ import com.vitorpamplona.amethyst.ui.navigation.AppBottomBar
|
|||||||
import com.vitorpamplona.amethyst.ui.navigation.AppNavigation
|
import com.vitorpamplona.amethyst.ui.navigation.AppNavigation
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.AppTopBar
|
import com.vitorpamplona.amethyst.ui.navigation.AppTopBar
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.DrawerContent
|
import com.vitorpamplona.amethyst.ui.navigation.DrawerContent
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.FollowListViewModel
|
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.Route.Companion.InvertedLayouts
|
import com.vitorpamplona.amethyst.ui.navigation.Route.Companion.InvertedLayouts
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.getRouteWithArguments
|
import com.vitorpamplona.amethyst.ui.navigation.getRouteWithArguments
|
||||||
@@ -166,12 +164,6 @@ fun MainScreen(
|
|||||||
DisplayErrorMessages(accountViewModel)
|
DisplayErrorMessages(accountViewModel)
|
||||||
DisplayNotifyMessages(accountViewModel, nav)
|
DisplayNotifyMessages(accountViewModel, nav)
|
||||||
|
|
||||||
val followListsViewModel: FollowListViewModel =
|
|
||||||
viewModel(
|
|
||||||
key = "FollowListViewModel",
|
|
||||||
factory = FollowListViewModel.Factory(accountViewModel.account),
|
|
||||||
)
|
|
||||||
|
|
||||||
val navBottomRow =
|
val navBottomRow =
|
||||||
remember(navController, accountViewModel) {
|
remember(navController, accountViewModel) {
|
||||||
{ route: Route, selected: Boolean ->
|
{ route: Route, selected: Boolean ->
|
||||||
@@ -228,7 +220,6 @@ fun MainScreen(
|
|||||||
navPopBack = navPopBack,
|
navPopBack = navPopBack,
|
||||||
openDrawer = { scope.launch { drawerState.open() } },
|
openDrawer = { scope.launch { drawerState.open() } },
|
||||||
accountStateViewModel = accountStateViewModel,
|
accountStateViewModel = accountStateViewModel,
|
||||||
followListsViewModel = followListsViewModel,
|
|
||||||
sharedPreferencesViewModel = sharedPreferencesViewModel,
|
sharedPreferencesViewModel = sharedPreferencesViewModel,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
@@ -265,7 +256,6 @@ private fun MainScaffold(
|
|||||||
navPopBack: () -> Unit,
|
navPopBack: () -> Unit,
|
||||||
openDrawer: () -> Unit,
|
openDrawer: () -> Unit,
|
||||||
accountStateViewModel: AccountStateViewModel,
|
accountStateViewModel: AccountStateViewModel,
|
||||||
followListsViewModel: FollowListViewModel,
|
|
||||||
sharedPreferencesViewModel: SharedPreferencesViewModel,
|
sharedPreferencesViewModel: SharedPreferencesViewModel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (String) -> Unit,
|
nav: (String) -> Unit,
|
||||||
@@ -350,7 +340,6 @@ private fun MainScaffold(
|
|||||||
) { isVisible ->
|
) { isVisible ->
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
AppTopBar(
|
AppTopBar(
|
||||||
followListsViewModel,
|
|
||||||
navState,
|
navState,
|
||||||
openDrawer,
|
openDrawer,
|
||||||
accountViewModel,
|
accountViewModel,
|
||||||
|
Reference in New Issue
Block a user