- Adds an Around ME feed
- Refactors location to operate as a flow - Refactors FeedStructures to prepare for custom feeds - Moves Account to operate feeds with location
This commit is contained in:
@@ -32,6 +32,7 @@ import androidx.security.crypto.EncryptedSharedPreferences
|
|||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.disk.DiskCache
|
import coil.disk.DiskCache
|
||||||
import coil.memory.MemoryCache
|
import coil.memory.MemoryCache
|
||||||
|
import com.vitorpamplona.amethyst.service.LocationState
|
||||||
import com.vitorpamplona.amethyst.service.playback.VideoCache
|
import com.vitorpamplona.amethyst.service.playback.VideoCache
|
||||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -50,6 +51,7 @@ class Amethyst : Application() {
|
|||||||
|
|
||||||
// Service Manager is only active when the activity is active.
|
// Service Manager is only active when the activity is active.
|
||||||
val serviceManager = ServiceManager(applicationIOScope)
|
val serviceManager = ServiceManager(applicationIOScope)
|
||||||
|
val locationManager = LocationState(this, applicationIOScope)
|
||||||
|
|
||||||
override fun onTerminate() {
|
override fun onTerminate() {
|
||||||
super.onTerminate()
|
super.onTerminate()
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.model
|
package com.vitorpamplona.amethyst.model
|
||||||
|
|
||||||
|
import android.location.Location
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
@@ -28,6 +29,8 @@ import androidx.lifecycle.asLiveData
|
|||||||
import androidx.lifecycle.liveData
|
import androidx.lifecycle.liveData
|
||||||
import androidx.lifecycle.switchMap
|
import androidx.lifecycle.switchMap
|
||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
|
import com.fonfon.kgeohash.toGeoHash
|
||||||
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
import com.vitorpamplona.amethyst.BuildConfig
|
import com.vitorpamplona.amethyst.BuildConfig
|
||||||
import com.vitorpamplona.amethyst.service.FileHeader
|
import com.vitorpamplona.amethyst.service.FileHeader
|
||||||
import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource
|
import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource
|
||||||
@@ -119,10 +122,8 @@ import kotlinx.coroutines.flow.StateFlow
|
|||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.combineTransform
|
import kotlinx.coroutines.flow.combineTransform
|
||||||
import kotlinx.coroutines.flow.emitAll
|
import kotlinx.coroutines.flow.emitAll
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.mapLatest
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.transformLatest
|
import kotlinx.coroutines.flow.transformLatest
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
@@ -158,17 +159,19 @@ class Account(
|
|||||||
val transientPaymentRequests: MutableStateFlow<Set<PaymentRequest>> = MutableStateFlow(emptySet())
|
val transientPaymentRequests: MutableStateFlow<Set<PaymentRequest>> = MutableStateFlow(emptySet())
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
class LiveFollowLists(
|
class LiveFollowList(
|
||||||
val users: Set<String> = emptySet(),
|
val authors: Set<String> = emptySet(),
|
||||||
val usersPlusMe: Set<String>,
|
val authorsPlusMe: Set<String>,
|
||||||
val hashtags: Set<String> = emptySet(),
|
val hashtags: Set<String> = emptySet(),
|
||||||
val geotags: Set<String> = emptySet(),
|
val geotags: Set<String> = emptySet(),
|
||||||
val communities: Set<String> = emptySet(),
|
val addresses: Set<String> = emptySet(),
|
||||||
)
|
)
|
||||||
|
|
||||||
class ListNameNotePair(
|
class FeedsBaseFlows(
|
||||||
val listName: String,
|
val listName: String,
|
||||||
val event: GeneralListEvent?,
|
val peopleList: StateFlow<NoteState> = MutableStateFlow(NoteState(Note(" "))),
|
||||||
|
val kind3: StateFlow<Account.LiveFollowList?> = MutableStateFlow(null),
|
||||||
|
val location: StateFlow<Location?> = MutableStateFlow(null),
|
||||||
)
|
)
|
||||||
|
|
||||||
val connectToRelaysFlow =
|
val connectToRelaysFlow =
|
||||||
@@ -216,7 +219,7 @@ class Account(
|
|||||||
localRelayList: Set<String>,
|
localRelayList: Set<String>,
|
||||||
): List<RelaySetupInfo> {
|
): List<RelaySetupInfo> {
|
||||||
val newDMRelaySet = newDMRelayEvent?.relays()?.map { RelayUrlFormatter.normalize(it) }?.toSet() ?: emptySet()
|
val newDMRelaySet = newDMRelayEvent?.relays()?.map { RelayUrlFormatter.normalize(it) }?.toSet() ?: emptySet()
|
||||||
val searchRelaySet = (searchRelayEvent?.relays() ?: Constants.defaultSearchRelaySet).map { RelayUrlFormatter.normalize(it) }.toSet()
|
val searchRelaySet = (searchRelayEvent?.relays() ?: DefaultSearchRelayList).map { RelayUrlFormatter.normalize(it) }.toSet()
|
||||||
val nip65RelaySet =
|
val nip65RelaySet =
|
||||||
nip65RelayEvent?.relays()?.map {
|
nip65RelayEvent?.relays()?.map {
|
||||||
AdvertisedRelayListEvent.AdvertisedRelayInfo(
|
AdvertisedRelayListEvent.AdvertisedRelayInfo(
|
||||||
@@ -463,23 +466,26 @@ class Account(
|
|||||||
}.toTypedArray(),
|
}.toTypedArray(),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun buildFollowLists(latestContactList: ContactListEvent?): LiveFollowLists {
|
fun buildFollowLists(latestContactList: ContactListEvent?): LiveFollowList {
|
||||||
// makes sure the output include only valid p tags
|
// makes sure the output include only valid p tags
|
||||||
val verifiedFollowingUsers = latestContactList?.verifiedFollowKeySet() ?: emptySet()
|
val verifiedFollowingUsers = latestContactList?.verifiedFollowKeySet() ?: emptySet()
|
||||||
|
|
||||||
return LiveFollowLists(
|
return LiveFollowList(
|
||||||
verifiedFollowingUsers,
|
authors = verifiedFollowingUsers,
|
||||||
verifiedFollowingUsers + signer.pubKey,
|
authorsPlusMe = verifiedFollowingUsers + signer.pubKey,
|
||||||
latestContactList
|
hashtags =
|
||||||
?.unverifiedFollowTagSet()
|
latestContactList
|
||||||
?.map { it.lowercase() }
|
?.unverifiedFollowTagSet()
|
||||||
?.toSet() ?: emptySet(),
|
?.map { it.lowercase() }
|
||||||
latestContactList
|
?.toSet() ?: emptySet(),
|
||||||
?.unverifiedFollowGeohashSet()
|
geotags =
|
||||||
?.toSet() ?: emptySet(),
|
latestContactList
|
||||||
latestContactList
|
?.unverifiedFollowGeohashSet()
|
||||||
?.verifiedFollowAddressSet()
|
?.toSet() ?: emptySet(),
|
||||||
?.toSet() ?: emptySet(),
|
addresses =
|
||||||
|
latestContactList
|
||||||
|
?.verifiedFollowAddressSet()
|
||||||
|
?.toSet() ?: emptySet(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -514,7 +520,7 @@ class Account(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
val liveKind3FollowsFlow: Flow<LiveFollowLists> =
|
val liveKind3FollowsFlow: Flow<LiveFollowList> =
|
||||||
userProfile().flow().follows.stateFlow.transformLatest {
|
userProfile().flow().follows.stateFlow.transformLatest {
|
||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
emit(buildFollowLists(it.user.latestContactList))
|
emit(buildFollowLists(it.user.latestContactList))
|
||||||
@@ -529,80 +535,108 @@ class Account(
|
|||||||
buildFollowLists(userProfile().latestContactList ?: settings.backupContactList),
|
buildFollowLists(userProfile().latestContactList ?: settings.backupContactList),
|
||||||
)
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
fun loadFlowsFor(listName: String): FeedsBaseFlows =
|
||||||
private val liveHomeList: Flow<ListNameNotePair> =
|
when (listName) {
|
||||||
settings.defaultHomeFollowList.flatMapLatest { listName ->
|
GLOBAL_FOLLOWS -> FeedsBaseFlows(listName)
|
||||||
loadPeopleListFlowFromListName(listName)
|
KIND3_FOLLOWS -> FeedsBaseFlows(listName, kind3 = liveKind3Follows)
|
||||||
}
|
AROUND_ME ->
|
||||||
|
FeedsBaseFlows(
|
||||||
fun peopleListFromListNameStarter(listName: String): ListNameNotePair =
|
listName,
|
||||||
if (listName != GLOBAL_FOLLOWS && listName != KIND3_FOLLOWS) {
|
location = Amethyst.instance.locationManager.locationStateFlow,
|
||||||
val note = LocalCache.checkGetOrCreateAddressableNote(listName)
|
)
|
||||||
val noteEvent = note?.event as? GeneralListEvent
|
else -> {
|
||||||
ListNameNotePair(listName, noteEvent)
|
val note = LocalCache.checkGetOrCreateAddressableNote(listName)
|
||||||
} else {
|
if (note != null) {
|
||||||
ListNameNotePair(listName, null)
|
FeedsBaseFlows(
|
||||||
}
|
listName,
|
||||||
|
peopleList =
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
note
|
||||||
fun loadPeopleListFlowFromListName(listName: String): Flow<ListNameNotePair> =
|
.flow()
|
||||||
if (listName != GLOBAL_FOLLOWS && listName != KIND3_FOLLOWS) {
|
.metadata.stateFlow,
|
||||||
val note = LocalCache.checkGetOrCreateAddressableNote(listName)
|
)
|
||||||
note?.flow()?.metadata?.stateFlow?.mapLatest {
|
} else {
|
||||||
val noteEvent = it.note.event as? GeneralListEvent
|
FeedsBaseFlows(listName)
|
||||||
ListNameNotePair(listName, noteEvent)
|
}
|
||||||
} ?: MutableStateFlow(ListNameNotePair(listName, null))
|
|
||||||
} else {
|
|
||||||
MutableStateFlow(ListNameNotePair(listName, null))
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun combinePeopleList(
|
|
||||||
kind3Follows: LiveFollowLists,
|
|
||||||
peopleListFollows: ListNameNotePair,
|
|
||||||
): LiveFollowLists? =
|
|
||||||
if (peopleListFollows.listName == GLOBAL_FOLLOWS) {
|
|
||||||
null
|
|
||||||
} else if (peopleListFollows.listName == KIND3_FOLLOWS) {
|
|
||||||
kind3Follows
|
|
||||||
} else if (peopleListFollows.event == null) {
|
|
||||||
LiveFollowLists(usersPlusMe = setOf(signer.pubKey))
|
|
||||||
} else {
|
|
||||||
val result = waitToDecrypt(peopleListFollows.event)
|
|
||||||
if (result == null) {
|
|
||||||
LiveFollowLists(usersPlusMe = setOf(signer.pubKey))
|
|
||||||
} else {
|
|
||||||
result
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun combinePeopleListFlows(
|
suspend fun mapIntoFollowLists(
|
||||||
kind3FollowsSource: Flow<LiveFollowLists>,
|
listName: String,
|
||||||
peopleListFollowsSource: Flow<ListNameNotePair>,
|
kind3: LiveFollowList?,
|
||||||
): Flow<LiveFollowLists?> =
|
noteState: NoteState,
|
||||||
combineTransform(kind3FollowsSource, peopleListFollowsSource) { kind3Follows, peopleListFollows ->
|
location: Location?,
|
||||||
checkNotInMainThread()
|
): LiveFollowList? =
|
||||||
emit(combinePeopleList(kind3Follows, peopleListFollows))
|
if (listName == GLOBAL_FOLLOWS) {
|
||||||
|
println("AABBCC combinePeopleList $listName Global $listName")
|
||||||
|
null
|
||||||
|
} else if (listName == KIND3_FOLLOWS) {
|
||||||
|
println("AABBCC combinePeopleList $listName Kind3 $kind3")
|
||||||
|
kind3
|
||||||
|
} else if (listName == AROUND_ME) {
|
||||||
|
val hash = location?.toGeoHash(com.vitorpamplona.amethyst.ui.actions.GeohashPrecision.KM_5_X_5.digits)
|
||||||
|
if (hash != null) {
|
||||||
|
println("AABBCC combinePeopleList AROUND ME Started $listName Kind3 $kind3")
|
||||||
|
// 2 neighbors deep = 25x25km
|
||||||
|
val hashes =
|
||||||
|
listOf(hash.toString()) +
|
||||||
|
hash.adjacent
|
||||||
|
.map { listOf(it.toString()) + it.adjacent.map { it.toString() } }
|
||||||
|
.flatten()
|
||||||
|
.distinct()
|
||||||
|
|
||||||
|
println("AABBCC combinePeopleList AROUND ME Finished $listName Kind3 $kind3")
|
||||||
|
|
||||||
|
LiveFollowList(
|
||||||
|
authorsPlusMe = setOf(signer.pubKey),
|
||||||
|
geotags = hashes.toSet(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
LiveFollowList(authorsPlusMe = setOf(signer.pubKey))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val peopleList = noteState.note.event as? GeneralListEvent
|
||||||
|
println("AABBCC combinePeopleList $listName General List ${noteState.note.idHex}")
|
||||||
|
if (peopleList != null) {
|
||||||
|
waitToDecrypt(peopleList) ?: LiveFollowList(authorsPlusMe = setOf(signer.pubKey))
|
||||||
|
} else {
|
||||||
|
LiveFollowList(authorsPlusMe = setOf(signer.pubKey))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val liveHomeFollowListFlow: Flow<LiveFollowLists?> by lazy {
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
combinePeopleListFlows(liveKind3Follows, liveHomeList)
|
fun combinePeopleListFlows(peopleListFollowsSource: Flow<String>): Flow<LiveFollowList?> =
|
||||||
}
|
peopleListFollowsSource
|
||||||
|
.transformLatest { listName ->
|
||||||
|
val followList = loadFlowsFor(listName)
|
||||||
|
emitAll(
|
||||||
|
combine(followList.kind3, followList.peopleList, followList.location) { kind3, peopleList, location ->
|
||||||
|
mapIntoFollowLists(followList.listName, kind3, peopleList, location)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val liveHomeFollowLists: StateFlow<LiveFollowLists?> by lazy {
|
val liveHomeFollowLists: StateFlow<LiveFollowList?> by lazy {
|
||||||
liveHomeFollowListFlow
|
combinePeopleListFlows(settings.defaultHomeFollowList)
|
||||||
.flowOn(Dispatchers.Default)
|
.flowOn(Dispatchers.Default)
|
||||||
.stateIn(
|
.stateIn(
|
||||||
scope,
|
scope,
|
||||||
SharingStarted.Eagerly,
|
SharingStarted.Eagerly,
|
||||||
runBlocking {
|
runBlocking {
|
||||||
combinePeopleList(
|
loadAndCombineFlows(settings.defaultHomeFollowList.value)
|
||||||
liveKind3Follows.value,
|
|
||||||
peopleListFromListNameStarter(settings.defaultHomeFollowList.value),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun loadAndCombineFlows(listName: String): LiveFollowList? {
|
||||||
|
val flows = loadFlowsFor(listName)
|
||||||
|
return mapIntoFollowLists(
|
||||||
|
flows.listName,
|
||||||
|
flows.kind3.value,
|
||||||
|
flows.peopleList.value,
|
||||||
|
flows.location.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* filter onion and local host from write relays
|
* filter onion and local host from write relays
|
||||||
* for each user pubkey, a list of valid relays.
|
* for each user pubkey, a list of valid relays.
|
||||||
@@ -699,7 +733,7 @@ class Account(
|
|||||||
liveHomeFollowLists
|
liveHomeFollowLists
|
||||||
.transformLatest { followList ->
|
.transformLatest { followList ->
|
||||||
if (followList != null) {
|
if (followList != null) {
|
||||||
emitAll(combine(followList.usersPlusMe.map { getNIP65RelayListFlow(it) }) { it })
|
emitAll(combine(followList.authorsPlusMe.map { getNIP65RelayListFlow(it) }) { it })
|
||||||
} else {
|
} else {
|
||||||
emit(null)
|
emit(null)
|
||||||
}
|
}
|
||||||
@@ -726,53 +760,33 @@ class Account(
|
|||||||
scope,
|
scope,
|
||||||
SharingStarted.Eagerly,
|
SharingStarted.Eagerly,
|
||||||
authorsPerRelay(
|
authorsPerRelay(
|
||||||
liveHomeFollowLists.value?.usersPlusMe?.map { getNIP65RelayListNote(it) } ?: emptyList(),
|
liveHomeFollowLists.value?.authorsPlusMe?.map { getNIP65RelayListNote(it) } ?: emptyList(),
|
||||||
connectToRelays.value.filter { it.feedTypes.contains(FeedType.FOLLOWS) && it.read }.map { it.url },
|
connectToRelays.value.filter { it.feedTypes.contains(FeedType.FOLLOWS) && it.read }.map { it.url },
|
||||||
settings.torSettings.torType.value,
|
settings.torSettings.torType.value,
|
||||||
).ifEmpty { null },
|
).ifEmpty { null },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
val liveNotificationFollowLists: StateFlow<LiveFollowList?> by lazy {
|
||||||
private val liveNotificationList: Flow<ListNameNotePair> by lazy {
|
combinePeopleListFlows(settings.defaultNotificationFollowList)
|
||||||
settings.defaultNotificationFollowList.flatMapLatest { listName ->
|
|
||||||
loadPeopleListFlowFromListName(listName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val liveNotificationFollowLists: StateFlow<LiveFollowLists?> by lazy {
|
|
||||||
combinePeopleListFlows(liveKind3FollowsFlow, liveNotificationList)
|
|
||||||
.flowOn(Dispatchers.Default)
|
.flowOn(Dispatchers.Default)
|
||||||
.stateIn(
|
.stateIn(
|
||||||
scope,
|
scope,
|
||||||
SharingStarted.Eagerly,
|
SharingStarted.Eagerly,
|
||||||
runBlocking {
|
runBlocking {
|
||||||
combinePeopleList(
|
loadAndCombineFlows(settings.defaultNotificationFollowList.value)
|
||||||
liveKind3Follows.value,
|
|
||||||
peopleListFromListNameStarter(settings.defaultNotificationFollowList.value),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
val liveStoriesFollowLists: StateFlow<LiveFollowList?> by lazy {
|
||||||
private val liveStoriesList: Flow<ListNameNotePair> by lazy {
|
combinePeopleListFlows(settings.defaultStoriesFollowList)
|
||||||
settings.defaultStoriesFollowList.flatMapLatest { listName ->
|
|
||||||
loadPeopleListFlowFromListName(listName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val liveStoriesFollowLists: StateFlow<LiveFollowLists?> by lazy {
|
|
||||||
combinePeopleListFlows(liveKind3FollowsFlow, liveStoriesList)
|
|
||||||
.flowOn(Dispatchers.Default)
|
.flowOn(Dispatchers.Default)
|
||||||
.stateIn(
|
.stateIn(
|
||||||
scope,
|
scope,
|
||||||
SharingStarted.Eagerly,
|
SharingStarted.Eagerly,
|
||||||
runBlocking {
|
runBlocking {
|
||||||
combinePeopleList(
|
loadAndCombineFlows(settings.defaultStoriesFollowList.value)
|
||||||
liveKind3Follows.value,
|
|
||||||
peopleListFromListNameStarter(settings.defaultStoriesFollowList.value),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -782,7 +796,7 @@ class Account(
|
|||||||
liveStoriesFollowLists
|
liveStoriesFollowLists
|
||||||
.transformLatest { followList ->
|
.transformLatest { followList ->
|
||||||
if (followList != null) {
|
if (followList != null) {
|
||||||
emitAll(combine(followList.usersPlusMe.map { getNIP65RelayListFlow(it) }) { it })
|
emitAll(combine(followList.authorsPlusMe.map { getNIP65RelayListFlow(it) }) { it })
|
||||||
} else {
|
} else {
|
||||||
emit(null)
|
emit(null)
|
||||||
}
|
}
|
||||||
@@ -809,31 +823,21 @@ class Account(
|
|||||||
scope,
|
scope,
|
||||||
SharingStarted.Eagerly,
|
SharingStarted.Eagerly,
|
||||||
authorsPerRelay(
|
authorsPerRelay(
|
||||||
liveStoriesFollowLists.value?.usersPlusMe?.map { getNIP65RelayListNote(it) } ?: emptyList(),
|
liveStoriesFollowLists.value?.authorsPlusMe?.map { getNIP65RelayListNote(it) } ?: emptyList(),
|
||||||
connectToRelays.value.filter { it.feedTypes.contains(FeedType.FOLLOWS) && it.read }.map { it.url },
|
connectToRelays.value.filter { it.feedTypes.contains(FeedType.FOLLOWS) && it.read }.map { it.url },
|
||||||
settings.torSettings.torType.value,
|
settings.torSettings.torType.value,
|
||||||
).ifEmpty { null },
|
).ifEmpty { null },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
val liveDiscoveryFollowLists: StateFlow<LiveFollowList?> by lazy {
|
||||||
private val liveDiscoveryList: Flow<ListNameNotePair> by lazy {
|
combinePeopleListFlows(settings.defaultDiscoveryFollowList)
|
||||||
settings.defaultDiscoveryFollowList.flatMapLatest { listName ->
|
|
||||||
loadPeopleListFlowFromListName(listName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val liveDiscoveryFollowLists: StateFlow<LiveFollowLists?> by lazy {
|
|
||||||
combinePeopleListFlows(liveKind3FollowsFlow, liveDiscoveryList)
|
|
||||||
.flowOn(Dispatchers.Default)
|
.flowOn(Dispatchers.Default)
|
||||||
.stateIn(
|
.stateIn(
|
||||||
scope,
|
scope,
|
||||||
SharingStarted.Eagerly,
|
SharingStarted.Eagerly,
|
||||||
runBlocking {
|
runBlocking {
|
||||||
combinePeopleList(
|
loadAndCombineFlows(settings.defaultDiscoveryFollowList.value)
|
||||||
liveKind3Follows.value,
|
|
||||||
peopleListFromListNameStarter(settings.defaultDiscoveryFollowList.value),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -843,7 +847,7 @@ class Account(
|
|||||||
liveDiscoveryFollowLists
|
liveDiscoveryFollowLists
|
||||||
.transformLatest { followList ->
|
.transformLatest { followList ->
|
||||||
if (followList != null) {
|
if (followList != null) {
|
||||||
emitAll(combine(followList.usersPlusMe.map { getNIP65RelayListFlow(it) }) { it })
|
emitAll(combine(followList.authorsPlusMe.map { getNIP65RelayListFlow(it) }) { it })
|
||||||
} else {
|
} else {
|
||||||
emit(null)
|
emit(null)
|
||||||
}
|
}
|
||||||
@@ -870,7 +874,7 @@ class Account(
|
|||||||
scope,
|
scope,
|
||||||
SharingStarted.Eagerly,
|
SharingStarted.Eagerly,
|
||||||
authorsPerRelay(
|
authorsPerRelay(
|
||||||
liveDiscoveryFollowLists.value?.usersPlusMe?.map { getNIP65RelayListNote(it) } ?: emptyList(),
|
liveDiscoveryFollowLists.value?.authorsPlusMe?.map { getNIP65RelayListNote(it) } ?: emptyList(),
|
||||||
connectToRelays.value.filter { it.read }.map { it.url },
|
connectToRelays.value.filter { it.read }.map { it.url },
|
||||||
settings.torSettings.torType.value,
|
settings.torSettings.torType.value,
|
||||||
).ifEmpty { null },
|
).ifEmpty { null },
|
||||||
@@ -879,19 +883,17 @@ class Account(
|
|||||||
|
|
||||||
private fun decryptLiveFollows(
|
private fun decryptLiveFollows(
|
||||||
listEvent: GeneralListEvent,
|
listEvent: GeneralListEvent,
|
||||||
onReady: (LiveFollowLists) -> Unit,
|
onReady: (LiveFollowList) -> Unit,
|
||||||
) {
|
) {
|
||||||
listEvent.privateTags(signer) { privateTagList ->
|
listEvent.privateTags(signer) { privateTagList ->
|
||||||
val users = (listEvent.bookmarkedPeople() + listEvent.filterUsers(privateTagList)).toSet()
|
val users = (listEvent.bookmarkedPeople() + listEvent.filterUsers(privateTagList)).toSet()
|
||||||
onReady(
|
onReady(
|
||||||
LiveFollowLists(
|
LiveFollowList(
|
||||||
users = users,
|
authors = users,
|
||||||
usersPlusMe = users + userProfile().pubkeyHex,
|
authorsPlusMe = users + userProfile().pubkeyHex,
|
||||||
hashtags =
|
hashtags = (listEvent.hashtags() + listEvent.filterHashtags(privateTagList)).toSet(),
|
||||||
(listEvent.hashtags() + listEvent.filterHashtags(privateTagList)).toSet(),
|
geotags = (listEvent.geohashes() + listEvent.filterGeohashes(privateTagList)).toSet(),
|
||||||
geotags =
|
addresses =
|
||||||
(listEvent.geohashes() + listEvent.filterGeohashes(privateTagList)).toSet(),
|
|
||||||
communities =
|
|
||||||
(listEvent.taggedAddresses() + listEvent.filterAddresses(privateTagList))
|
(listEvent.taggedAddresses() + listEvent.filterAddresses(privateTagList))
|
||||||
.map { it.toTag() }
|
.map { it.toTag() }
|
||||||
.toSet(),
|
.toSet(),
|
||||||
@@ -900,7 +902,7 @@ class Account(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun waitToDecrypt(peopleListFollows: GeneralListEvent): LiveFollowLists? =
|
suspend fun waitToDecrypt(peopleListFollows: GeneralListEvent): LiveFollowList? =
|
||||||
withTimeoutOrNull(1000) {
|
withTimeoutOrNull(1000) {
|
||||||
suspendCancellableCoroutine { continuation ->
|
suspendCancellableCoroutine { continuation ->
|
||||||
decryptLiveFollows(peopleListFollows) {
|
decryptLiveFollows(peopleListFollows) {
|
||||||
@@ -3192,7 +3194,7 @@ class Account(
|
|||||||
flowHiddenUsers.value.hiddenUsers.contains(userHex) ||
|
flowHiddenUsers.value.hiddenUsers.contains(userHex) ||
|
||||||
flowHiddenUsers.value.spammers.contains(userHex)
|
flowHiddenUsers.value.spammers.contains(userHex)
|
||||||
|
|
||||||
fun followingKeySet(): Set<HexKey> = liveKind3Follows.value.users
|
fun followingKeySet(): Set<HexKey> = liveKind3Follows.value.authors
|
||||||
|
|
||||||
fun isAcceptable(user: User): Boolean {
|
fun isAcceptable(user: User): Boolean {
|
||||||
if (userProfile().pubkeyHex == user.pubkeyHex) {
|
if (userProfile().pubkeyHex == user.pubkeyHex) {
|
||||||
@@ -3250,8 +3252,8 @@ class Account(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
note.reportsBy(liveKind3Follows.value.usersPlusMe) +
|
note.reportsBy(liveKind3Follows.value.authorsPlusMe) +
|
||||||
(note.author?.reportsBy(liveKind3Follows.value.usersPlusMe) ?: emptyList()) +
|
(note.author?.reportsBy(liveKind3Follows.value.authorsPlusMe) ?: emptyList()) +
|
||||||
innerReports
|
innerReports
|
||||||
).toSet()
|
).toSet()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,9 @@ val GLOBAL_FOLLOWS = " Global "
|
|||||||
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
|
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
|
||||||
val KIND3_FOLLOWS = " All Follows "
|
val KIND3_FOLLOWS = " All Follows "
|
||||||
|
|
||||||
|
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
|
||||||
|
val AROUND_ME = " Around Me "
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class AccountSettings(
|
class AccountSettings(
|
||||||
val keyPair: KeyPair,
|
val keyPair: KeyPair,
|
||||||
|
|||||||
+69
-52
@@ -26,75 +26,92 @@ import android.location.Geocoder
|
|||||||
import android.location.Location
|
import android.location.Location
|
||||||
import android.location.LocationListener
|
import android.location.LocationListener
|
||||||
import android.location.LocationManager
|
import android.location.LocationManager
|
||||||
import android.os.HandlerThread
|
import android.os.Looper
|
||||||
import android.util.LruCache
|
import android.util.LruCache
|
||||||
import androidx.compose.runtime.MutableState
|
import com.fonfon.kgeohash.toGeoHash
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import com.vitorpamplona.amethyst.service.LocationState.Companion.MIN_DISTANCE
|
||||||
|
import com.vitorpamplona.amethyst.service.LocationState.Companion.MIN_TIME
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class LocationUtil(
|
class LocationFlow(
|
||||||
context: Context,
|
private val context: Context,
|
||||||
) {
|
) {
|
||||||
companion object {
|
|
||||||
const val MIN_TIME: Long = 1000L
|
|
||||||
const val MIN_DISTANCE: Float = 0.0f
|
|
||||||
}
|
|
||||||
|
|
||||||
private val locationManager =
|
|
||||||
context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
|
||||||
private var locationListener: LocationListener? = null
|
|
||||||
|
|
||||||
val locationStateFlow = MutableStateFlow<Location>(Location(LocationManager.NETWORK_PROVIDER))
|
|
||||||
val providerState = mutableStateOf(false)
|
|
||||||
val isStart: MutableState<Boolean> = mutableStateOf(false)
|
|
||||||
|
|
||||||
private val locHandlerThread = HandlerThread("LocationUtil Thread")
|
|
||||||
|
|
||||||
init {
|
|
||||||
locHandlerThread.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
fun start(
|
fun get(
|
||||||
minTimeMs: Long = MIN_TIME,
|
minTimeMs: Long = MIN_TIME,
|
||||||
minDistanceM: Float = MIN_DISTANCE,
|
minDistanceM: Float = MIN_DISTANCE,
|
||||||
) {
|
): Flow<Location> =
|
||||||
locationListener().let {
|
callbackFlow {
|
||||||
locationListener = it
|
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||||
|
|
||||||
|
val locationCallback =
|
||||||
|
object : LocationListener {
|
||||||
|
override fun onLocationChanged(location: Location) {
|
||||||
|
launch { send(location) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onProviderEnabled(provider: String) {}
|
||||||
|
|
||||||
|
override fun onProviderDisabled(provider: String) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
println("AABBCC LocationState Start")
|
||||||
locationManager.requestLocationUpdates(
|
locationManager.requestLocationUpdates(
|
||||||
LocationManager.NETWORK_PROVIDER,
|
LocationManager.NETWORK_PROVIDER,
|
||||||
minTimeMs,
|
minTimeMs,
|
||||||
minDistanceM,
|
minDistanceM,
|
||||||
it,
|
locationCallback,
|
||||||
locHandlerThread.looper,
|
Looper.getMainLooper(),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
providerState.value = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
|
|
||||||
isStart.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stop() {
|
awaitClose {
|
||||||
locationListener?.let { locationManager.removeUpdates(it) }
|
locationManager.removeUpdates(locationCallback)
|
||||||
isStart.value = false
|
println("AABBCC LocationState Stop")
|
||||||
}
|
|
||||||
|
|
||||||
private fun locationListener() =
|
|
||||||
object : LocationListener {
|
|
||||||
override fun onLocationChanged(location: Location) {
|
|
||||||
locationStateFlow.value = location
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onProviderEnabled(provider: String) {
|
|
||||||
providerState.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onProviderDisabled(provider: String) {
|
|
||||||
providerState.value = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class LocationState(
|
||||||
|
context: Context,
|
||||||
|
scope: CoroutineScope,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
const val MIN_TIME: Long = 10000L
|
||||||
|
const val MIN_DISTANCE: Float = 100.0f
|
||||||
|
}
|
||||||
|
|
||||||
|
private var latestLocation: Location = Location(LocationManager.NETWORK_PROVIDER)
|
||||||
|
|
||||||
|
val locationStateFlow =
|
||||||
|
LocationFlow(context)
|
||||||
|
.get(MIN_TIME, MIN_DISTANCE)
|
||||||
|
.onEach {
|
||||||
|
latestLocation = it
|
||||||
|
}.stateIn(
|
||||||
|
scope,
|
||||||
|
SharingStarted.WhileSubscribed(5000),
|
||||||
|
latestLocation,
|
||||||
|
)
|
||||||
|
|
||||||
|
val geohashStateFlow =
|
||||||
|
locationStateFlow
|
||||||
|
.map { it.toGeoHash(com.vitorpamplona.amethyst.ui.actions.GeohashPrecision.KM_5_X_5.digits).toString() }
|
||||||
|
.stateIn(
|
||||||
|
scope,
|
||||||
|
SharingStarted.WhileSubscribed(5000),
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
object CachedGeoLocations {
|
object CachedGeoLocations {
|
||||||
val locationNames = LruCache<String, String>(20)
|
val locationNames = LruCache<String, String>(20)
|
||||||
|
|
||||||
+1
-1
@@ -162,7 +162,7 @@ object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
|
|||||||
fun createLiveStreamFilter(): List<TypedFilter> {
|
fun createLiveStreamFilter(): List<TypedFilter> {
|
||||||
val follows =
|
val follows =
|
||||||
account.liveDiscoveryFollowLists.value
|
account.liveDiscoveryFollowLists.value
|
||||||
?.users
|
?.authors
|
||||||
?.toList()
|
?.toList()
|
||||||
?.ifEmpty { null }
|
?.ifEmpty { null }
|
||||||
|
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
|
|||||||
mapOf(
|
mapOf(
|
||||||
"g" to
|
"g" to
|
||||||
hashToLoad
|
hashToLoad
|
||||||
.map { listOf(it, it.lowercase(), it.uppercase(), it.capitalize()) }
|
.map { listOf(it.lowercase()) }
|
||||||
.flatten(),
|
.flatten(),
|
||||||
),
|
),
|
||||||
limit = 100,
|
limit = 100,
|
||||||
@@ -225,7 +225,7 @@ object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun createFollowCommunitiesFilter(): TypedFilter? {
|
fun createFollowCommunitiesFilter(): TypedFilter? {
|
||||||
val communitiesToLoad = account.liveHomeFollowLists.value?.communities ?: return null
|
val communitiesToLoad = account.liveHomeFollowLists.value?.addresses ?: return null
|
||||||
|
|
||||||
if (communitiesToLoad.isEmpty()) return null
|
if (communitiesToLoad.isEmpty()) return null
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import androidx.compose.ui.text.input.TextFieldValue
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.fonfon.kgeohash.toGeoHash
|
import com.fonfon.kgeohash.toGeoHash
|
||||||
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor
|
import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor
|
||||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
||||||
@@ -43,7 +44,6 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
|||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.FileHeader
|
import com.vitorpamplona.amethyst.service.FileHeader
|
||||||
import com.vitorpamplona.amethyst.service.LocationUtil
|
|
||||||
import com.vitorpamplona.amethyst.service.Nip96Uploader
|
import com.vitorpamplona.amethyst.service.Nip96Uploader
|
||||||
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
||||||
import com.vitorpamplona.amethyst.ui.components.MediaCompressor
|
import com.vitorpamplona.amethyst.ui.components.MediaCompressor
|
||||||
@@ -78,7 +78,10 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.mapLatest
|
import kotlinx.coroutines.flow.mapLatest
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
@@ -163,8 +166,7 @@ open class NewPostViewModel : ViewModel() {
|
|||||||
|
|
||||||
// GeoHash
|
// GeoHash
|
||||||
var wantsToAddGeoHash by mutableStateOf(false)
|
var wantsToAddGeoHash by mutableStateOf(false)
|
||||||
var locUtil: LocationUtil? = null
|
var location: StateFlow<String?>? = null
|
||||||
var location: Flow<String>? = null
|
|
||||||
|
|
||||||
// ZapRaiser
|
// ZapRaiser
|
||||||
var canAddZapRaiser by mutableStateOf(false)
|
var canAddZapRaiser by mutableStateOf(false)
|
||||||
@@ -530,14 +532,7 @@ open class NewPostViewModel : ViewModel() {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
val geoLocation = locUtil?.locationStateFlow?.value
|
val geoHash = location?.value
|
||||||
val geoHash =
|
|
||||||
if (wantsToAddGeoHash && geoLocation != null) {
|
|
||||||
geoLocation.toGeoHash(GeohashPrecision.KM_5_X_5.digits).toString()
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
val localZapRaiserAmount = if (wantsZapraiser) zapRaiserAmount else null
|
val localZapRaiserAmount = if (wantsZapraiser) zapRaiserAmount else null
|
||||||
|
|
||||||
nip95attachments.forEach {
|
nip95attachments.forEach {
|
||||||
@@ -1262,28 +1257,20 @@ open class NewPostViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
fun startLocation(context: Context) {
|
fun locationFlow(): Flow<String?> {
|
||||||
locUtil = LocationUtil(context)
|
if (location == null) {
|
||||||
locUtil?.let {
|
|
||||||
location =
|
location =
|
||||||
it.locationStateFlow.mapLatest { it.toGeoHash(GeohashPrecision.KM_5_X_5.digits).toString() }
|
Amethyst.instance.locationManager.locationStateFlow
|
||||||
saveDraft()
|
.mapLatest { it.toGeoHash(GeohashPrecision.KM_5_X_5.digits).toString() }
|
||||||
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
|
||||||
}
|
}
|
||||||
viewModelScope.launch(Dispatchers.IO) { locUtil?.start() }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stopLocation() {
|
return location!!
|
||||||
viewModelScope.launch(Dispatchers.IO) { locUtil?.stop() }
|
|
||||||
location = null
|
|
||||||
locUtil = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
Log.d("Init", "OnCleared: ${this.javaClass.simpleName}")
|
Log.d("Init", "OnCleared: ${this.javaClass.simpleName}")
|
||||||
viewModelScope.launch(Dispatchers.IO) { locUtil?.stop() }
|
|
||||||
location = null
|
|
||||||
locUtil = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleNIP04And24() {
|
fun toggleNIP04And24() {
|
||||||
@@ -1386,7 +1373,7 @@ open class NewPostViewModel : ViewModel() {
|
|||||||
elementList[i] = elementList[nextIndex].also { elementList[nextIndex] = "null" }
|
elementList[i] = elementList[nextIndex].also { elementList[nextIndex] = "null" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
elementList.removeLast()
|
elementList.removeAt(elementList.size - 1)
|
||||||
val newEntries = keyList.zip(elementList) { key, content -> Pair(key, content) }
|
val newEntries = keyList.zip(elementList) { key, content -> Pair(key, content) }
|
||||||
this.clear()
|
this.clear()
|
||||||
this.putAll(newEntries)
|
this.putAll(newEntries)
|
||||||
|
|||||||
+1
-1
@@ -143,7 +143,7 @@ class Kind3RelayListViewModel : ViewModel() {
|
|||||||
val proposed =
|
val proposed =
|
||||||
RelayListRecommendationProcessor
|
RelayListRecommendationProcessor
|
||||||
.reliableRelaySetFor(
|
.reliableRelaySetFor(
|
||||||
account.liveKind3Follows.value.users.mapNotNull {
|
account.liveKind3Follows.value.authors.mapNotNull {
|
||||||
account.getNIP65RelayList(it)
|
account.getNIP65RelayList(it)
|
||||||
},
|
},
|
||||||
relayUrlsToIgnore =
|
relayUrlsToIgnore =
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ open class DiscoverLiveFeedFilter(
|
|||||||
|
|
||||||
override fun sort(collection: Set<Note>): List<Note> {
|
override fun sort(collection: Set<Note>): List<Note> {
|
||||||
val followingKeySet =
|
val followingKeySet =
|
||||||
account.liveDiscoveryFollowLists.value?.users ?: account.liveKind3Follows.value.users
|
account.liveDiscoveryFollowLists.value?.authors ?: account.liveKind3Follows.value.authors
|
||||||
|
|
||||||
val counter = ParticipantListBuilder()
|
val counter = ParticipantListBuilder()
|
||||||
val participantCounts =
|
val participantCounts =
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils
|
|||||||
class FilterByListParams(
|
class FilterByListParams(
|
||||||
val isGlobal: Boolean,
|
val isGlobal: Boolean,
|
||||||
val isHiddenList: Boolean,
|
val isHiddenList: Boolean,
|
||||||
val followLists: Account.LiveFollowLists?,
|
val followLists: Account.LiveFollowList?,
|
||||||
val hiddenLists: Account.LiveHiddenUsers,
|
val hiddenLists: Account.LiveHiddenUsers,
|
||||||
val now: Long = TimeUtils.oneMinuteFromNow(),
|
val now: Long = TimeUtils.oneMinuteFromNow(),
|
||||||
) {
|
) {
|
||||||
@@ -45,22 +45,22 @@ class FilterByListParams(
|
|||||||
if (followLists == null) return false
|
if (followLists == null) return false
|
||||||
|
|
||||||
return if (noteEvent is LiveActivitiesEvent) {
|
return if (noteEvent is LiveActivitiesEvent) {
|
||||||
noteEvent.participantsIntersect(followLists.users) ||
|
noteEvent.participantsIntersect(followLists.authors) ||
|
||||||
noteEvent.isTaggedHashes(followLists.hashtags) ||
|
noteEvent.isTaggedHashes(followLists.hashtags) ||
|
||||||
noteEvent.isTaggedGeoHashes(followLists.geotags) ||
|
noteEvent.isTaggedGeoHashes(followLists.geotags) ||
|
||||||
noteEvent.isTaggedAddressableNotes(followLists.communities)
|
noteEvent.isTaggedAddressableNotes(followLists.addresses)
|
||||||
} else {
|
} else {
|
||||||
noteEvent.pubKey in followLists.users ||
|
noteEvent.pubKey in followLists.authors ||
|
||||||
noteEvent.isTaggedHashes(followLists.hashtags) ||
|
noteEvent.isTaggedHashes(followLists.hashtags) ||
|
||||||
noteEvent.isTaggedGeoHashes(followLists.geotags) ||
|
noteEvent.isTaggedGeoHashes(followLists.geotags) ||
|
||||||
noteEvent.isTaggedAddressableNotes(followLists.communities)
|
noteEvent.isTaggedAddressableNotes(followLists.addresses)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isATagInList(aTag: ATag): Boolean {
|
fun isATagInList(aTag: ATag): Boolean {
|
||||||
if (followLists == null) return false
|
if (followLists == null) return false
|
||||||
|
|
||||||
return aTag.pubKeyHex in followLists.users
|
return aTag.pubKeyHex in followLists.authors
|
||||||
}
|
}
|
||||||
|
|
||||||
fun match(
|
fun match(
|
||||||
@@ -89,7 +89,7 @@ class FilterByListParams(
|
|||||||
fun create(
|
fun create(
|
||||||
userHex: String,
|
userHex: String,
|
||||||
selectedListName: String,
|
selectedListName: String,
|
||||||
followLists: Account.LiveFollowLists?,
|
followLists: Account.LiveFollowList?,
|
||||||
hiddenUsers: Account.LiveHiddenUsers,
|
hiddenUsers: Account.LiveHiddenUsers,
|
||||||
): FilterByListParams =
|
): FilterByListParams =
|
||||||
FilterByListParams(
|
FilterByListParams(
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ class NotificationFeedFilter(
|
|||||||
it.event !is NIP90ContentDiscoveryRequestEvent &&
|
it.event !is NIP90ContentDiscoveryRequestEvent &&
|
||||||
it.event !is GiftWrapEvent &&
|
it.event !is GiftWrapEvent &&
|
||||||
(it.event is LnZapEvent || notifAuthor != loggedInUserHex) &&
|
(it.event is LnZapEvent || notifAuthor != loggedInUserHex) &&
|
||||||
(filterParams.isGlobal || filterParams.followLists?.users?.contains(notifAuthor) == true) &&
|
(filterParams.isGlobal || filterParams.followLists?.authors?.contains(notifAuthor) == true) &&
|
||||||
it.event?.isTaggedUser(loggedInUserHex) ?: false &&
|
it.event?.isTaggedUser(loggedInUserHex) ?: false &&
|
||||||
(filterParams.isHiddenList || notifAuthor == null || !account.isHidden(notifAuthor)) &&
|
(filterParams.isHiddenList || notifAuthor == null || !account.isHidden(notifAuthor)) &&
|
||||||
tagsAnEventByUser(it, loggedInUserHex)
|
tagsAnEventByUser(it, loggedInUserHex)
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class ThreadFeedFilter(
|
|||||||
|
|
||||||
override fun feed(): List<Note> {
|
override fun feed(): List<Note> {
|
||||||
val cachedSignatures: MutableMap<Note, LevelSignature> = mutableMapOf()
|
val cachedSignatures: MutableMap<Note, LevelSignature> = mutableMapOf()
|
||||||
val followingKeySet = account.liveKind3Follows.value.users
|
val followingKeySet = account.liveKind3Follows.value.authors
|
||||||
val eventsToWatch = ThreadAssembler().findThreadFor(noteId)
|
val eventsToWatch = ThreadAssembler().findThreadFor(noteId)
|
||||||
val eventsInHex = eventsToWatch.map { it.idHex }.toSet()
|
val eventsInHex = eventsToWatch.map { it.idHex }.toSet()
|
||||||
val now = TimeUtils.now()
|
val now = TimeUtils.now()
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ import com.vitorpamplona.amethyst.R
|
|||||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
import com.vitorpamplona.amethyst.model.FeatureSetType
|
||||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||||
import com.vitorpamplona.amethyst.ui.note.SearchIcon
|
import com.vitorpamplona.amethyst.ui.note.SearchIcon
|
||||||
import com.vitorpamplona.amethyst.ui.screen.CodeName
|
import com.vitorpamplona.amethyst.ui.screen.FeedDefinition
|
||||||
import com.vitorpamplona.amethyst.ui.screen.FollowListState
|
import com.vitorpamplona.amethyst.ui.screen.FollowListState
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
@@ -112,7 +112,7 @@ private fun LoggedInUserPictureDrawer(
|
|||||||
fun FollowListWithRoutes(
|
fun FollowListWithRoutes(
|
||||||
followListsModel: FollowListState,
|
followListsModel: FollowListState,
|
||||||
listName: String,
|
listName: String,
|
||||||
onChange: (CodeName) -> Unit,
|
onChange: (FeedDefinition) -> Unit,
|
||||||
) {
|
) {
|
||||||
val allLists by followListsModel.kind3GlobalPeopleRoutes.collectAsStateWithLifecycle()
|
val allLists by followListsModel.kind3GlobalPeopleRoutes.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
@@ -128,7 +128,7 @@ fun FollowListWithRoutes(
|
|||||||
fun FollowListWithoutRoutes(
|
fun FollowListWithoutRoutes(
|
||||||
followListsModel: FollowListState,
|
followListsModel: FollowListState,
|
||||||
listName: String,
|
listName: String,
|
||||||
onChange: (CodeName) -> Unit,
|
onChange: (FeedDefinition) -> Unit,
|
||||||
) {
|
) {
|
||||||
val allLists by followListsModel.kind3GlobalPeople.collectAsStateWithLifecycle()
|
val allLists by followListsModel.kind3GlobalPeople.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
|||||||
@@ -394,7 +394,7 @@ private fun FollowingAndFollowerCounts(
|
|||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text =
|
text =
|
||||||
followingCount.value.users.size
|
followingCount.value.authors.size
|
||||||
.toString(),
|
.toString(),
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
)
|
)
|
||||||
|
|||||||
+62
-6
@@ -20,10 +20,12 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.navigation
|
package com.vitorpamplona.amethyst.ui.navigation
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@@ -33,6 +35,8 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -41,12 +45,20 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.map
|
import androidx.lifecycle.map
|
||||||
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
|
import com.google.accompanist.permissions.isGranted
|
||||||
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation
|
||||||
import com.vitorpamplona.amethyst.ui.note.LoadCityName
|
import com.vitorpamplona.amethyst.ui.note.LoadCityName
|
||||||
import com.vitorpamplona.amethyst.ui.screen.CodeName
|
import com.vitorpamplona.amethyst.ui.screen.AroundMeFeedDefinition
|
||||||
import com.vitorpamplona.amethyst.ui.screen.CommunityName
|
import com.vitorpamplona.amethyst.ui.screen.CommunityName
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.FeedDefinition
|
||||||
import com.vitorpamplona.amethyst.ui.screen.GeoHashName
|
import com.vitorpamplona.amethyst.ui.screen.GeoHashName
|
||||||
import com.vitorpamplona.amethyst.ui.screen.HashtagName
|
import com.vitorpamplona.amethyst.ui.screen.HashtagName
|
||||||
import com.vitorpamplona.amethyst.ui.screen.Name
|
import com.vitorpamplona.amethyst.ui.screen.Name
|
||||||
@@ -55,15 +67,18 @@ import com.vitorpamplona.amethyst.ui.screen.ResourceName
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SpinnerSelectionDialog
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SpinnerSelectionDialog
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
import com.vitorpamplona.quartz.events.PeopleListEvent
|
import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun FeedFilterSpinner(
|
fun FeedFilterSpinner(
|
||||||
placeholderCode: String,
|
placeholderCode: String,
|
||||||
explainer: String,
|
explainer: String,
|
||||||
options: ImmutableList<CodeName>,
|
options: ImmutableList<FeedDefinition>,
|
||||||
onSelect: (Int) -> Unit,
|
onSelect: (Int) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
@@ -75,20 +90,61 @@ fun FeedFilterSpinner(
|
|||||||
id = R.string.select_an_option,
|
id = R.string.select_an_option,
|
||||||
)
|
)
|
||||||
|
|
||||||
var currentText by
|
var selected by
|
||||||
remember(placeholderCode, options) {
|
remember(placeholderCode, options) {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
options.firstOrNull { it.code == placeholderCode }?.name?.name(context) ?: selectAnOption,
|
options.firstOrNull { it.code == placeholderCode },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val currentText by
|
||||||
|
remember(placeholderCode, options) {
|
||||||
|
derivedStateOf {
|
||||||
|
selected?.name?.name(context) ?: selectAnOption
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Spacer(modifier = Size20Modifier)
|
Spacer(modifier = Size20Modifier)
|
||||||
Text(currentText)
|
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
Text(currentText)
|
||||||
|
|
||||||
|
if (selected is AroundMeFeedDefinition) {
|
||||||
|
val locationPermissionState =
|
||||||
|
rememberPermissionState(
|
||||||
|
Manifest.permission.ACCESS_COARSE_LOCATION,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!locationPermissionState.status.isGranted) {
|
||||||
|
LaunchedEffect(locationPermissionState) { locationPermissionState.launchPermissionRequest() }
|
||||||
|
} else {
|
||||||
|
val location by Amethyst.instance.locationManager.geohashStateFlow
|
||||||
|
.collectAsStateWithLifecycle(null)
|
||||||
|
|
||||||
|
location?.let {
|
||||||
|
LoadCityName(
|
||||||
|
geohashStr = it,
|
||||||
|
onLoading = {
|
||||||
|
Spacer(modifier = StdHorzSpacer)
|
||||||
|
LoadingAnimation()
|
||||||
|
},
|
||||||
|
) { cityName ->
|
||||||
|
Text(
|
||||||
|
text = "($cityName)",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
lineHeight = 12.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.ExpandMore,
|
imageVector = Icons.Default.ExpandMore,
|
||||||
contentDescription = explainer,
|
contentDescription = explainer,
|
||||||
@@ -115,7 +171,7 @@ fun FeedFilterSpinner(
|
|||||||
options = options,
|
options = options,
|
||||||
onDismiss = { optionsShowing = false },
|
onDismiss = { optionsShowing = false },
|
||||||
onSelect = {
|
onSelect = {
|
||||||
currentText = options[it].name.name(context)
|
selected = options[it]
|
||||||
optionsShowing = false
|
optionsShowing = false
|
||||||
onSelect(it)
|
onSelect(it)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -671,13 +671,13 @@ fun LoadModerators(
|
|||||||
|
|
||||||
val followingKeySet =
|
val followingKeySet =
|
||||||
accountViewModel.account.liveDiscoveryFollowLists.value
|
accountViewModel.account.liveDiscoveryFollowLists.value
|
||||||
?.users
|
?.authors
|
||||||
val allParticipants =
|
val allParticipants =
|
||||||
ParticipantListBuilder().followsThatParticipateOn(baseNote, followingKeySet).minus(hosts)
|
ParticipantListBuilder().followsThatParticipateOn(baseNote, followingKeySet).minus(hosts)
|
||||||
|
|
||||||
val newParticipantUsers =
|
val newParticipantUsers =
|
||||||
if (followingKeySet == null) {
|
if (followingKeySet == null) {
|
||||||
val allFollows = accountViewModel.account.liveKind3Follows.value.users
|
val allFollows = accountViewModel.account.liveKind3Follows.value.authors
|
||||||
val followingParticipants =
|
val followingParticipants =
|
||||||
ParticipantListBuilder().followsThatParticipateOn(baseNote, allFollows).minus(hosts)
|
ParticipantListBuilder().followsThatParticipateOn(baseNote, allFollows).minus(hosts)
|
||||||
|
|
||||||
@@ -724,7 +724,7 @@ private fun LoadParticipants(
|
|||||||
|
|
||||||
val followingKeySet =
|
val followingKeySet =
|
||||||
accountViewModel.account.liveDiscoveryFollowLists.value
|
accountViewModel.account.liveDiscoveryFollowLists.value
|
||||||
?.users
|
?.authors
|
||||||
|
|
||||||
val allParticipants =
|
val allParticipants =
|
||||||
ParticipantListBuilder()
|
ParticipantListBuilder()
|
||||||
@@ -733,7 +733,7 @@ private fun LoadParticipants(
|
|||||||
|
|
||||||
val newParticipantUsers =
|
val newParticipantUsers =
|
||||||
if (followingKeySet == null) {
|
if (followingKeySet == null) {
|
||||||
val allFollows = accountViewModel.account.liveKind3Follows.value.users
|
val allFollows = accountViewModel.account.liveKind3Follows.value.authors
|
||||||
val followingParticipants =
|
val followingParticipants =
|
||||||
ParticipantListBuilder()
|
ParticipantListBuilder()
|
||||||
.followsThatParticipateOn(baseNote, allFollows)
|
.followsThatParticipateOn(baseNote, allFollows)
|
||||||
@@ -882,7 +882,7 @@ fun RenderChannelThumb(
|
|||||||
launch(Dispatchers.IO) {
|
launch(Dispatchers.IO) {
|
||||||
val followingKeySet =
|
val followingKeySet =
|
||||||
accountViewModel.account.liveDiscoveryFollowLists.value
|
accountViewModel.account.liveDiscoveryFollowLists.value
|
||||||
?.users
|
?.authors
|
||||||
val allParticipants =
|
val allParticipants =
|
||||||
ParticipantListBuilder()
|
ParticipantListBuilder()
|
||||||
.followsThatParticipateOn(baseNote, followingKeySet)
|
.followsThatParticipateOn(baseNote, followingKeySet)
|
||||||
@@ -890,7 +890,7 @@ fun RenderChannelThumb(
|
|||||||
|
|
||||||
val newParticipantUsers =
|
val newParticipantUsers =
|
||||||
if (followingKeySet == null) {
|
if (followingKeySet == null) {
|
||||||
val allFollows = accountViewModel.account.liveKind3Follows.value.users
|
val allFollows = accountViewModel.account.liveKind3Follows.value.authors
|
||||||
val followingParticipants =
|
val followingParticipants =
|
||||||
ParticipantListBuilder().followsThatParticipateOn(baseNote, allFollows).toList()
|
ParticipantListBuilder().followsThatParticipateOn(baseNote, allFollows).toList()
|
||||||
|
|
||||||
|
|||||||
@@ -373,6 +373,6 @@ fun WatchUserFollows(
|
|||||||
} else {
|
} else {
|
||||||
val state by accountViewModel.account.liveKind3Follows.collectAsStateWithLifecycle()
|
val state by accountViewModel.account.liveKind3Follows.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
onFollowChanges(state.users.contains(userHex))
|
onFollowChanges(state.authors.contains(userHex))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -355,5 +355,5 @@ fun WatchAddressableNoteFollows(
|
|||||||
) {
|
) {
|
||||||
val state by accountViewModel.account.liveKind3Follows.collectAsStateWithLifecycle()
|
val state by accountViewModel.account.liveKind3Follows.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
onFollowChanges(state.communities.contains(note.idHex))
|
onFollowChanges(state.addresses.contains(note.idHex))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import android.util.Log
|
|||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.AROUND_ME
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||||
@@ -33,10 +34,24 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
|||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.quartz.events.AudioHeaderEvent
|
||||||
|
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
||||||
|
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||||
|
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
|
||||||
import com.vitorpamplona.quartz.events.ContactListEvent
|
import com.vitorpamplona.quartz.events.ContactListEvent
|
||||||
import com.vitorpamplona.quartz.events.DeletionEvent
|
import com.vitorpamplona.quartz.events.DeletionEvent
|
||||||
|
import com.vitorpamplona.quartz.events.GenericRepostEvent
|
||||||
|
import com.vitorpamplona.quartz.events.HighlightEvent
|
||||||
|
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
|
||||||
|
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
|
||||||
|
import com.vitorpamplona.quartz.events.LongTextNoteEvent
|
||||||
import com.vitorpamplona.quartz.events.MuteListEvent
|
import com.vitorpamplona.quartz.events.MuteListEvent
|
||||||
import com.vitorpamplona.quartz.events.PeopleListEvent
|
import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||||
|
import com.vitorpamplona.quartz.events.PinListEvent
|
||||||
|
import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||||
|
import com.vitorpamplona.quartz.events.RepostEvent
|
||||||
|
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||||
|
import com.vitorpamplona.quartz.events.WikiNoteEvent
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -56,33 +71,56 @@ class FollowListState(
|
|||||||
val viewModelScope: CoroutineScope,
|
val viewModelScope: CoroutineScope,
|
||||||
) {
|
) {
|
||||||
val kind3Follow =
|
val kind3Follow =
|
||||||
CodeName(
|
PeopleListOutBoxFeedDefinition(
|
||||||
KIND3_FOLLOWS,
|
code = KIND3_FOLLOWS,
|
||||||
ResourceName(R.string.follow_list_kind3follows),
|
name = ResourceName(R.string.follow_list_kind3follows),
|
||||||
CodeNameType.HARDCODED,
|
type = CodeNameType.HARDCODED,
|
||||||
|
kinds = DEFAULT_FEED_KINDS,
|
||||||
|
unpackList = listOf(ContactListEvent.blockListFor(account.signer.pubKey)),
|
||||||
)
|
)
|
||||||
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> =
|
val globalFollow =
|
||||||
|
GlobalFeedDefinition(
|
||||||
|
code = GLOBAL_FOLLOWS,
|
||||||
|
name = ResourceName(R.string.follow_list_global),
|
||||||
|
type = CodeNameType.HARDCODED,
|
||||||
|
kinds = DEFAULT_FEED_KINDS,
|
||||||
|
relays = account.activeGlobalRelays().toList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
val aroundMe =
|
||||||
|
AroundMeFeedDefinition(
|
||||||
|
code = AROUND_ME,
|
||||||
|
name = ResourceName(R.string.follow_list_aroundme),
|
||||||
|
type = CodeNameType.HARDCODED,
|
||||||
|
kinds = DEFAULT_FEED_KINDS,
|
||||||
|
)
|
||||||
|
|
||||||
|
val muteListFollow =
|
||||||
|
PeopleListOutBoxFeedDefinition(
|
||||||
|
code = MuteListEvent.blockListFor(account.userProfile().pubkeyHex),
|
||||||
|
name = ResourceName(R.string.follow_list_mute_list),
|
||||||
|
type = CodeNameType.HARDCODED,
|
||||||
|
kinds = DEFAULT_FEED_KINDS,
|
||||||
|
unpackList = listOf(MuteListEvent.blockListFor(account.userProfile().pubkeyHex)),
|
||||||
|
)
|
||||||
|
|
||||||
|
val defaultLists = persistentListOf(kind3Follow, globalFollow, aroundMe, muteListFollow)
|
||||||
|
|
||||||
|
fun getPeopleLists(): List<FeedDefinition> =
|
||||||
account
|
account
|
||||||
.getAllPeopleLists()
|
.getAllPeopleLists()
|
||||||
.map {
|
.map {
|
||||||
CodeName(
|
PeopleListOutBoxFeedDefinition(
|
||||||
it.idHex,
|
it.idHex,
|
||||||
PeopleListName(it),
|
PeopleListName(it),
|
||||||
CodeNameType.PEOPLE_LIST,
|
CodeNameType.PEOPLE_LIST,
|
||||||
|
kinds = DEFAULT_FEED_KINDS,
|
||||||
|
listOf(it.idHex),
|
||||||
)
|
)
|
||||||
}.sortedBy { it.name.name() }
|
}.sortedBy { it.name.name() }
|
||||||
|
|
||||||
val livePeopleListsFlow = MutableStateFlow(emptyList<CodeName>())
|
val livePeopleListsFlow = MutableStateFlow(emptyList<FeedDefinition>())
|
||||||
|
|
||||||
fun updateFeedWith(newNotes: Set<Note>) {
|
fun updateFeedWith(newNotes: Set<Note>) {
|
||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
@@ -118,29 +156,46 @@ class FollowListState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
val liveKind3FollowsFlow: Flow<List<CodeName>> =
|
val liveKind3FollowsFlow: Flow<List<FeedDefinition>> =
|
||||||
account.liveKind3Follows.transformLatest {
|
account.liveKind3Follows.transformLatest {
|
||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
|
|
||||||
val communities =
|
val communities =
|
||||||
it.communities.mapNotNull {
|
it.addresses.mapNotNull {
|
||||||
LocalCache.checkGetOrCreateAddressableNote(it)?.let { communityNote ->
|
LocalCache.checkGetOrCreateAddressableNote(it)?.let { communityNote ->
|
||||||
CodeName(
|
TagFeedDefinition(
|
||||||
"Community/${communityNote.idHex}",
|
"Community/${communityNote.idHex}",
|
||||||
CommunityName(communityNote),
|
CommunityName(communityNote),
|
||||||
CodeNameType.ROUTE,
|
CodeNameType.ROUTE,
|
||||||
|
kinds = DEFAULT_COMMUNITY_FEEDS,
|
||||||
|
aTags = listOf(communityNote.idHex),
|
||||||
|
relays = account.activeGlobalRelays().toList(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val hashtags =
|
val hashtags =
|
||||||
it.hashtags.map {
|
it.hashtags.map {
|
||||||
CodeName("Hashtag/$it", HashtagName(it), CodeNameType.ROUTE)
|
TagFeedDefinition(
|
||||||
|
"Hashtag/$it",
|
||||||
|
HashtagName(it),
|
||||||
|
CodeNameType.ROUTE,
|
||||||
|
kinds = DEFAULT_FEED_KINDS,
|
||||||
|
tTags = listOf(it),
|
||||||
|
relays = account.activeGlobalRelays().toList(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val geotags =
|
val geotags =
|
||||||
it.geotags.map {
|
it.geotags.map {
|
||||||
CodeName("Geohash/$it", GeoHashName(it), CodeNameType.ROUTE)
|
TagFeedDefinition(
|
||||||
|
"Geohash/$it",
|
||||||
|
GeoHashName(it),
|
||||||
|
CodeNameType.ROUTE,
|
||||||
|
kinds = DEFAULT_FEED_KINDS,
|
||||||
|
gTags = listOf(it),
|
||||||
|
relays = account.activeGlobalRelays().toList(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
@@ -156,7 +211,7 @@ class FollowListState(
|
|||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
emit(
|
emit(
|
||||||
listOf(
|
listOf(
|
||||||
listOf(kind3Follow, globalFollow),
|
listOf(kind3Follow, aroundMe, globalFollow),
|
||||||
myLivePeopleListsFlow,
|
myLivePeopleListsFlow,
|
||||||
myLiveKind3FollowsFlow,
|
myLiveKind3FollowsFlow,
|
||||||
listOf(muteListFollow),
|
listOf(muteListFollow),
|
||||||
@@ -172,7 +227,7 @@ class FollowListState(
|
|||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
emit(
|
emit(
|
||||||
listOf(
|
listOf(
|
||||||
listOf(kind3Follow, globalFollow),
|
listOf(kind3Follow, aroundMe, globalFollow),
|
||||||
myLivePeopleListsFlow,
|
myLivePeopleListsFlow,
|
||||||
listOf(muteListFollow),
|
listOf(muteListFollow),
|
||||||
).flatten().toImmutableList(),
|
).flatten().toImmutableList(),
|
||||||
@@ -238,8 +293,78 @@ class CommunityName(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class CodeName(
|
abstract class FeedDefinition(
|
||||||
val code: String,
|
val code: String,
|
||||||
val name: Name,
|
val name: Name,
|
||||||
val type: CodeNameType,
|
val type: CodeNameType,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
class GlobalFeedDefinition(
|
||||||
|
code: String,
|
||||||
|
name: Name,
|
||||||
|
type: CodeNameType,
|
||||||
|
val kinds: List<Int>,
|
||||||
|
val relays: List<String>,
|
||||||
|
) : FeedDefinition(code, name, type)
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
class TagFeedDefinition(
|
||||||
|
code: String,
|
||||||
|
name: Name,
|
||||||
|
type: CodeNameType,
|
||||||
|
val kinds: List<Int>,
|
||||||
|
val relays: List<String>,
|
||||||
|
val pTags: List<String>? = null,
|
||||||
|
val eTags: List<String>? = null,
|
||||||
|
val aTags: List<String>? = null,
|
||||||
|
val tTags: List<String>? = null,
|
||||||
|
val gTags: List<String>? = null,
|
||||||
|
) : FeedDefinition(code, name, type)
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
class AroundMeFeedDefinition(
|
||||||
|
code: String,
|
||||||
|
name: Name,
|
||||||
|
type: CodeNameType,
|
||||||
|
val kinds: List<Int>,
|
||||||
|
) : FeedDefinition(code, name, type)
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
class PeopleListOutBoxFeedDefinition(
|
||||||
|
code: String,
|
||||||
|
name: Name,
|
||||||
|
type: CodeNameType,
|
||||||
|
val kinds: List<Int>,
|
||||||
|
val unpackList: List<String>,
|
||||||
|
) : FeedDefinition(code, name, type)
|
||||||
|
|
||||||
|
val DEFAULT_FEED_KINDS =
|
||||||
|
listOf(
|
||||||
|
TextNoteEvent.KIND,
|
||||||
|
RepostEvent.KIND,
|
||||||
|
GenericRepostEvent.KIND,
|
||||||
|
ClassifiedsEvent.KIND,
|
||||||
|
LongTextNoteEvent.KIND,
|
||||||
|
PollNoteEvent.KIND,
|
||||||
|
HighlightEvent.KIND,
|
||||||
|
AudioTrackEvent.KIND,
|
||||||
|
AudioHeaderEvent.KIND,
|
||||||
|
PinListEvent.KIND,
|
||||||
|
LiveActivitiesChatMessageEvent.KIND,
|
||||||
|
LiveActivitiesEvent.KIND,
|
||||||
|
WikiNoteEvent.KIND,
|
||||||
|
)
|
||||||
|
|
||||||
|
val DEFAULT_COMMUNITY_FEEDS =
|
||||||
|
listOf(
|
||||||
|
TextNoteEvent.KIND,
|
||||||
|
LongTextNoteEvent.KIND,
|
||||||
|
ClassifiedsEvent.KIND,
|
||||||
|
HighlightEvent.KIND,
|
||||||
|
AudioHeaderEvent.KIND,
|
||||||
|
AudioTrackEvent.KIND,
|
||||||
|
PinListEvent.KIND,
|
||||||
|
WikiNoteEvent.KIND,
|
||||||
|
CommunityPostApprovalEvent.KIND,
|
||||||
|
)
|
||||||
|
|||||||
+1
-2
@@ -120,7 +120,6 @@ import kotlinx.coroutines.flow.flatMapLatest
|
|||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.joinAll
|
import kotlinx.coroutines.joinAll
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
@@ -396,7 +395,7 @@ class AccountViewModel(
|
|||||||
note.flow().metadata.stateFlow,
|
note.flow().metadata.stateFlow,
|
||||||
note.flow().reports.stateFlow,
|
note.flow().reports.stateFlow,
|
||||||
) { hiddenUsers, followingUsers, autor, metadata, reports ->
|
) { hiddenUsers, followingUsers, autor, metadata, reports ->
|
||||||
emit(isNoteAcceptable(metadata.note, hiddenUsers, followingUsers.users))
|
emit(isNoteAcceptable(metadata.note, hiddenUsers, followingUsers.authors))
|
||||||
}.flowOn(Dispatchers.Default)
|
}.flowOn(Dispatchers.Default)
|
||||||
.stateIn(
|
.stateIn(
|
||||||
viewModelScope,
|
viewModelScope,
|
||||||
|
|||||||
+2
-15
@@ -188,7 +188,6 @@ import kotlinx.coroutines.CancellationException
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
@@ -1335,21 +1334,9 @@ fun LocationAsHash(postViewModel: NewPostViewModel) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DisplayLocationObserver(postViewModel: NewPostViewModel) {
|
fun DisplayLocationObserver(postViewModel: NewPostViewModel) {
|
||||||
val context = LocalContext.current
|
val location by postViewModel.locationFlow().collectAsStateWithLifecycle(null)
|
||||||
var locationDescriptionFlow by remember(postViewModel) { mutableStateOf<Flow<String>?>(null) }
|
|
||||||
|
|
||||||
DisposableEffect(key1 = context) {
|
location?.let { DisplayLocationInTitle(geohash = it) }
|
||||||
postViewModel.startLocation(context = context)
|
|
||||||
locationDescriptionFlow = postViewModel.location
|
|
||||||
|
|
||||||
onDispose { postViewModel.stopLocation() }
|
|
||||||
}
|
|
||||||
|
|
||||||
locationDescriptionFlow?.let {
|
|
||||||
val location by it.collectAsStateWithLifecycle(null)
|
|
||||||
|
|
||||||
location?.let { DisplayLocationInTitle(geohash = it) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -416,6 +416,7 @@
|
|||||||
|
|
||||||
<string name="follow_list_selection">Follow List</string>
|
<string name="follow_list_selection">Follow List</string>
|
||||||
<string name="follow_list_kind3follows">All Follows</string>
|
<string name="follow_list_kind3follows">All Follows</string>
|
||||||
|
<string name="follow_list_aroundme">Around Me</string>
|
||||||
<string name="follow_list_global">Global</string>
|
<string name="follow_list_global">Global</string>
|
||||||
<string name="follow_list_mute_list">Mute List</string>
|
<string name="follow_list_mute_list">Mute List</string>
|
||||||
|
|
||||||
|
|||||||
@@ -119,6 +119,8 @@ class ContactListEvent(
|
|||||||
const val KIND = 3
|
const val KIND = 3
|
||||||
const val ALT = "Follow List"
|
const val ALT = "Follow List"
|
||||||
|
|
||||||
|
fun blockListFor(pubKeyHex: HexKey): String = "3:$pubKeyHex:"
|
||||||
|
|
||||||
fun createFromScratch(
|
fun createFromScratch(
|
||||||
followUsers: List<Contact> = emptyList(),
|
followUsers: List<Contact> = emptyList(),
|
||||||
followTags: List<String> = emptyList(),
|
followTags: List<String> = emptyList(),
|
||||||
|
|||||||
Reference in New Issue
Block a user